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

@@ -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()) {
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.GuiHost;
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.pack.StructurePackageClosure;
import art.arcane.iris.core.project.IrisProjectCopier;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
@@ -33,6 +34,7 @@ import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedPackInstaller;
@@ -635,10 +637,12 @@ public final class ModdedStudioCommands {
LinkedHashSet<String> generatorKeys = new LinkedHashSet<>();
LinkedHashSet<String> lootKeys = new LinkedHashSet<>();
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
LinkedHashSet<String> structureKeys = new LinkedHashSet<>();
regionKeys.addAll(dimension.getRegions());
lootKeys.addAll(dimension.getLoot().getTables());
spawnerKeys.addAll(dimension.getEntitySpawners());
collectStructureKeys(structureKeys, dimension.getStructures());
for (String regionKey : regionKeys) {
IrisRegion region = dm.getRegionLoader().load(regionKey);
@@ -652,6 +656,7 @@ public final class ModdedStudioCommands {
});
lootKeys.addAll(region.getLoot().getTables());
spawnerKeys.addAll(region.getEntitySpawners());
collectStructureKeys(structureKeys, region.getStructures());
}
for (String biomeKey : biomeKeys) {
IrisBiome biome = dm.getBiomeLoader().load(biomeKey);
@@ -661,6 +666,7 @@ public final class ModdedStudioCommands {
biome.getGenerators().forEach((IrisBiomeGeneratorLink link) -> generatorKeys.add(link.getGenerator()));
lootKeys.addAll(biome.getLoot().getTables());
spawnerKeys.addAll(biome.getEntitySpawners());
collectStructureKeys(structureKeys, biome.getStructures());
for (IrisObjectPlacement placement : biome.getObjects()) {
objectKeys.addAll(placement.getPlace());
}
@@ -674,6 +680,11 @@ public final class ModdedStudioCommands {
}
StringBuilder hashes = new StringBuilder();
StructurePackageClosure structureClosure = StructurePackageClosure.collect(packFolder, structureKeys);
if (!structureClosure.isValid()) {
throw new IOException("Structure package closure is invalid: " + String.join("; ", structureClosure.errors()));
}
hashes.append(structureClosure.writeTo(folder, true));
for (String objectKey : objectKeys) {
try {
File objectFile = dm.getObjectLoader().findFile(objectKey);
@@ -716,6 +727,20 @@ public final class ModdedStudioCommands {
return output;
}
private static void collectStructureKeys(LinkedHashSet<String> keys, List<IrisStructurePlacement> placements) {
if (placements == null) {
return;
}
for (IrisStructurePlacement placement : placements) {
if (placement == null || placement.getStructures() == null) {
continue;
}
for (String structureKey : placement.getStructures()) {
keys.add(structureKey);
}
}
}
private static String copyJson(File folder, String category, String key, File file) {
if (file == null || !file.isFile()) {
return "";
@@ -19,8 +19,7 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.MainWorldService;
@@ -29,6 +28,7 @@ import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedModConfig;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.modded.ModdedPrimaryWorldRouter;
import art.arcane.iris.modded.ModdedStartup;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
@@ -187,10 +187,10 @@ public final class ModdedWorldCommands {
}
private static int enableInstalled(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, String packDimension, long seed) {
if (!loadPackDimension(source, pack, packDimension)) {
if (blockIfPackBroken(source, dimensionId, pack)) {
return 0;
}
if (blockIfPackBroken(source, dimensionId, pack)) {
if (!loadPackDimension(source, pack, packDimension)) {
return 0;
}
try {
@@ -218,10 +218,10 @@ public final class ModdedWorldCommands {
return 0;
}
String dimensionId = DEFAULT_NAMESPACE + ":primary";
if (!loadPackDimension(source, pack, packDimension)) {
if (blockIfPackBroken(source, dimensionId, pack)) {
return 0;
}
if (blockIfPackBroken(source, dimensionId, pack)) {
if (!loadPackDimension(source, pack, packDimension)) {
return 0;
}
try {
@@ -290,6 +290,9 @@ public final class ModdedWorldCommands {
}
private static int applyMainWorld(CommandSourceStack source, String pack, String packDimension, String packRef, long seed) {
if (blockIfPackBroken(source, "the main world", pack)) {
return 0;
}
try {
if (!loadPackDimension(source, pack, packDimension)) {
return 0;
@@ -299,9 +302,6 @@ public final class ModdedWorldCommands {
IrisModdedCommands.fail(source, "Pack '" + pack + "' is not ready yet (still loading or validating). Try the command again in a moment.");
return 0;
}
if (blockIfPackBroken(source, "the main world", pack)) {
return 0;
}
ModdedModConfig.setMainWorld(packRef, seed);
if (!MainWorldService.stage(packRef, seed)) {
IrisModdedCommands.fail(source, "Failed to write server.properties; check file permissions and set level-type manually.");
@@ -321,16 +321,18 @@ public final class ModdedWorldCommands {
}
private static boolean blockIfPackBroken(CommandSourceStack source, String dimensionId, String pack) {
PackValidationResult validation = PackValidationRegistry.get(pack);
if (validation == null || validation.isLoadable()) {
try {
ModdedStartup.requirePackForWorldCreation(pack);
return false;
} catch (BrokenPackException e) {
IrisModdedCommands.fail(source, "Refusing to create world '" + dimensionId
+ "' using pack '" + pack + "' because required validation failed:");
for (String reason : e.getReasons()) {
IrisModdedCommands.fail(source, " - " + reason);
}
IrisModdedCommands.fail(source, "Fix the pack and run /iris pack validate " + pack + " to revalidate.");
return true;
}
IrisModdedCommands.fail(source, "Refusing to create world '" + dimensionId + "' using broken pack '" + pack + "':");
for (String reason : validation.getBlockingErrors()) {
IrisModdedCommands.fail(source, " - " + reason);
}
IrisModdedCommands.fail(source, "Fix the pack and run /iris pack validate " + pack + " to revalidate.");
return true;
}
private static boolean loadPackDimension(CommandSourceStack source, String pack, String packDimension) {
@@ -34,8 +34,6 @@ import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterUpdate;
@@ -287,7 +285,6 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
}
}
RNG rng = new RNG(Cache.key(chunkX, chunkZ));
chunk.iterate(MatterCavern.class, (Integer x, Integer yf, Integer z, MatterCavern v) -> {
int y = yf + minHeight;
if (y < level.getMinY() || y >= level.getMaxY()) {
@@ -315,20 +312,20 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
if (grid[x][z] == Integer.MIN_VALUE) {
continue;
}
update(engine, level, x, grid[x][z], z, baseX, baseZ, chunk, rng);
update(engine, level, x, grid[x][z], z, baseX, baseZ, chunk);
}
}
chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> {
if (v != null && v.isUpdate()) {
update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk, rng);
update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk);
}
});
chunk.deleteSlices(MatterUpdate.class);
engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds());
}
private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk, RNG rng) {
private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk) {
if (y < level.getMinY() || y >= level.getMaxY()) {
return;
}
@@ -339,9 +336,8 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
if (!ModdedBlockResolution.isStorageChest(state)) {
return;
}
RNG rx = rng.nextParallelRNG(BlockPosition.toLong(x, y, z));
try {
ModdedLootApplier.apply(engine, level, pos, state, chunk, rx, x & 15, z & 15);
ModdedLootApplier.apply(engine, level, pos, state, chunk);
} catch (Throwable e) {
IrisLogging.reportError(e);
}
@@ -0,0 +1,620 @@
/*
* 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.modded.structure;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.util.common.math.IrisBlockVector;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.mojang.datafixers.util.Pair;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.core.Vec3i;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.JigsawBlock;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.TerrainAdjustment;
import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement;
import net.minecraft.world.level.levelgen.structure.pools.FeaturePoolElement;
import net.minecraft.world.level.levelgen.structure.pools.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.pools.alias.PoolAliasBinding;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
final class ModdedJigsawStructureCapture {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private final MinecraftServer server;
private final StructureKey sourceKey;
private final StructureKey targetKey;
private final StructureSource.Kind sourceKind;
private final Registry<Structure> structureRegistry;
private final Registry<StructureTemplatePool> poolRegistry;
private final Registry<Block> blockRegistry;
private final StructureTemplateManager templateManager;
private final RegistryOps<Tag> registryOps;
private final Map<ResourceKey<StructureTemplatePool>, ResourceKey<StructureTemplatePool>> aliases;
private final Map<String, byte[]> objects;
private final Map<String, Map<String, Object>> pieces;
private final Map<String, Map<String, Object>> pools;
private final EnumSet<StructureCapability> capabilities;
private final List<StructureLoss> losses;
private final Set<String> recordedLosses;
private final Set<String> visitedPools;
private final Deque<String> pendingPools;
private int blocks;
private ModdedJigsawStructureCapture(
MinecraftServer server,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind
) {
this.server = Objects.requireNonNull(server);
this.sourceKey = Objects.requireNonNull(sourceKey);
this.targetKey = Objects.requireNonNull(targetKey);
this.sourceKind = Objects.requireNonNull(sourceKind);
structureRegistry = server.registryAccess().lookupOrThrow(Registries.STRUCTURE);
poolRegistry = server.registryAccess().lookupOrThrow(Registries.TEMPLATE_POOL);
blockRegistry = server.registryAccess().lookupOrThrow(Registries.BLOCK);
templateManager = server.getStructureManager();
registryOps = RegistryOps.create(NbtOps.INSTANCE, server.registryAccess());
aliases = new HashMap<>();
objects = new LinkedHashMap<>();
pieces = new LinkedHashMap<>();
pools = new LinkedHashMap<>();
capabilities = EnumSet.of(
StructureCapability.BLOCKS,
StructureCapability.CONNECTORS,
StructureCapability.IRIS_PLACEMENT
);
losses = new ArrayList<>();
recordedLosses = new HashSet<>();
visitedPools = new HashSet<>();
pendingPools = new ArrayDeque<>();
}
static Capture capture(
MinecraftServer server,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind
) throws IOException {
return new ModdedJigsawStructureCapture(server, sourceKey, targetKey, sourceKind).capture();
}
private Capture capture() throws IOException {
Identifier sourceIdentifier = identifier(sourceKey);
Structure structure = structureRegistry.getValue(sourceIdentifier);
if (structure == null) {
throw new IllegalArgumentException("No registered structure exists for " + sourceKey);
}
if (!(structure instanceof JigsawStructure jigsaw)) {
throw new UnsupportedStructureTypeException("Structure " + sourceKey + " uses "
+ structure.getClass().getSimpleName() + "; only jigsaw structure graphs can be converted to Iris assembly resources");
}
CompoundTag encodedStructure = encodeStructure(structure);
configureAliases(jigsaw);
recordRootLosses(jigsaw);
String startPoolKey = poolKey(jigsaw.getStartPool().value());
pendingPools.add(startPoolKey);
while (!pendingPools.isEmpty()) {
capturePool(pendingPools.removeFirst());
}
if (pieces.isEmpty()) {
throw new IllegalStateException("Structure " + sourceKey + " produced no importable pieces");
}
int maxDepth = Math.max(1, encodedStructure.getIntOr("size", 1));
int maxDistance = readHorizontalDistance(encodedStructure);
StructureSource source = StructureSource.identified(
sourceKind,
sourceKey,
SharedConstants.getCurrentVersion().name(),
NbtUtils.structureToSnbt(encodedStructure).getBytes(StandardCharsets.UTF_8)
);
StructureResourceBundle bundle = buildBundle(source, startPoolKey, maxDepth, maxDistance);
return new Capture(bundle, blocks, pieces.size(), pools.size());
}
private CompoundTag encodeStructure(Structure structure) {
Tag encoded = Structure.DIRECT_CODEC.encodeStart(registryOps, structure).getOrThrow();
if (!(encoded instanceof CompoundTag compound)) {
throw new IllegalStateException("Structure codec did not produce a compound for " + sourceKey);
}
return compound;
}
private void configureAliases(JigsawStructure structure) {
List<PoolAliasBinding> bindings = structure.getPoolAliases();
if (bindings.isEmpty()) {
return;
}
RandomSource random = RandomSource.create(stableSeed(sourceKey.value()));
for (PoolAliasBinding binding : bindings) {
binding.forEachResolved(random, aliases::put);
}
addLossOnce(
"pool_aliases_resolved_once",
StructureLoss.warning(
StructureCapability.CONNECTORS,
"pool_aliases_resolved_once",
bindings.size() + " native pool alias binding(s) were resolved deterministically for the imported graph; per-placement alias variation is not represented."
)
);
}
private void recordRootLosses(JigsawStructure structure) {
losses.add(StructureLoss.warning(
StructureCapability.NATIVE_PLACEMENT,
"native_placement_settings_not_imported",
"Native start height, heightmap projection, expansion, padding, and placement-set settings are not represented by Iris assembly placement."
));
losses.add(StructureLoss.warning(
StructureCapability.LIQUID_SETTINGS,
"native_liquid_settings_not_imported",
"Native structure liquid placement behavior is not represented beyond the captured block and waterlogged states."
));
if (structure.terrainAdaptation() != TerrainAdjustment.NONE) {
losses.add(StructureLoss.warning(
StructureCapability.TERRAIN_ADAPTATION,
"terrain_adaptation_not_imported",
"Native terrain adaptation '" + structure.terrainAdaptation().getSerializedName()
+ "' is not represented by the Iris assembly."
));
}
}
private void capturePool(String sourcePoolKey) throws IOException {
if (!visitedPools.add(sourcePoolKey)) {
return;
}
Identifier sourcePoolIdentifier = Identifier.tryParse(sourcePoolKey);
StructureTemplatePool pool = sourcePoolIdentifier == null ? null : poolRegistry.getValue(sourcePoolIdentifier);
if (pool == null) {
throw new IllegalStateException("Jigsaw graph references missing template pool " + sourcePoolKey);
}
String irisPoolName = poolName(targetKey.path(), sourcePoolKey);
List<Object> entries = new ArrayList<>();
List<Pair<StructurePoolElement, Integer>> templates = pool.getTemplates();
for (int index = 0; index < templates.size(); index++) {
Pair<StructurePoolElement, Integer> weighted = templates.get(index);
StructurePoolElement element = weighted.getFirst();
int weight = Math.max(1, weighted.getSecond());
Map<String, Object> entry = new LinkedHashMap<>();
if (element == EmptyPoolElement.INSTANCE) {
entry.put("empty", true);
} else {
entry.put("piece", captureElement(sourcePoolKey, index, element));
}
entry.put("weight", weight);
entries.add(entry);
}
Map<String, Object> poolJson = new LinkedHashMap<>();
poolJson.put("pieces", entries);
String fallback = resolvedPoolKey(pool.getFallback().value());
if (!fallback.equals(sourcePoolKey)) {
poolJson.put("fallback", poolName(targetKey.path(), fallback));
pendingPools.addLast(fallback);
}
pools.put(irisPoolName, poolJson);
}
private String captureElement(String sourcePoolKey, int index, StructurePoolElement element) throws IOException {
Objects.requireNonNull(element, "pool element");
if (element instanceof SinglePoolElement single) {
return captureSingle(single);
}
String generatedName = generatedPieceName(targetKey.path(), sourcePoolKey, index, element);
if (pieces.containsKey(generatedName)) {
return generatedName;
}
if (element instanceof ListPoolElement list) {
capabilities.add(StructureCapability.LIST_ELEMENTS);
CompositeCapture composite = captureList(list, generatedName);
blocks += composite.object().getBlocks().size();
emitPiece(generatedName, composite.object(), composite.losses(), element);
return generatedName;
}
IrisObject object = emptyObject(element);
StructureCapability unsupported = element instanceof FeaturePoolElement
? StructureCapability.FEATURE_ELEMENTS : StructureCapability.BLOCKS;
StructureLoss loss = StructureLoss.warning(
unsupported,
"unsupported_pool_element",
"Pool element " + element.getClass().getSimpleName() + " was represented as an empty Iris piece."
);
emitPiece(generatedName, object, List.of(loss), element);
return generatedName;
}
private String captureSingle(SinglePoolElement element) throws IOException {
Identifier templateIdentifier = element.getTemplateLocation();
StructureKey templateKey = StructureKey.parse(templateIdentifier.toString());
boolean legacy = element instanceof LegacySinglePoolElement;
String pieceName = legacy
? legacyPieceName(targetKey.path(), templateKey.value())
: pieceName(targetKey.path(), templateKey.value());
if (pieces.containsKey(pieceName)) {
return pieceName;
}
StructureTemplate template = templateManager.get(templateIdentifier)
.orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
templateKey, template, blockRegistry, true, !legacy);
capabilities.addAll(capture.capabilities());
blocks += capture.blocks();
List<StructureLoss> pieceLosses = new ArrayList<>(capture.losses());
pieceLosses.addAll(elementLosses(element));
emitPiece(pieceName, capture.object(), pieceLosses, element);
return pieceName;
}
private CompositeCapture captureList(ListPoolElement list, String pieceName) throws IOException {
Vec3i size = list.getSize(templateManager, Rotation.NONE);
IrisObject composite = new IrisObject(
Math.max(1, size.getX()),
Math.max(1, size.getY()),
Math.max(1, size.getZ())
);
List<StructureLoss> compositeLosses = new ArrayList<>();
for (StructurePoolElement child : list.getElements()) {
if (child == EmptyPoolElement.INSTANCE) {
continue;
}
if (child instanceof SinglePoolElement single) {
Identifier templateIdentifier = single.getTemplateLocation();
StructureTemplate template = templateManager.get(templateIdentifier)
.orElseThrow(() -> new IllegalStateException("Missing structure template " + templateIdentifier));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
StructureKey.parse(templateIdentifier.toString()),
template,
blockRegistry,
true,
!(single instanceof LegacySinglePoolElement)
);
merge(composite, capture.object());
capabilities.addAll(capture.capabilities());
compositeLosses.addAll(capture.losses());
compositeLosses.addAll(elementLosses(single));
continue;
}
if (child instanceof ListPoolElement nested) {
CompositeCapture nestedCapture = captureList(nested, pieceName);
merge(composite, nestedCapture.object());
compositeLosses.addAll(nestedCapture.losses());
continue;
}
StructureCapability unsupported = child instanceof FeaturePoolElement
? StructureCapability.FEATURE_ELEMENTS : StructureCapability.LIST_ELEMENTS;
compositeLosses.add(StructureLoss.warning(
unsupported,
"list_child_not_imported",
"List element child " + child.getClass().getSimpleName() + " could not be flattened into the Iris object."
));
}
compositeLosses.addAll(elementLosses(list));
return new CompositeCapture(composite, compositeLosses);
}
private List<StructureLoss> elementLosses(StructurePoolElement element) {
List<StructureLoss> elementLosses = new ArrayList<>();
if (element.getProjection() != StructureTemplatePool.Projection.RIGID) {
elementLosses.add(StructureLoss.warning(
StructureCapability.PROJECTION,
"terrain_matching_projection_not_imported",
"Pool projection '" + element.getProjection().getSerializedName()
+ "' was converted to rigid Iris piece placement."
));
}
Tag encoded = StructurePoolElement.CODEC.encodeStart(registryOps, element).getOrThrow();
if (encoded instanceof CompoundTag compound) {
String processors = compound.getStringOr("processors", "");
if (!processors.isEmpty() && !processors.equals("minecraft:empty")) {
elementLosses.add(StructureLoss.warning(
StructureCapability.PROCESSORS,
"native_processors_not_imported",
"Native processor list '" + processors + "' was not applied to the captured template."
));
}
if (compound.contains("override_liquid_settings")) {
elementLosses.add(StructureLoss.warning(
StructureCapability.LIQUID_SETTINGS,
"element_liquid_settings_not_imported",
"The pool element's liquid setting override is not represented by Iris placement."
));
}
}
return elementLosses;
}
private void emitPiece(
String pieceName,
IrisObject object,
List<StructureLoss> pieceLosses,
StructurePoolElement element
) throws IOException {
String objectResource = "objects/" + pieceName + ".iob";
objects.put(pieceName, serialize(object));
for (StructureLoss loss : pieceLosses) {
losses.add(loss.affecting(objectResource));
}
List<Map<String, Object>> connectors = connectors(element, pieceName);
Map<String, Object> pieceJson = new LinkedHashMap<>();
pieceJson.put("object", pieceName);
pieceJson.put("connectors", connectors);
pieceJson.put("rotatable", true);
pieces.put(pieceName, pieceJson);
}
private List<Map<String, Object>> connectors(StructurePoolElement element, String pieceName) {
List<StructureTemplate.JigsawBlockInfo> sourceConnectors = element.getShuffledJigsawBlocks(
templateManager,
BlockPos.ZERO,
Rotation.NONE,
RandomSource.create(stableSeed(sourceKey.value() + ":" + pieceName))
);
List<Map<String, Object>> connectors = new ArrayList<>(sourceConnectors.size());
for (StructureTemplate.JigsawBlockInfo source : sourceConnectors) {
recordConnectorLosses(source, pieceName);
ResourceKey<StructureTemplatePool> resolvedPool = aliases.getOrDefault(source.pool(), source.pool());
String sourcePoolKey = resolvedPool.identifier().toString();
pendingPools.addLast(sourcePoolKey);
Map<String, Object> position = new LinkedHashMap<>();
position.put("x", source.info().pos().getX());
position.put("y", source.info().pos().getY());
position.put("z", source.info().pos().getZ());
Map<String, Object> connector = new LinkedHashMap<>();
connector.put("position", position);
connector.put("direction", directionName(JigsawBlock.getFrontFacing(source.info().state())));
connector.put("top", directionName(JigsawBlock.getTopFacing(source.info().state())));
connector.put("pool", poolName(targetKey.path(), sourcePoolKey));
connector.put("name", source.name().toString());
connector.put("targetName", source.target().toString());
connector.put("joint", source.jointType().getSerializedName().equals("aligned") ? "ALIGNED" : "ROLLABLE");
connectors.add(connector);
}
return connectors;
}
private void recordConnectorLosses(StructureTemplate.JigsawBlockInfo connector, String pieceName) {
String pieceResource = "jigsaw-pieces/" + pieceName + ".json";
if (connector.placementPriority() != 0 || connector.selectionPriority() != 0) {
addLossOnce(
"connector-priority:" + pieceName,
StructureLoss.warning(
StructureCapability.CONNECTORS,
"connector_priorities_not_imported",
"Native connector placement and selection priorities are not represented by Iris assembly ordering."
).affecting(pieceResource)
);
}
}
private StructureResourceBundle buildBundle(
StructureSource source,
String startPoolKey,
int maxDepth,
int maxDistance
) {
StructureResourceBundle.Builder bundle = StructureResourceBundle.builder(targetKey)
.source(source)
.backend(StructureBackend.IRIS_ASSEMBLY)
.capabilities(capabilities)
.losses(losses);
for (Map.Entry<String, byte[]> entry : objects.entrySet()) {
bundle.resource("objects/" + entry.getKey() + ".iob", entry.getValue());
}
for (Map.Entry<String, Map<String, Object>> entry : pieces.entrySet()) {
bundle.textResource("jigsaw-pieces/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
for (Map.Entry<String, Map<String, Object>> entry : pools.entrySet()) {
bundle.textResource("jigsaw-pools/" + entry.getKey() + ".json", GSON.toJson(entry.getValue()));
}
bundle.textResource(
"structures/" + targetKey.path() + ".json",
GSON.toJson(structureJson(sourceKey.value(), poolName(targetKey.path(), startPoolKey), maxDepth, maxDistance))
);
return bundle.build();
}
private String resolvedPoolKey(StructureTemplatePool pool) {
ResourceKey<StructureTemplatePool> raw = poolResourceKey(pool);
return aliases.getOrDefault(raw, raw).identifier().toString();
}
private String poolKey(StructureTemplatePool pool) {
return poolResourceKey(pool).identifier().toString();
}
private ResourceKey<StructureTemplatePool> poolResourceKey(StructureTemplatePool pool) {
Identifier identifier = poolRegistry.getKey(pool);
if (identifier == null) {
throw new IllegalStateException("Jigsaw structure references an unregistered template pool");
}
return ResourceKey.create(Registries.TEMPLATE_POOL, identifier);
}
private void addLossOnce(String key, StructureLoss loss) {
if (recordedLosses.add(key)) {
losses.add(loss);
}
}
private static void merge(IrisObject target, IrisObject source) {
for (IrisBlockVector position : source.getBlocks().keys()) {
int x = position.getBlockX() + source.getCenter().getX();
int y = position.getBlockY() + source.getCenter().getY();
int z = position.getBlockZ() + source.getCenter().getZ();
if (x < 0 || y < 0 || z < 0 || x >= target.getW() || y >= target.getH() || z >= target.getD()) {
continue;
}
target.setUnsignedTile(x, y, z, null);
target.setUnsigned(x, y, z, source.getBlocks().get(position));
TileData tile = source.getStates().get(position);
if (tile != null) {
target.setUnsignedTile(x, y, z, tile);
}
}
}
private static IrisObject emptyObject() {
return new IrisObject(1, 1, 1);
}
private IrisObject emptyObject(StructurePoolElement element) {
Vec3i size = element.getSize(templateManager, Rotation.NONE);
return new IrisObject(
Math.max(1, size.getX()),
Math.max(1, size.getY()),
Math.max(1, size.getZ())
);
}
private static byte[] serialize(IrisObject object) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
object.write(output);
return output.toByteArray();
}
private static int readHorizontalDistance(CompoundTag encodedStructure) {
int scalar = encodedStructure.getIntOr("max_distance_from_center", -1);
if (scalar > 0) {
return scalar;
}
CompoundTag compound = encodedStructure.getCompoundOrEmpty("max_distance_from_center");
return Math.max(1, compound.getIntOr("horizontal", 80));
}
static Map<String, Object> structureJson(String source, String startPool, int maxDepth, int maxDistance) {
Map<String, Object> root = new LinkedHashMap<>();
root.put("startPool", startPool);
root.put("maxDepth", Math.max(1, Math.min(30, maxDepth)));
root.put("maxSizeChunks", Math.max(1, Math.min(32, (Math.max(1, maxDistance) + 15) / 16)));
root.put("placeMode", "STRUCTURE_PIECE");
root.put("vanillaSource", source);
return root;
}
static String poolName(String base, String sourcePoolKey) {
StructureKey key = StructureKey.parse(sourcePoolKey);
return base + "/pool/" + key.namespace() + "/" + key.path();
}
static String pieceName(String base, String templateKey) {
StructureKey key = StructureKey.parse(templateKey);
return base + "/piece/" + key.namespace() + "/" + key.path();
}
static String legacyPieceName(String base, String templateKey) {
StructureKey key = StructureKey.parse(templateKey);
return base + "/piece/generated/legacy/" + key.namespace() + "/" + key.path();
}
static String directionName(Direction direction) {
return switch (direction) {
case UP -> "UP_POSITIVE_Y";
case DOWN -> "DOWN_NEGATIVE_Y";
case SOUTH -> "SOUTH_POSITIVE_Z";
case EAST -> "EAST_POSITIVE_X";
case WEST -> "WEST_NEGATIVE_X";
case NORTH -> "NORTH_NEGATIVE_Z";
};
}
private static String generatedPieceName(String base, String sourcePoolKey, int index, StructurePoolElement element) {
StructureKey poolKey = StructureKey.parse(sourcePoolKey);
String type = element instanceof ListPoolElement ? "list" : element instanceof FeaturePoolElement ? "feature" : "unsupported";
return base + "/piece/generated/" + type + "/" + poolKey.namespace() + "/" + poolKey.path() + "/" + index;
}
private static long stableSeed(String value) {
long hash = 0xcbf29ce484222325L;
for (int index = 0; index < value.length(); index++) {
hash ^= value.charAt(index);
hash *= 0x100000001b3L;
}
return hash;
}
private static Identifier identifier(StructureKey key) {
return Identifier.fromNamespaceAndPath(key.namespace(), key.path());
}
record Capture(StructureResourceBundle bundle, int blocks, int pieces, int pools) {
Capture {
Objects.requireNonNull(bundle);
}
}
static final class UnsupportedStructureTypeException extends IllegalArgumentException {
UnsupportedStructureTypeException(String message) {
super(message);
}
}
private record CompositeCapture(IrisObject object, List<StructureLoss> losses) {
CompositeCapture {
Objects.requireNonNull(object);
losses = List.copyOf(losses);
}
}
}
@@ -0,0 +1,426 @@
/*
* 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.modded.structure;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.authoring.IrisStructureBundleFactory;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import net.minecraft.SharedConstants;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
public final class ModdedStructureImportService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private final Supplier<MinecraftServer> server;
public ModdedStructureImportService(Supplier<MinecraftServer> server) {
this.server = Objects.requireNonNull(server);
}
public List<StructureKey> templateKeys() throws StructureImportException {
try {
MinecraftServer activeServer = requireServerThread();
return activeServer.getStructureManager().listTemplates()
.map((Identifier identifier) -> StructureKey.parse(identifier.toString()))
.sorted()
.toList();
} catch (RuntimeException failure) {
throw report("Failed to list native structure templates", failure);
}
}
public List<StructureKey> jigsawStructureKeys() throws StructureImportException {
try {
MinecraftServer activeServer = requireServerThread();
Registry<Structure> registry = activeServer.registryAccess().lookupOrThrow(Registries.STRUCTURE);
List<StructureKey> keys = new ArrayList<>();
for (Identifier identifier : registry.keySet()) {
if (registry.getValue(identifier) instanceof JigsawStructure) {
keys.add(StructureKey.parse(identifier.toString()));
}
}
keys.sort(Comparator.naturalOrder());
return List.copyOf(keys);
} catch (RuntimeException failure) {
throw report("Failed to list registered jigsaw structures", failure);
}
}
public PreparedImport prepareTemplate(TemplateImportOptions options) throws StructureImportException {
Objects.requireNonNull(options);
try {
MinecraftServer activeServer = requireServerThread();
Identifier sourceIdentifier = identifier(options.sourceKey());
StructureTemplate template = activeServer.getStructureManager().get(sourceIdentifier)
.orElseThrow(() -> new IllegalArgumentException("No structure template exists for " + options.sourceKey()));
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.capture(
options.sourceKey(),
template,
activeServer.registryAccess().lookupOrThrow(Registries.BLOCK),
false
);
StructureSource source = StructureSource.identified(
options.sourceKind(),
options.sourceKey(),
SharedConstants.getCurrentVersion().name(),
NbtUtils.structureToSnbt(capture.sourceTag()).getBytes(StandardCharsets.UTF_8)
);
IrisStructureBundleFactory.SinglePieceOptions bundleOptions = new IrisStructureBundleFactory.SinglePieceOptions(
options.targetKey(),
source,
options.targetKey().path(),
capture.object(),
Math.max(capture.width(), capture.depth()),
options.placeMode(),
options.objectOnly(),
capture.capabilities(),
capture.losses()
);
StructureResourceBundle bundle = IrisStructureBundleFactory.singlePiece(bundleOptions);
if (!options.objectOnly()) {
StructureResourceBundleGraphCompiler.requireViable(bundle);
}
return new PreparedImport(
options.packRoot(),
options.writeOptions(),
ImportKind.TEMPLATE,
bundle,
capture.blocks(),
1,
options.objectOnly() ? 0 : 1
);
} catch (IOException | RuntimeException failure) {
throw report("Failed to prepare structure template import " + options.sourceKey(), failure);
}
}
public PreparedImport prepareJigsawStructure(JigsawImportOptions options) throws StructureImportException {
Objects.requireNonNull(options);
try {
MinecraftServer activeServer = requireServerThread();
ModdedJigsawStructureCapture.Capture capture = ModdedJigsawStructureCapture.capture(
activeServer,
options.sourceKey(),
options.targetKey(),
options.sourceKind()
);
StructureResourceBundleGraphCompiler.requireViable(capture.bundle());
return new PreparedImport(
options.packRoot(),
options.writeOptions(),
ImportKind.JIGSAW_STRUCTURE,
capture.bundle(),
capture.blocks(),
capture.pieces(),
capture.pools()
);
} catch (ModdedJigsawStructureCapture.UnsupportedStructureTypeException failure) {
StructureLoss loss = StructureLoss.error(
StructureCapability.IRIS_PLACEMENT,
"unsupported_native_structure_type",
failure.getMessage()
);
throw report("Cannot import native structure graph " + options.sourceKey(), failure, List.of(loss));
} catch (IOException | RuntimeException failure) {
throw report("Failed to prepare jigsaw structure import " + options.sourceKey(), failure);
}
}
public ImportResult write(PreparedImport prepared) {
Objects.requireNonNull(prepared);
StructureWriteResult writeResult = new StructureTransactionWriter(prepared.packRoot())
.write(prepared.bundle(), prepared.writeOptions());
writeResult.failure().ifPresent((Throwable failure) -> LOGGER.error(
"Failed to commit modded structure import {} to {}",
prepared.bundle().key(),
prepared.packRoot(),
failure
));
if (writeResult.committed()) {
IrisData.getLoaded(prepared.packRoot().toFile()).ifPresent(IrisData::invalidateStructureResources);
}
String message = writeMessage(prepared, writeResult);
return new ImportResult(
writeResult.successful(),
message,
prepared.kind(),
prepared.bundle().key(),
prepared.blocks(),
prepared.pieces(),
prepared.pools(),
prepared.bundle().capabilities(),
prepared.bundle().losses(),
Optional.of(writeResult)
);
}
public ImportResult importTemplate(TemplateImportOptions options) {
try {
return write(prepareTemplate(options));
} catch (StructureImportException failure) {
return failed(options.targetKey(), ImportKind.TEMPLATE, failure.getMessage(), failure.losses());
}
}
public ImportResult importJigsawStructure(JigsawImportOptions options) {
try {
return write(prepareJigsawStructure(options));
} catch (StructureImportException failure) {
return failed(options.targetKey(), ImportKind.JIGSAW_STRUCTURE, failure.getMessage(), failure.losses());
}
}
private MinecraftServer requireServerThread() {
MinecraftServer activeServer = server.get();
if (activeServer == null) {
throw new IllegalStateException("Minecraft server is not available");
}
if (!activeServer.isSameThread()) {
throw new IllegalStateException("Structure registry capture must run on the logical server thread");
}
return activeServer;
}
private StructureImportException report(String context, Exception failure) {
return report(context, failure, List.of());
}
private StructureImportException report(String context, Exception failure, List<StructureLoss> losses) {
LOGGER.error(context, failure);
return new StructureImportException(context + ": " + failureDetail(failure), failure, losses);
}
private static ImportResult failed(
StructureKey targetKey,
ImportKind kind,
String message,
List<StructureLoss> losses
) {
return new ImportResult(
false,
message,
kind,
targetKey,
0,
0,
0,
Set.of(),
losses,
Optional.empty()
);
}
private static String writeMessage(PreparedImport prepared, StructureWriteResult result) {
if (result.successful()) {
return switch (result.status()) {
case DRY_RUN -> "Validated import of '" + prepared.bundle().key() + "' without writing files";
case ADDED -> "Imported '" + prepared.bundle().key() + "'";
case OVERWRITTEN -> "Overwrote owned import '" + prepared.bundle().key() + "'";
case UNCHANGED -> "Import '" + prepared.bundle().key() + "' is already current";
case COMMITTED_CLEANUP_REQUIRED -> "Imported '" + prepared.bundle().key()
+ "'; obsolete staging cleanup is still required";
default -> "Imported '" + prepared.bundle().key() + "'";
};
}
if (!result.conflicts().isEmpty()) {
StructureWriteResult.Conflict conflict = result.conflicts().getFirst();
return "Import conflict for '" + prepared.bundle().key() + "': " + conflict.relativePath()
+ " is " + conflict.reason().name().toLowerCase() + "; existing files were preserved";
}
return "Import failed for '" + prepared.bundle().key() + "': "
+ result.failure().map(ModdedStructureImportService::failureDetail).orElse(result.status().name());
}
private static String failureDetail(Throwable failure) {
String message = failure.getMessage();
return message == null || message.isBlank() ? failure.getClass().getSimpleName() : message;
}
private static Identifier identifier(StructureKey key) {
return Identifier.fromNamespaceAndPath(key.namespace(), key.path());
}
public enum ImportKind {
TEMPLATE,
JIGSAW_STRUCTURE
}
public record TemplateImportOptions(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind,
StructureWriteOptions writeOptions,
boolean objectOnly,
String placeMode
) {
public TemplateImportOptions {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(targetKey);
Objects.requireNonNull(sourceKind);
Objects.requireNonNull(writeOptions);
Objects.requireNonNull(placeMode);
if (placeMode.isBlank()) {
throw new IllegalArgumentException("Structure place mode cannot be blank");
}
}
public static TemplateImportOptions create(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureWriteMode mode
) {
return new TemplateImportOptions(
packRoot,
sourceKey,
targetKey,
inferredSourceKind(sourceKey),
new StructureWriteOptions(mode, false),
false,
"CENTER_HEIGHT"
);
}
}
public record JigsawImportOptions(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureSource.Kind sourceKind,
StructureWriteOptions writeOptions
) {
public JigsawImportOptions {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(targetKey);
Objects.requireNonNull(sourceKind);
Objects.requireNonNull(writeOptions);
}
public static JigsawImportOptions create(
Path packRoot,
StructureKey sourceKey,
StructureKey targetKey,
StructureWriteMode mode
) {
return new JigsawImportOptions(
packRoot,
sourceKey,
targetKey,
inferredSourceKind(sourceKey),
new StructureWriteOptions(mode, false)
);
}
}
public record PreparedImport(
Path packRoot,
StructureWriteOptions writeOptions,
ImportKind kind,
StructureResourceBundle bundle,
int blocks,
int pieces,
int pools
) {
public PreparedImport {
packRoot = normalizedRoot(packRoot);
Objects.requireNonNull(writeOptions);
Objects.requireNonNull(kind);
Objects.requireNonNull(bundle);
if (blocks < 0 || pieces < 0 || pools < 0) {
throw new IllegalArgumentException("Prepared import counts cannot be negative");
}
}
}
public record ImportResult(
boolean success,
String message,
ImportKind kind,
StructureKey targetKey,
int blocks,
int pieces,
int pools,
Set<StructureCapability> capabilities,
List<StructureLoss> losses,
Optional<StructureWriteResult> writeResult
) {
public ImportResult {
Objects.requireNonNull(message);
Objects.requireNonNull(kind);
Objects.requireNonNull(targetKey);
capabilities = Set.copyOf(capabilities);
losses = List.copyOf(losses);
Objects.requireNonNull(writeResult);
}
}
public static final class StructureImportException extends Exception {
private final List<StructureLoss> losses;
public StructureImportException(String message, Throwable cause, List<StructureLoss> losses) {
super(message, cause);
this.losses = List.copyOf(losses);
}
public List<StructureLoss> losses() {
return losses;
}
}
private static Path normalizedRoot(Path path) {
return Objects.requireNonNull(path).toAbsolutePath().normalize();
}
private static StructureSource.Kind inferredSourceKind(StructureKey sourceKey) {
return sourceKey.namespace().equals("minecraft")
? StructureSource.Kind.VANILLA : StructureSource.Kind.DATAPACK;
}
}
@@ -0,0 +1,339 @@
/*
* 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.modded.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureLoss;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.modded.ModdedBlockResolution;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.modded.ModdedTileData;
import net.minecraft.core.HolderGetter;
import net.minecraft.core.Vec3i;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
final class ModdedStructureTemplateCapture {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private ModdedStructureTemplateCapture() {
}
static Capture capture(
StructureKey sourceKey,
StructureTemplate template,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved
) {
return capture(sourceKey, template, blockLookup, connectorsPreserved, true);
}
static Capture capture(
StructureKey sourceKey,
StructureTemplate template,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved,
boolean includeAir
) {
Objects.requireNonNull(template);
CompoundTag sourceTag = template.save(new CompoundTag());
return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, includeAir);
}
static Capture captureTag(
StructureKey sourceKey,
CompoundTag sourceTag,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved
) {
return captureTag(sourceKey, sourceTag, blockLookup, connectorsPreserved, true);
}
static Capture captureTag(
StructureKey sourceKey,
CompoundTag sourceTag,
HolderGetter<Block> blockLookup,
boolean connectorsPreserved,
boolean includeAir
) {
Objects.requireNonNull(sourceKey);
Objects.requireNonNull(sourceTag);
Objects.requireNonNull(blockLookup);
Vec3i size = readSize(sourceTag);
IrisObject object = new IrisObject(size.getX(), size.getY(), size.getZ());
List<StructureLoss> losses = new ArrayList<>();
ListTag palette = firstPalette(sourceTag, losses);
ListTag blocks = sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG);
CaptureCounts counts = captureBlocks(sourceKey, palette, blocks, blockLookup, object, losses, includeAir);
recordEntityLoss(sourceTag, losses);
recordMarkerLosses(counts, connectorsPreserved, losses);
EnumSet<StructureCapability> capabilities = EnumSet.of(StructureCapability.BLOCKS);
if (counts.tiles() > 0) {
capabilities.add(StructureCapability.BLOCK_ENTITIES);
}
if (connectorsPreserved && counts.jigsaws() > 0) {
capabilities.add(StructureCapability.CONNECTORS);
}
return new Capture(
object,
counts.blocks(),
counts.tiles(),
counts.jigsaws(),
counts.dataMarkers(),
size.getX(),
size.getY(),
size.getZ(),
capabilities,
losses,
sourceTag.copy()
);
}
private static Vec3i readSize(CompoundTag sourceTag) {
ListTag size = sourceTag.getListOrEmpty(StructureTemplate.SIZE_TAG);
int width = size.getIntOr(0, 0);
int height = size.getIntOr(1, 0);
int depth = size.getIntOr(2, 0);
if (width < 1 || height < 1 || depth < 1) {
throw new IllegalArgumentException("Structure template has invalid dimensions "
+ width + "x" + height + "x" + depth);
}
return new Vec3i(width, height, depth);
}
private static ListTag firstPalette(CompoundTag sourceTag, List<StructureLoss> losses) {
ListTag palettes = sourceTag.getList(StructureTemplate.PALETTE_LIST_TAG).orElse(null);
if (palettes != null) {
if (palettes.isEmpty()) {
throw new IllegalArgumentException("Structure template has no palettes");
}
if (palettes.size() > 1) {
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"palette_variants_not_imported",
"Only palette 0 was converted; " + (palettes.size() - 1)
+ " additional palette(s) remain native-only."
));
}
return palettes.getListOrEmpty(0);
}
ListTag palette = sourceTag.getListOrEmpty(StructureTemplate.PALETTE_TAG);
if (palette.isEmpty() && !sourceTag.getListOrEmpty(StructureTemplate.BLOCKS_TAG).isEmpty()) {
throw new IllegalArgumentException("Structure template has blocks but no palette");
}
return palette;
}
private static CaptureCounts captureBlocks(
StructureKey sourceKey,
ListTag palette,
ListTag blocks,
HolderGetter<Block> blockLookup,
IrisObject object,
List<StructureLoss> losses,
boolean includeAir
) {
int blockCount = 0;
int tiles = 0;
int jigsaws = 0;
int dataMarkers = 0;
for (int index = 0; index < blocks.size(); index++) {
CompoundTag blockTag = blocks.getCompoundOrEmpty(index);
BlockPosition position = readPosition(blockTag);
if (!withinObject(position, object)) {
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"out_of_bounds_block_not_imported",
"Block " + index + " at " + position.x() + "," + position.y() + "," + position.z()
+ " is outside the declared template bounds."
));
continue;
}
int paletteIndex = blockTag.getIntOr(StructureTemplate.BLOCK_TAG_STATE, 0);
BlockState state = NbtUtils.readBlockState(blockLookup, palette.getCompoundOrEmpty(paletteIndex));
CompoundTag blockEntityTag = blockTag.getCompound(StructureTemplate.BLOCK_TAG_NBT).orElse(null);
if (state.is(Blocks.STRUCTURE_VOID)) {
continue;
}
if (state.is(Blocks.STRUCTURE_BLOCK)) {
dataMarkers++;
continue;
}
if (state.is(Blocks.JIGSAW)) {
jigsaws++;
BlockState finalState = resolveJigsawFinalState(sourceKey, position, blockEntityTag, losses);
if (finalState == null || finalState.isAir()) {
continue;
}
state = finalState;
blockEntityTag = null;
}
if (!includeAir && state.isAir()) {
continue;
}
object.setUnsigned(position.x(), position.y(), position.z(), ModdedBlockState.of(state, null));
blockCount++;
if (blockEntityTag != null && captureTile(sourceKey, position, state, blockEntityTag, object, losses)) {
tiles++;
}
}
return new CaptureCounts(blockCount, tiles, jigsaws, dataMarkers);
}
private static BlockPosition readPosition(CompoundTag blockTag) {
ListTag position = blockTag.getListOrEmpty(StructureTemplate.BLOCK_TAG_POS);
return new BlockPosition(
position.getIntOr(0, Integer.MIN_VALUE),
position.getIntOr(1, Integer.MIN_VALUE),
position.getIntOr(2, Integer.MIN_VALUE)
);
}
private static boolean withinObject(BlockPosition position, IrisObject object) {
return position.x() >= 0 && position.x() < object.getW()
&& position.y() >= 0 && position.y() < object.getH()
&& position.z() >= 0 && position.z() < object.getD();
}
private static BlockState resolveJigsawFinalState(
StructureKey sourceKey,
BlockPosition position,
CompoundTag blockEntityTag,
List<StructureLoss> losses
) {
String finalState = blockEntityTag == null
? "minecraft:air"
: blockEntityTag.getStringOr("final_state", "minecraft:air");
try {
return ModdedBlockResolution.strictParse(finalState).handle();
} catch (IllegalArgumentException failure) {
LOGGER.error("Failed to parse jigsaw final state '{}' in {} at {},{},{}",
finalState, sourceKey, position.x(), position.y(), position.z(), failure);
losses.add(StructureLoss.warning(
StructureCapability.BLOCKS,
"jigsaw_final_state_not_imported",
"Jigsaw final state '" + finalState + "' at " + position.x() + "," + position.y() + ","
+ position.z() + " could not be parsed."
));
return null;
}
}
private static boolean captureTile(
StructureKey sourceKey,
BlockPosition position,
BlockState state,
CompoundTag blockEntityTag,
IrisObject object,
List<StructureLoss> losses
) {
try {
String blockKey = ModdedBlockState.serialize(state);
ModdedTileData tile = ModdedTileData.capture(blockKey, NbtUtils.structureToSnbt(blockEntityTag));
object.setUnsignedTile(position.x(), position.y(), position.z(), tile);
return true;
} catch (IOException | RuntimeException failure) {
LOGGER.error("Failed to capture block entity in {} at {},{},{}",
sourceKey, position.x(), position.y(), position.z(), failure);
losses.add(StructureLoss.warning(
StructureCapability.BLOCK_ENTITIES,
"block_entity_not_imported",
"Block entity data at " + position.x() + "," + position.y() + "," + position.z()
+ " could not be encoded."
));
return false;
}
}
private static void recordEntityLoss(CompoundTag sourceTag, List<StructureLoss> losses) {
int entityCount = sourceTag.getListOrEmpty(StructureTemplate.ENTITIES_TAG).size();
if (entityCount > 0) {
losses.add(StructureLoss.warning(
StructureCapability.ENTITIES,
"entities_not_imported",
entityCount + " structure entit" + (entityCount == 1 ? "y was" : "ies were")
+ " not converted into the Iris snapshot."
));
}
}
private static void recordMarkerLosses(
CaptureCounts counts,
boolean connectorsPreserved,
List<StructureLoss> losses
) {
if (counts.dataMarkers() > 0) {
losses.add(StructureLoss.warning(
StructureCapability.PROCESSORS,
"data_markers_not_imported",
counts.dataMarkers() + " structure data marker(s) require native pool-element handlers and were omitted."
));
}
if (!connectorsPreserved && counts.jigsaws() > 0) {
losses.add(StructureLoss.warning(
StructureCapability.CONNECTORS,
"connectors_not_imported",
counts.jigsaws() + " jigsaw connector(s) were resolved to final blocks without importing their pool graph."
));
}
}
record Capture(
IrisObject object,
int blocks,
int tiles,
int jigsaws,
int dataMarkers,
int width,
int height,
int depth,
Set<StructureCapability> capabilities,
List<StructureLoss> losses,
CompoundTag sourceTag
) {
Capture {
Objects.requireNonNull(object);
capabilities = Set.copyOf(capabilities);
losses = List.copyOf(losses);
sourceTag = sourceTag.copy();
}
}
private record CaptureCounts(int blocks, int tiles, int jigsaws, int dataMarkers) {
}
private record BlockPosition(int x, int y, int z) {
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.modded;
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 static org.junit.Assert.assertSame;
public class IrisModdedLocateRoutingTest {
@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));
}
}
@@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class IrisModdedStructureParityTest {
@@ -120,14 +121,18 @@ public class IrisModdedStructureParityTest {
}
@Test
public void possibleBiomeFallbackIsOnlyRequiredForMissingOrEmptyConfigurations() {
Set<String> registered = Set.of("minecraft:deep_ocean", "minecraft:dark_forest", "minecraft:plains");
public void structureBiomeContractRejectsAnEmptyConfiguredSet() {
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(Set.of()));
assertFalse(IrisModdedBiomeSource.requiresPossibleBiomeFallback(
Set.of("minecraft:deep_ocean", "minecraft:dark_forest"), registered));
assertTrue(IrisModdedBiomeSource.requiresPossibleBiomeFallback(
Set.of("minecraft:deep_ocean", "overworld:missing"), registered));
assertTrue(IrisModdedBiomeSource.requiresPossibleBiomeFallback(Set.of(), registered));
assertEquals("Iris has no configured structure biomes", error.getMessage());
}
@Test
public void structureBiomeContractPreservesConfiguredKeysDuringBootstrap() {
Set<String> configured = Set.of("minecraft:deep_ocean", "minecraft:dark_forest");
assertSame(configured, IrisModdedBiomeSource.requireConfiguredStructureBiomeKeys(configured));
}
@Test
@@ -185,6 +190,27 @@ public class IrisModdedStructureParityTest {
throw new AssertionError("Expected failed engine binding to propagate");
}
@Test
public void structureBiomeBootstrapAllowsOnlyPendingBindingsToUseMetadata() {
IrisModdedChunkGenerator.EngineBinding<String> binding =
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
binding.throwIfFailed("overworld:overworld");
}
@Test
public void structureBiomeBootstrapPropagatesBindingFailure() {
IrisModdedChunkGenerator.EngineBinding<String> binding =
new IrisModdedChunkGenerator.EngineBinding<>(1L, TimeUnit.SECONDS);
IllegalArgumentException failure = new IllegalArgumentException("broken pack");
binding.fail(failure);
IllegalStateException error = assertThrows(IllegalStateException.class,
() -> binding.throwIfFailed("overworld:overworld"));
assertSame(failure, error.getCause());
}
@Test
public void initialEntitySpawnsUseThePaperCompletionMarker() {
assertSame(MantleFlag.INITIAL_SPAWNED_MARKER, ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG);
@@ -35,6 +35,7 @@ import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedForcedDatapackTest {
@@ -75,6 +76,71 @@ public class ModdedForcedDatapackTest {
}
}
@Test
public void publishesCompleteStagingDirectoryOverExistingPack() throws IOException {
Path root = Files.createTempDirectory("iris-forced-pack-publish");
try {
Path published = Files.createDirectory(root.resolve("iris"));
Files.writeString(published.resolve("old.txt"), "old", StandardCharsets.UTF_8);
Path staging = Files.createDirectory(root.resolve("staging"));
Files.writeString(staging.resolve("new.txt"), "new", StandardCharsets.UTF_8);
ModdedForcedDatapack.publishDirectory(staging, published);
assertFalse(Files.exists(staging));
assertFalse(Files.exists(published.resolve("old.txt")));
assertEquals("new", Files.readString(published.resolve("new.txt"), StandardCharsets.UTF_8));
assertEquals(List.of("iris"), directoryEntries(root));
} finally {
deleteTree(root);
}
}
@Test
public void publishesStagingDirectoryWhenNoPriorPackExists() throws IOException {
Path root = Files.createTempDirectory("iris-forced-pack-first-publish");
try {
Path published = root.resolve("iris");
Path staging = Files.createDirectory(root.resolve("staging"));
Files.writeString(staging.resolve("pack.mcmeta"), "first", StandardCharsets.UTF_8);
ModdedForcedDatapack.publishDirectory(staging, published);
assertEquals("first",
Files.readString(published.resolve("pack.mcmeta"), StandardCharsets.UTF_8));
assertEquals(List.of("iris"), directoryEntries(root));
} finally {
deleteTree(root);
}
}
@Test
public void restoresPublishedPackWhenAtomicStagingMoveFails() throws IOException {
Path root = Files.createTempDirectory("iris-forced-pack-rollback");
try {
Path published = Files.createDirectory(root.resolve("iris"));
Files.writeString(published.resolve("pack.mcmeta"), "known-good", StandardCharsets.UTF_8);
Path missingStaging = root.resolve("missing-staging");
assertThrows(IOException.class,
() -> ModdedForcedDatapack.publishDirectory(missingStaging, published));
assertEquals("known-good",
Files.readString(published.resolve("pack.mcmeta"), StandardCharsets.UTF_8));
assertEquals(List.of("iris"), directoryEntries(root));
} finally {
deleteTree(root);
}
}
private List<String> directoryEntries(Path root) throws IOException {
try (Stream<Path> entries = Files.list(root)) {
return entries.map((Path path) -> path.getFileName().toString())
.sorted()
.toList();
}
}
private void deleteTree(Path root) throws IOException {
List<Path> paths = new ArrayList<>();
try (Stream<Path> walk = Files.walk(root)) {
@@ -0,0 +1,249 @@
package art.arcane.iris.modded;
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;
public class ModdedLifecycleFailureContractTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test
public void existingDimensionRepointBindsBeforeHandlePublication() throws IOException {
String managerSource = source("ModdedDimensionManager.java");
String create = method(managerSource, "public static Handle create(");
int existingStart = requiredIndex(create, "if (existing != null");
int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1);
int injectionStart = requiredIndex(create, "try {", presentStart);
assertRepointBeforePublication(create.substring(existingStart, presentStart));
assertRepointBeforePublication(create.substring(presentStart, injectionStart));
String generatorSource = source("IrisModdedChunkGenerator.java");
String repointAndBind = method(generatorSource, "void repointAndBind(");
assertBefore(repointAndBind, "ModdedWorldEngines.prepareReplacement(",
"ModdedWorldEngines.installReplacement(");
assertBefore(repointAndBind, "ModdedWorldEngines.installReplacement(", "this.activePack = pack;");
String failure = catchBlock(repointAndBind);
assertTrue(failure.contains("ModdedWorldEngines.closeUnregistered(replacement);"));
assertTrue(failure.contains("addSuppressed("));
String worldEnginesSource = source("ModdedWorldEngines.java");
String installReplacement = method(worldEnginesSource, "static void installReplacement(");
assertTrue(installReplacement.contains("ENGINES.compute("));
assertBefore(installReplacement, "close(current);", "return activeReplacement;");
assertFalse(installReplacement.contains("catch ("));
}
@Test
public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException {
String source = source("ModdedDimensionManager.java");
String injection = method(source, "private static Handle inject(");
assertBefore(injection, "generator.bindLevel(level);", "serverAccess.putLevelIfAbsent(server, key, level);");
assertBefore(injection, "serverAccess.putLevelIfAbsent(server, key, level);",
"server.getPlayerList().addWorldborderListener(level);");
assertFalse(injection.contains("serverAccess.putLevel(server, key, previous);"));
String failure = catchBlock(injection);
assertBefore(failure, "rollbackInjection(", "throw ");
String rollback = method(source, "void rollbackInjection(");
assertBefore(rollback, "serverAccess.hasLevel(server, key)", "serverAccess.removeLevel(server, key);");
assertTrue(rollback.contains("generator.unbindEngine("));
assertTrue(rollback.contains("level.close();"));
assertTrue(rollback.contains("addSuppressed("));
}
@Test
public void persistentReinjectionRethrowsTheOriginalCause() throws IOException {
String source = source("ModdedStartup.java");
String reinjection = method(source, "private static void reinjectPersistentDimensions(");
String failure = catchBlock(reinjection);
assertTrue(failure.contains("LOGGER.error("));
assertFalse(failure.contains("e.toString()"));
assertFalse(failure.contains("continue;"));
assertTrue(failure.contains("throw new IllegalStateException("));
assertTrue(failure.contains(", e);"));
}
@Test
public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException {
String source = source("ModdedDimensionManager.java");
String loading = method(source, "private static IrisDimension loadPackDimension(");
assertFalse(loading.contains("return null;"));
assertFalse(loading.contains("e.toString()"));
int catchIndex = loading.indexOf("catch (");
if (catchIndex >= 0) {
String failure = blockAt(loading, requiredIndex(loading, "{", catchIndex));
assertTrue(failure.contains("throw "));
assertTrue(failure.contains(", e);") || failure.contains("throw e;"));
}
}
@Test
public void productionPackResolutionNeverUsesTheParityProbeAsData() throws IOException {
String source = source("ModdedWorldEngines.java");
String resolvePack = method(source, "static File resolvePack(");
assertFalse(source.contains("System.getProperty(\"iris.parity\")"));
assertFalse(source.contains("parityPack"));
assertFalse(source.contains("falling back to parity pack"));
assertTrue(resolvePack.contains("throw new IllegalStateException("));
}
@Test
public void engineBootstrapResetsStartupExactlyOnceDuringStop() throws IOException {
String source = source("ModdedEngineBootstrap.java");
String stop = method(source, "public static void stop(");
assertEquals(1, occurrences(stop, "ModdedStartup.reset();"));
}
@Test
public void engineEvictionRetainsTheMappingUntilCloseSucceeds() throws IOException {
String source = source("ModdedWorldEngines.java");
String eviction = method(source, "static void evictOrThrow(");
assertTrue(eviction.contains("ENGINES.computeIfPresent("));
assertFalse(eviction.contains("ENGINES.remove("));
assertBefore(eviction, "close(current);", "return null;");
}
@Test
public void generatorUnbindRetainsOwnershipWhenEvictionFails() throws IOException {
String source = source("IrisModdedChunkGenerator.java");
String unbind = method(source, "synchronized void unbindEngine(ServerLevel level)");
assertFalse(unbind.contains("finally"));
assertBefore(unbind, "ModdedWorldEngines.evictOrThrow(level);", "clearEngineBinding();");
}
@Test
public void engineBootstrapRollsBackEveryPublishedBindingBeforeRethrowing() throws IOException {
String source = source("ModdedEngineBootstrap.java");
String bind = method(source, "public static ModdedPlatform bind(");
String failure = catchBlock(bind);
assertBefore(bind, "createdServices.enableAll();", "runtime = new BoundRuntime(");
assertTrue(bind.contains("rollback.add(() -> GuiHost.suppressDesktop("));
assertTrue(bind.contains("rollback.add(() -> restorePlatform("));
assertTrue(bind.contains("rollback.add(() -> ModdedDimensionManager.restoreAccess("));
assertTrue(bind.contains("rollback.add(() -> IrisObjectRotation.restorePlatformRotator("));
assertTrue(bind.contains("rollback.add(() -> BlockDataMergeSupport.restorePlatformMerger("));
assertTrue(bind.contains("rollback.add(() -> TileData.restorePlatformReader("));
assertTrue(bind.contains("rollback.add(() -> TileData.restorePlatformFactory("));
assertTrue(bind.contains("rollback.add(() -> GuiHost.set("));
assertTrue(bind.contains("rollback.add(() -> DecoratorPlatformHooks.restore("));
assertTrue(bind.contains("bindService(PreservationRegistry.class"));
assertTrue(bind.contains("bindService(EngineEffectsProvider.class"));
assertTrue(bind.contains("bindService(EnginePlatformHooks.class"));
assertTrue(bind.contains("bindService(EngineWorldManagerProvider.class"));
assertTrue(bind.contains("rollback.add(customContentDiscovery::rollback);"));
assertBefore(failure, "createdServices.rollback(failure);", "rollback.restore(failure);");
assertBefore(failure, "rollback.restore(failure);", "throw ");
}
private static void assertRepointBeforePublication(String branch) {
assertBefore(branch, "repointAndBind(", "new Handle(");
assertBefore(branch, "repointAndBind(", "HANDLES.put(");
assertBefore(branch, "repointAndBind(", "return ");
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = requiredIndex(source, first);
int secondIndex = requiredIndex(source, second);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static int requiredIndex(String source, String needle) {
return requiredIndex(source, needle, 0);
}
private static int requiredIndex(String source, String needle, int start) {
int index = source.indexOf(needle, start);
assertTrue("Missing source contract token: " + needle, index >= 0);
return index;
}
private static String source(String fileName) throws IOException {
String sourceRoot = System.getProperty(SOURCE_ROOT_PROPERTY);
assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY,
sourceRoot != null && !sourceRoot.isBlank());
Path sourcePath = Path.of(sourceRoot, "art", "arcane", "iris", "modded", fileName);
return Files.readString(sourcePath);
}
private static String method(String source, String signature) {
int start = requiredIndex(source, signature);
int openBrace = requiredIndex(source, "{", start);
return source.substring(start, matchingBrace(source, openBrace) + 1);
}
private static String catchBlock(String source) {
int catchIndex = requiredIndex(source, "catch (");
int openBrace = requiredIndex(source, "{", catchIndex);
return blockAt(source, openBrace);
}
private static String blockAt(String source, int openBrace) {
return source.substring(openBrace, matchingBrace(source, openBrace) + 1);
}
private static int matchingBrace(String source, int openBrace) {
int depth = 0;
boolean quoted = false;
boolean character = false;
boolean escaped = false;
for (int i = openBrace; i < source.length(); i++) {
char current = source.charAt(i);
if (escaped) {
escaped = false;
continue;
}
if ((quoted || character) && current == '\\') {
escaped = true;
continue;
}
if (!character && current == '"') {
quoted = !quoted;
continue;
}
if (!quoted && current == '\'') {
character = !character;
continue;
}
if (quoted || character) {
continue;
}
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return i;
}
}
}
throw new IllegalArgumentException("Unclosed source block");
}
private static int occurrences(String source, String needle) {
int count = 0;
int offset = 0;
while (true) {
int index = source.indexOf(needle, offset);
if (index < 0) {
return count;
}
count++;
offset = index + needle.length();
}
}
}
@@ -1,9 +1,13 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.IrisLootMode;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.RandomizableContainer;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.storage.loot.LootTable;
@@ -12,6 +16,7 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
@@ -25,7 +30,7 @@ public class ModdedLootApplierTest {
public void addAppendsSourcesInOrder() {
List<String> sources = new ArrayList<>(List.of("placement-native"));
ModdedLootApplier.injectSources(sources, List.of("dimension-iris", "region-iris"), IrisLootMode.ADD, false);
LootResolver.injectSources(sources, List.of("dimension-iris", "region-iris"), IrisLootMode.ADD, false);
assertEquals(List.of("placement-native", "dimension-iris", "region-iris"), sources);
}
@@ -34,7 +39,7 @@ public class ModdedLootApplierTest {
public void clearRemovesNativeAndIrisSourcesBeforeAdding() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.CLEAR, false);
assertEquals(List.of("biome-iris"), sources);
}
@@ -43,7 +48,7 @@ public class ModdedLootApplierTest {
public void replaceRemovesNativeAndIrisSourcesBeforeAdding() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.injectSources(sources, List.of("biome-iris"), IrisLootMode.REPLACE, false);
LootResolver.injectSources(sources, List.of("biome-iris"), IrisLootMode.REPLACE, false);
assertEquals(List.of("biome-iris"), sources);
}
@@ -58,7 +63,7 @@ public class ModdedLootApplierTest {
public void fallbackDoesNotOverrideExistingNativeOrIrisSources() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, false);
LootResolver.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, false);
assertEquals(List.of("placement-native", "dimension-iris"), sources);
}
@@ -67,7 +72,7 @@ public class ModdedLootApplierTest {
public void fallbackAddsSourcesWhenPlacementIsEmpty() {
List<String> sources = new ArrayList<>();
ModdedLootApplier.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, true);
LootResolver.injectSources(sources, List.of("fallback-iris"), IrisLootMode.FALLBACK, true);
assertEquals(List.of("fallback-iris"), sources);
}
@@ -76,7 +81,7 @@ public class ModdedLootApplierTest {
public void zeroMultiplierRemovesNativeAndIrisSources() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.scaleSources(sources, 0D, new RNG(17L));
LootResolver.scaleSources(sources, 0D, new RNG(17L));
assertTrue(sources.isEmpty());
}
@@ -85,7 +90,7 @@ public class ModdedLootApplierTest {
public void unitMultiplierPreservesNativeAndIrisSources() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.scaleSources(sources, 1D, new RNG(17L));
LootResolver.scaleSources(sources, 1D, new RNG(17L));
assertEquals(List.of("placement-native", "dimension-iris"), sources);
}
@@ -94,7 +99,7 @@ public class ModdedLootApplierTest {
public void doubleMultiplierScalesTheUnifiedSourceList() {
List<String> sources = new ArrayList<>(List.of("placement-native", "dimension-iris"));
ModdedLootApplier.scaleSources(sources, 2D, new RNG(17L));
LootResolver.scaleSources(sources, 2D, new RNG(17L));
assertEquals(4, sources.size());
assertEquals("placement-native", sources.get(0));
@@ -142,6 +147,51 @@ public class ModdedLootApplierTest {
assertTrue(container.changed);
}
@Test
public void doubleChestHasExactlyOneCanonicalHalf() {
BlockPos leftPos = new BlockPos(0, 64, 0);
BlockPos rightPos = new BlockPos(1, 64, 0);
assertTrue(ModdedLootApplier.isCanonicalPair(leftPos, rightPos));
assertFalse(ModdedLootApplier.isCanonicalPair(rightPos, leftPos));
}
@Test
public void nativeLootTableIsDetectedBeforeContainerAccess() {
ResourceKey<LootTable> key = ResourceKey.create(
Registries.LOOT_TABLE,
Identifier.parse("minecraft:chests/simple_dungeon")
);
RandomizableContainer empty = containerWithLootTable(null);
RandomizableContainer nativeLoot = containerWithLootTable(key);
assertFalse(ModdedLootApplier.hasNativeLootTable(empty));
assertTrue(ModdedLootApplier.hasNativeLootTable(nativeLoot));
}
private RandomizableContainer containerWithLootTable(ResourceKey<LootTable> key) {
return (RandomizableContainer) Proxy.newProxyInstance(
RandomizableContainer.class.getClassLoader(),
new Class<?>[]{RandomizableContainer.class},
(proxy, method, args) -> {
if (method.getName().equals("getLootTable")) {
return key;
}
Class<?> returnType = method.getReturnType();
if (returnType == boolean.class) {
return false;
}
if (returnType == int.class) {
return 0;
}
if (returnType == long.class) {
return 0L;
}
return null;
}
);
}
private static final class TrackingContainer extends SimpleContainer {
private boolean changed;
@@ -0,0 +1,93 @@
package art.arcane.iris.modded;
import art.arcane.iris.modded.service.ModdedService;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
public class ModdedServiceManagerTest {
@Test
public void enablesAndDisablesEveryRegisteredService() {
ModdedServiceManager manager = new ModdedServiceManager();
FirstService first = manager.register(FirstService.class, new FirstService());
SecondService second = manager.register(SecondService.class, new SecondService(null, null));
manager.enableAll();
manager.disableAll();
assertEquals(1, first.enableCount);
assertEquals(1, first.disableCount);
assertEquals(1, second.enableCount);
assertEquals(1, second.disableCount);
}
@Test
public void rollsBackEnabledServicesAndPreservesCleanupFailures() {
ModdedServiceManager manager = new ModdedServiceManager();
FirstService first = manager.register(FirstService.class, new FirstService());
RuntimeException original = new RuntimeException("enable failed");
RuntimeException cleanup = new RuntimeException("cleanup failed");
SecondService second = manager.register(
SecondService.class, new SecondService(original, cleanup));
RuntimeException thrown = assertThrows(RuntimeException.class, manager::enableAll);
assertSame(original, thrown);
assertEquals(1, first.enableCount);
assertEquals(1, first.disableCount);
assertEquals(1, second.enableCount);
assertEquals(1, second.disableCount);
assertEquals(1, thrown.getSuppressed().length);
assertSame(cleanup, thrown.getSuppressed()[0]);
manager.rollback(thrown);
assertNull(manager.service(FirstService.class));
assertNull(manager.service(SecondService.class));
}
private static final class FirstService implements ModdedService {
private int enableCount;
private int disableCount;
@Override
public void onEnable() {
enableCount++;
}
@Override
public void onDisable() {
disableCount++;
}
}
private static final class SecondService implements ModdedService {
private final RuntimeException enableFailure;
private final RuntimeException disableFailure;
private int enableCount;
private int disableCount;
private SecondService(RuntimeException enableFailure, RuntimeException disableFailure) {
this.enableFailure = enableFailure;
this.disableFailure = disableFailure;
}
@Override
public void onEnable() {
enableCount++;
if (enableFailure != null) {
throw enableFailure;
}
}
@Override
public void onDisable() {
disableCount++;
if (disableFailure != null) {
throw disableFailure;
}
}
}
}
@@ -11,6 +11,7 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedStructureHooksTest {
@@ -51,4 +52,47 @@ public class ModdedStructureHooksTest {
assertEquals(320, ModdedPlatformWorld.exclusiveMaxHeight(-64, 384));
assertEquals(384, ModdedPlatformWorld.exclusiveMaxHeight(0, 384));
}
@Test
public void structureRegistryHooksRejectUnavailableServer() {
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> null);
IllegalStateException structureKeys = assertThrows(
IllegalStateException.class, hooks::structureKeys);
IllegalStateException structureSetKeys = assertThrows(
IllegalStateException.class, hooks::structureSetKeys);
IllegalStateException structureBiomeKeys = assertThrows(
IllegalStateException.class, () -> hooks.structureBiomeKeys("minecraft:village"));
assertTrue(structureKeys.getMessage().contains("before the Minecraft server is available"));
assertTrue(structureSetKeys.getMessage().contains("before the Minecraft server is available"));
assertTrue(structureBiomeKeys.getMessage().contains("before the Minecraft server is available"));
}
@Test
public void structureReachabilityHooksRejectUnavailableLevel() {
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> null);
IllegalStateException reachable = assertThrows(
IllegalStateException.class, () -> hooks.reachableStructureKeys(null));
IllegalStateException possibleBiomes = assertThrows(
IllegalStateException.class, () -> hooks.possibleBiomeKeys(null));
assertTrue(reachable.getMessage().contains("without a bound modded ServerLevel"));
assertTrue(possibleBiomes.getMessage().contains("without a bound modded ServerLevel"));
}
@Test
public void structureRegistryHooksPreserveServerSupplierFailure() {
IllegalStateException cause = new IllegalStateException("server supplier failed");
ModdedStructureHooks hooks = new ModdedStructureHooks(() -> {
throw cause;
});
IllegalStateException failure = assertThrows(
IllegalStateException.class, hooks::structureKeys);
assertTrue(failure.getMessage().contains("access the Minecraft server"));
assertEquals(cause, failure.getCause());
}
}
@@ -0,0 +1,30 @@
package art.arcane.iris.modded;
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 ModdedStructurePlacementFailureContractTest {
@Test
public void structurePlacementPropagatesRuntimeFailuresWithContext() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/ModdedStructureHooks.java");
String source = Files.readString(sourcePath);
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 error)");
assertTrue(catchStart >= 0);
String failurePath = method.substring(catchStart);
assertTrue(failurePath.contains("throw NativeStructureGenerationException.failure("));
assertTrue(failurePath.contains("\"capture placement\", structureKey, chunkX, chunkZ, error"));
assertFalse(failurePath.contains("return null;"));
assertFalse(failurePath.contains("catch (Throwable"));
}
}
@@ -39,8 +39,8 @@ public class ModdedTileParityTest {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
registries = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
TileData.bindFallbackReader(new ModdedTileReader(() -> null));
TileData.bindFallbackFactory(ModdedTileData::fromProperties);
TileData.bindPlatformReader(new ModdedTileReader(() -> null));
TileData.bindPlatformFactory(ModdedTileData::fromProperties);
}
@Test
@@ -5,6 +5,9 @@ import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -23,6 +26,29 @@ public class ModdedWorldCheckTest {
assertFalse(thread.isDaemon());
}
@Test
public void poiAuditRunsInASecondServerTaskAfterVillageGeneration() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/ModdedWorldCheck.java");
String source = Files.readString(sourcePath);
int preparationSubmit = source.indexOf(
"WorldCheckPreparation preparation = serverRef.submit(() -> run(serverRef)).join();");
int completionSubmit = source.indexOf(
"exitCode = serverRef.submit(() -> runAndRequestStop(", preparationSubmit);
int completionMethod = source.indexOf("private static boolean completeWorldCheck");
int deferredAudit = source.indexOf("PoiAudit poi = auditStructurePois", completionMethod);
int structureMethod = source.indexOf("private static StructureCheckResult checkNativeStructure");
int structureMethodEnd = source.indexOf("private static StructureStart resolveStructureStart",
structureMethod);
String structureSource = source.substring(structureMethod, structureMethodEnd);
assertTrue(preparationSubmit >= 0);
assertTrue(completionSubmit > preparationSubmit);
assertTrue(deferredAudit > completionMethod);
assertFalse(structureSource.contains("auditStructurePois"));
assertFalse(source.contains("prepareDeferredAudits"));
}
@Test
public void validStructureStartIsGenerationEvidence() {
assertTrue(ModdedWorldCheck.hasNativeStructureEvidence(true, 0));
@@ -115,15 +141,6 @@ public class ModdedWorldCheckTest {
assertFalse(ModdedWorldCheck.villageFoundationPass(1));
}
@Test
public void villageFoundationAuditRejectsMissingBaseAndInvalidSupport() {
assertTrue(ModdedWorldCheck.villageFoundationSupported(true, false, true, false));
assertTrue(ModdedWorldCheck.villageFoundationSupported(true, true, false, false));
assertFalse(ModdedWorldCheck.villageFoundationSupported(false, false, true, false));
assertFalse(ModdedWorldCheck.villageFoundationSupported(true, false, false, false));
assertFalse(ModdedWorldCheck.villageFoundationSupported(true, false, true, true));
}
@Test
public void villagePoiGateRequiresInBoundsPoiWithoutOutOfBoundsRecords() {
assertTrue(ModdedWorldCheck.villagePoiPass(1, 0));
@@ -0,0 +1,89 @@
package art.arcane.iris.modded;
import art.arcane.iris.nativegen.NativeStructureGenerationException;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
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.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NativeStructureFailureContractTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void structureLocateDoesNotCatchAndFallThroughToAnotherImplementation() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedChunkGenerator.java");
String source = Files.readString(sourcePath);
int locateStart = source.indexOf("public Pair<BlockPos, Holder<Structure>> findNearestMapStructure");
int locateEnd = source.indexOf("public boolean isNativeStructureReachable", locateStart);
String locate = source.substring(locateStart, locateEnd);
int filterStart = source.indexOf("private HolderSet<Structure> filterReachableNativeStructures");
int filterEnd = source.indexOf("private ServerLevel boundLevel", filterStart);
String filter = source.substring(filterStart, filterEnd);
assertTrue(locate.contains("Engine current = engine();"));
assertFalse(locate.contains("catch (Throwable"));
assertFalse(locate.contains("return null;\n } catch"));
assertTrue(filter.contains("unregistered structure holder"));
assertFalse(filter.contains("catch (Throwable"));
assertFalse(filter.contains("failed closed"));
}
@Test
public void globalStructureDisableRefusesGeneratorBinding() {
IrisModdedChunkGenerator.requireGlobalStructureGeneration(true, "overworld:overworld");
IllegalStateException error = assertThrows(
IllegalStateException.class,
() -> IrisModdedChunkGenerator.requireGlobalStructureGeneration(
false, "overworld:overworld"));
assertTrue(error.getMessage().contains("overworld:overworld"));
assertTrue(error.getMessage().contains("generate-structures=false"));
assertTrue(error.getMessage().contains("importedStructures.disabled"));
}
@Test
public void structureTerrainPreparationPrecedesVegetationAndPlacement() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"),
"art/arcane/iris/modded/IrisModdedChunkGenerator.java");
String source = Files.readString(sourcePath);
int placementStart = source.indexOf("private void placeVanillaStructures");
int placementEnd = source.indexOf("private static String nativeStructureBatchContext", placementStart);
String placement = source.substring(placementStart, placementEnd);
assertTrue(placement.contains("\"terrain integration\""));
assertTrue(placement.contains("prepareSurfaceStructures"));
assertTrue(placement.contains("clearIntersectingVegetation"));
assertTrue(placement.indexOf("prepareSurfaceStructures")
< placement.indexOf("clearIntersectingVegetation"));
assertTrue(placement.indexOf("clearIntersectingVegetation")
< placement.indexOf("for (NativePlacementGroup group"));
}
@Test
public void structureFailurePreservesPhaseIdentityChunkAndCause() {
IllegalArgumentException cause = new IllegalArgumentException("broken placement");
NativeStructureGenerationException error = NativeStructureGenerationException.failure(
"placement", "minecraft:monument", 12, -8, cause);
assertSame(cause, error.getCause());
assertTrue(error.getMessage().contains("placement"));
assertTrue(error.getMessage().contains("minecraft:monument"));
assertTrue(error.getMessage().contains("12,-8"));
assertTrue(error.getMessage().contains("aborted"));
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.modded.api;
import net.minecraft.resources.Identifier;
import org.junit.Test;
import java.util.Collection;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedCustomContentRegistryTest {
@Test
public void publishesProvidersOnlyAfterCompleteDiscoveryAndCanRollBack() {
String modId = "iris_discovery_success";
TestProvider provider = new TestProvider(modId, null);
boolean previousDiscoveryComplete = ModdedCustomContentRegistry.discoveryComplete();
ModdedCustomContentRegistry.Discovery discovery =
ModdedCustomContentRegistry.discover(List.of(provider));
try {
assertTrue(ModdedCustomContentRegistry.discoveryComplete());
assertTrue(ModdedCustomContentRegistry.hasProvider(modId));
} finally {
discovery.rollback();
}
assertEquals(previousDiscoveryComplete, ModdedCustomContentRegistry.discoveryComplete());
assertFalse(ModdedCustomContentRegistry.hasProvider(modId));
}
@Test
public void failedDiscoveryPublishesNothingAndPreservesTheCause() {
String firstModId = "iris_discovery_staged";
String failingModId = "iris_discovery_failure";
RuntimeException original = new RuntimeException("provider init failed");
boolean previousDiscoveryComplete = ModdedCustomContentRegistry.discoveryComplete();
RuntimeException thrown = assertThrows(RuntimeException.class,
() -> ModdedCustomContentRegistry.discover(List.of(
new TestProvider(firstModId, null),
new TestProvider(failingModId, original))));
assertSame(original, thrown);
assertEquals(previousDiscoveryComplete, ModdedCustomContentRegistry.discoveryComplete());
assertFalse(ModdedCustomContentRegistry.hasProvider(firstModId));
assertFalse(ModdedCustomContentRegistry.hasProvider(failingModId));
}
private static final class TestProvider implements ModdedDataProvider {
private final String modId;
private final RuntimeException failure;
private TestProvider(String modId, RuntimeException failure) {
this.modId = modId;
this.failure = failure;
}
@Override
public String modId() {
return modId;
}
@Override
public Collection<Identifier> getTypes(ModdedDataType type) {
return List.of();
}
@Override
public boolean isValidProvider(Identifier id, ModdedDataType type) {
return false;
}
@Override
public void init() {
if (failure != null) {
throw failure;
}
}
}
}
@@ -20,6 +20,7 @@ public class IrisModdedStructureCommandTest {
assertTrue(source.contains("generator.findNearestMapStructure("));
assertTrue(source.contains("NATIVE_STRUCTURE_LOCATE_RADIUS = 100"));
assertTrue(source.contains("HolderSet.direct(target.holder())"));
assertFalse(source.contains("NativeStructureLocateCapability"));
assertTrue(source.contains("boolean teleported = player.teleportTo("));
assertTrue(source.contains("combineStructureKeys(irisKeys, nativeKeys)"));
assertTrue(source.contains("irisGenerator.isNativeStructureReachable(holder)"));
@@ -33,16 +34,64 @@ public class IrisModdedStructureCommandTest {
}
@Test
public void generatorLocatePrefersIrisPlacementsAndRejectsDormantNativeStarts() throws IOException {
public void generatorLocateUsesIrisOnlyForExplicitReplacement() throws IOException {
String source = moddedSource("IrisModdedChunkGenerator.java");
int methodStart = source.indexOf("private Pair<BlockPos, Holder<Structure>> findNearestIrisStructure(");
int methodEnd = source.indexOf("private HolderSet<Structure> filterReachableNativeStructures(", methodStart);
String method = source.substring(methodStart, methodEnd);
int unexploredGuard = method.indexOf("if (findUnexplored)");
int registryLookup = method.indexOf("level.registryAccess().lookupOrThrow(Registries.STRUCTURE)");
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(current,");
int replacementCheck = method.indexOf(
"decision.status() != NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
int irisLocate = method.indexOf("IrisStructureLocator.locate(", replacementCheck);
assertTrue(source.contains("public Pair<BlockPos, Holder<Structure>> findNearestMapStructure("));
assertTrue(source.contains("findNearestIrisStructure("));
assertTrue(source.contains("filterReachableNativeStructures("));
assertTrue(source.contains("IrisStructureLocator.suppressesVanilla(current, key)"));
assertTrue(unexploredGuard >= 0);
assertTrue(registryLookup > unexploredGuard);
assertTrue(policyResolution > registryLookup);
assertTrue(replacementCheck > policyResolution);
assertTrue(irisLocate > replacementCheck);
assertTrue(method.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
assertTrue(method.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
assertFalse(method.contains("NativeStructureLocateCapability"));
assertTrue(source.contains("structureBiomeSource.isStructureReachable(holder)"));
assertTrue(source.contains("LocateStatus.SEARCH_LIMIT_REACHED"));
assertTrue(source.contains("new BlockPos(result.originX(), result.baseY(), result.originZ())"));
assertFalse(source.contains("isPaperUnavailable"));
}
@Test
public void commandResolvesNativePolicyBeforeAnyVanillaAliasLookup() throws IOException {
String source = source("IrisModdedCommands.java");
int methodStart = source.indexOf("private static int gotoStructure(");
int methodEnd = source.indexOf("private static void locateIrisStructure(", methodStart);
String method = source.substring(methodStart, methodEnd);
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
int genericIrisLookup = method.indexOf("IrisStructureLocator.isPlaced(engine, key)", nativeResolution);
int policyResolution = method.indexOf("NativeStructureGenerationPolicy.resolve(engine, target.key(), false)", genericIrisLookup);
int replacementCheck = method.indexOf(
"decision.status() == NativeStructureGenerationStatus.REPLACED_BY_IRIS", policyResolution);
int replacementLocate = method.indexOf("locateIrisStructure(source, level, engine, player, target.key())",
replacementCheck);
assertTrue(nativeResolution >= 0);
assertTrue(genericIrisLookup > nativeResolution);
assertTrue(policyResolution > genericIrisLookup);
assertTrue(replacementCheck > policyResolution);
assertTrue(replacementLocate > replacementCheck);
}
@Test
public void verifyResolvesRegisteredNativeBeforeGenericIrisAliases() throws IOException {
String source = source("IrisModdedCommands.java");
int methodStart = source.indexOf("private static int verifyStructure(");
int methodEnd = source.indexOf("private static Optional<NativeStructureTarget> resolveNativeStructure(",
methodStart);
String method = source.substring(methodStart, methodEnd);
int nativeResolution = method.indexOf("resolveNativeStructure(source, level, engine, key)");
int genericIrisLookup = method.indexOf("IrisStructureLocator.isPlaced(engine, key)", nativeResolution);
assertTrue(nativeResolution >= 0);
assertTrue(genericIrisLookup > nativeResolution);
assertTrue(method.contains("NativeStructureAvailability.IRIS_SUPPRESSED"));
}
@Test
@@ -0,0 +1,69 @@
/*
* 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.modded.structure;
import net.minecraft.core.Direction;
import org.junit.Test;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class ModdedJigsawStructureCaptureTest {
@Test
public void namespacedSourcePathsRemainDistinctAndPortable() {
String nested = ModdedJigsawStructureCapture.pieceName("village", "mod:a/b");
String underscored = ModdedJigsawStructureCapture.pieceName("village", "mod_a:b");
assertEquals("village/piece/mod/a/b", nested);
assertEquals("village/piece/mod_a/b", underscored);
assertNotEquals(nested, underscored);
assertEquals("village/pool/mod/a/b", ModdedJigsawStructureCapture.poolName("village", "mod:a/b"));
assertEquals(
"village/piece/generated/legacy/mod/a/b",
ModdedJigsawStructureCapture.legacyPieceName("village", "mod:a/b")
);
}
@Test
public void rootJsonRetainsGraphLimitsAndSourceIdentity() {
Map<String, Object> root = ModdedJigsawStructureCapture.structureJson(
"minecraft:village_plains",
"village/pool/minecraft/village/plains/town_centers",
6,
81
);
assertEquals("minecraft:village_plains", root.get("vanillaSource"));
assertEquals(6, root.get("maxDepth"));
assertEquals(6, root.get("maxSizeChunks"));
assertEquals("STRUCTURE_PIECE", root.get("placeMode"));
}
@Test
public void connectorDirectionsUseIrisAxisNames() {
assertEquals("UP_POSITIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.UP));
assertEquals("DOWN_NEGATIVE_Y", ModdedJigsawStructureCapture.directionName(Direction.DOWN));
assertEquals("NORTH_NEGATIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.NORTH));
assertEquals("SOUTH_POSITIVE_Z", ModdedJigsawStructureCapture.directionName(Direction.SOUTH));
assertEquals("EAST_POSITIVE_X", ModdedJigsawStructureCapture.directionName(Direction.EAST));
assertEquals("WEST_NEGATIVE_X", ModdedJigsawStructureCapture.directionName(Direction.WEST));
}
}
@@ -0,0 +1,86 @@
/*
* 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.modded.structure;
import art.arcane.iris.core.structure.authoring.StructureBackend;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import art.arcane.iris.core.structure.authoring.StructureResourceBundle;
import art.arcane.iris.core.structure.authoring.StructureSource;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStructureImportServiceTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void writesThroughOwnedAddOnlyAndOverwriteTransactions() throws Exception {
Path root = temporaryFolder.newFolder("modded-import").toPath();
ModdedStructureImportService service = new ModdedStructureImportService(() -> null);
ModdedStructureImportService.PreparedImport first = prepared(root, "one", StructureWriteMode.ADD_ONLY);
ModdedStructureImportService.ImportResult added = service.write(first);
ModdedStructureImportService.ImportResult conflict = service.write(first);
ModdedStructureImportService.ImportResult overwritten = service.write(
prepared(root, "two", StructureWriteMode.OVERWRITE)
);
assertTrue(added.success());
assertEquals(StructureWriteResult.Status.ADDED, added.writeResult().orElseThrow().status());
assertFalse(conflict.success());
assertEquals(StructureWriteResult.Status.ADD_ONLY_CONFLICT, conflict.writeResult().orElseThrow().status());
assertTrue(overwritten.success());
assertEquals(StructureWriteResult.Status.OVERWRITTEN, overwritten.writeResult().orElseThrow().status());
assertTrue(overwritten.capabilities().contains(StructureCapability.BLOCKS));
}
private static ModdedStructureImportService.PreparedImport prepared(
Path root,
String content,
StructureWriteMode mode
) {
StructureKey key = StructureKey.parse("iris:test_structure");
StructureResourceBundle bundle = StructureResourceBundle.builder(key)
.source(StructureSource.of(StructureSource.Kind.DATAPACK, StructureKey.parse("test:source")))
.backend(StructureBackend.SNAPSHOT)
.capability(StructureCapability.BLOCKS)
.textResource("structures/test_structure.json", content)
.build();
return new ModdedStructureImportService.PreparedImport(
root,
new StructureWriteOptions(mode, false),
ModdedStructureImportService.ImportKind.TEMPLATE,
bundle,
1,
1,
1
);
}
}
@@ -0,0 +1,184 @@
/*
* 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.modded.structure;
import art.arcane.iris.core.structure.authoring.StructureCapability;
import art.arcane.iris.core.structure.authoring.StructureKey;
import net.minecraft.SharedConstants;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.IntTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.server.Bootstrap;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedStructureTemplateCaptureTest {
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void capturesBlocksTilesAndExplicitStandaloneLosses() throws Exception {
CompoundTag template = templateTag();
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
template,
BuiltInRegistries.BLOCK,
false
);
assertEquals(4, capture.width());
assertEquals(1, capture.height());
assertEquals(1, capture.depth());
assertEquals(3, capture.blocks());
assertEquals(1, capture.tiles());
assertEquals(1, capture.jigsaws());
assertEquals(1, capture.dataMarkers());
assertEquals(3, capture.object().getBlocks().size());
assertEquals(1, capture.object().getStates().size());
assertTrue(capture.capabilities().contains(StructureCapability.BLOCKS));
assertTrue(capture.capabilities().contains(StructureCapability.BLOCK_ENTITIES));
assertFalse(capture.capabilities().contains(StructureCapability.CONNECTORS));
assertTrue(hasLoss(capture, "connectors_not_imported"));
assertTrue(hasLoss(capture, "data_markers_not_imported"));
assertTrue(hasLoss(capture, "entities_not_imported"));
ByteArrayOutputStream serialized = new ByteArrayOutputStream();
capture.object().write(serialized);
assertTrue(serialized.size() > 0);
}
@Test
public void graphCaptureReportsConnectorCapabilityWithoutStandaloneConnectorLoss() {
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
templateTag(),
BuiltInRegistries.BLOCK,
true
);
assertTrue(capture.capabilities().contains(StructureCapability.CONNECTORS));
assertFalse(hasLoss(capture, "connectors_not_imported"));
}
@Test
public void reportsAdditionalNativePalettes() {
CompoundTag template = templateTag();
ListTag palettes = new ListTag();
ListTag first = template.getListOrEmpty(StructureTemplate.PALETTE_TAG);
palettes.add(first.copy());
palettes.add(first.copy());
template.remove(StructureTemplate.PALETTE_TAG);
template.put(StructureTemplate.PALETTE_LIST_TAG, palettes);
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/template"),
template,
BuiltInRegistries.BLOCK,
true
);
assertTrue(hasLoss(capture, "palette_variants_not_imported"));
}
@Test
public void legacyCaptureOmitsAirBlocks() {
CompoundTag template = new CompoundTag();
template.put(StructureTemplate.SIZE_TAG, intList(1, 1, 1));
ListTag palette = new ListTag();
palette.add(NbtUtils.writeBlockState(Blocks.AIR.defaultBlockState()));
template.put(StructureTemplate.PALETTE_TAG, palette);
ListTag blocks = new ListTag();
blocks.add(block(0, 0, 0, 0, null));
template.put(StructureTemplate.BLOCKS_TAG, blocks);
ModdedStructureTemplateCapture.Capture capture = ModdedStructureTemplateCapture.captureTag(
StructureKey.parse("minecraft:test/legacy"),
template,
BuiltInRegistries.BLOCK,
true,
false
);
assertEquals(0, capture.blocks());
assertTrue(capture.object().getBlocks().isEmpty());
}
private static CompoundTag templateTag() {
CompoundTag template = new CompoundTag();
template.put(StructureTemplate.SIZE_TAG, intList(4, 1, 1));
ListTag palette = new ListTag();
palette.add(NbtUtils.writeBlockState(Blocks.STONE.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.CHEST.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.JIGSAW.defaultBlockState()));
palette.add(NbtUtils.writeBlockState(Blocks.STRUCTURE_BLOCK.defaultBlockState()));
template.put(StructureTemplate.PALETTE_TAG, palette);
ListTag blocks = new ListTag();
blocks.add(block(0, 0, 0, 0, null));
CompoundTag chest = new CompoundTag();
chest.putString("id", "minecraft:chest");
chest.putString("CustomName", "test");
blocks.add(block(1, 0, 0, 1, chest));
CompoundTag jigsaw = new CompoundTag();
jigsaw.putString("final_state", "minecraft:oak_planks");
blocks.add(block(2, 0, 0, 2, jigsaw));
blocks.add(block(3, 0, 0, 3, new CompoundTag()));
template.put(StructureTemplate.BLOCKS_TAG, blocks);
ListTag entities = new ListTag();
entities.add(new CompoundTag());
template.put(StructureTemplate.ENTITIES_TAG, entities);
return template;
}
private static CompoundTag block(int x, int y, int z, int state, CompoundTag nbt) {
CompoundTag block = new CompoundTag();
block.put(StructureTemplate.BLOCK_TAG_POS, intList(x, y, z));
block.putInt(StructureTemplate.BLOCK_TAG_STATE, state);
if (nbt != null) {
block.put(StructureTemplate.BLOCK_TAG_NBT, nbt);
}
return block;
}
private static ListTag intList(int... values) {
ListTag list = new ListTag();
for (int value : values) {
list.add(IntTag.valueOf(value));
}
return list;
}
private static boolean hasLoss(ModdedStructureTemplateCapture.Capture capture, String code) {
return capture.losses().stream().anyMatch(loss -> loss.code().equals(code));
}
}