Gate pools, placements and structures

- Filter excluded biomes, regions, spawners, loot tables, jigsaw pieces and structures out of every pool through CompatPools and cascade empty pools to the container
- Evaluate each object placement per object from the .iob palette header, honouring chance-1 edit rules and reporting every missing palette key
- Treat edit find lists and marker mark lists as match-only
- Exclude jigsaw pieces on missing palette keys, drop them from pools and exclude structures whose start pool is gone
- Drop custom biome spawns whose entity is missing before the datapack JSON is written
- Skip excluded placements in the object, floating object, tree and locator paths while keeping the per-chunk RNG draws unchanged
- Keep the engine focus biome and region null when the gate excluded them
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-03 04:54:35 -04:00
1 parent 516ef71c9c
commit 0d464aabf3
44 files changed
+2774 -93

No files matched your search

@@ -2,6 +2,7 @@ package art.arcane.iris.core;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.IrisGeneratorBinding;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.nms.datapack.DataVersion;
@@ -235,6 +236,7 @@ public final class IrisDatapackCompiler {
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
if (biome == null || !biome.isCustom()) {
continue;
@@ -250,7 +252,7 @@ public final class IrisDatapackCompiler {
}
requirements.put(
"worldgen/biome/" + biomeKey,
fingerprintContent(customBiome.generateJson(fixer)));
fingerprintContent(customBiome.generateJson(fixer, contentGate)));
TreeSet<String> tags = new TreeSet<>();
for (String tag : customBiome.getEffectiveTags(derivativeKey)) {
String normalizedTag = normalizeRegistryKey(tag);
@@ -142,8 +142,19 @@ public class TreeSVC implements IrisService {
return;
}
KList<String> pool = placement.compatPlace(worldAccess.getData());
if (pool.isEmpty()) {
IrisLogging.debug("Tree placement has no object available on this Minecraft version");
return;
}
saplingPlane.forEach(block -> block.setType(Material.AIR));
IrisObject object = worldAccess.getData().getObjectLoader().load(placement.getPlace().getRandom(RNG.r));
IrisObject object = worldAccess.getData().getObjectLoader().load(pool.getRandom(RNG.r));
if (object == null) {
return;
}
String treeMarker = object.getLoadKey() + "@" + ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
List<BlockState> blockStateList = new KList<>();
KMap<Location, BlockData> dataCache = new KMap<>();
@@ -311,12 +322,12 @@ public class TreeSVC implements IrisService {
// Retrieve objectPlacements of type `species` from biome
IrisBiome biome = worldAccess.getEngine().getBiome(location.getBlockX(), location.getBlockY()-worldAccess.getTarget().getWorld().minHeight(), location.getBlockZ());
placements.addAll(matchObjectPlacements(biome.getObjects(), size, type));
placements.addAll(matchObjectPlacements(biome.getObjects(), size, type, worldAccess.getData()));
// Add more or find any in the region
if (isUseAll || placements.isEmpty()) {
IrisRegion region = worldAccess.getEngine().getRegion(location.getBlockX(), location.getBlockZ());
placements.addAll(matchObjectPlacements(region.getObjects(), size, type));
placements.addAll(matchObjectPlacements(region.getObjects(), size, type, worldAccess.getData()));
}
// Check if no matches were found, return a random one if they are
@@ -329,14 +340,15 @@ public class TreeSVC implements IrisService {
* @param objects The object placements to check
* @param size The size of the sapling area to filter with
* @param type The type of the tree to filter with
* @param data The pack, for the version-content gate
* @return A list of objectPlacements that matched. May be empty.
*/
private KList<IrisObjectPlacement> matchObjectPlacements(KList<IrisObjectPlacement> objects, IrisTreeSize size, TreeType type) {
private KList<IrisObjectPlacement> matchObjectPlacements(KList<IrisObjectPlacement> objects, IrisTreeSize size, TreeType type, IrisData data) {
KList<IrisObjectPlacement> p = new KList<>();
for (IrisObjectPlacement i : objects) {
if (i.matches(size, type)) {
if (i.matches(size, type) && !i.isCompatExcluded(data)) {
p.add(i);
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
@@ -178,8 +179,9 @@ public class IrisComplex implements DataProvider {
fluidHeight = engine.getDimension().getFluidHeight();
generators = new HashMap<>();
generatorBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
focusBiome = engine.getFocus();
focusRegion = engine.getFocusRegion();
// A registrant the version-content gate excluded never enters a pool, focus included.
focusBiome = compatUsable(engine.getFocus());
focusRegion = compatUsable(engine.getFocusRegion());
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
KList<IrisRegion> preparedRegions = new KList<>();
@@ -195,7 +197,7 @@ public class IrisComplex implements DataProvider {
} else {
engine.getDimension().getRegions().forEach(regionKey -> {
IrisRegion region = data.getRegionLoader().load(regionKey);
if (region == null) {
if (region == null || region.isCompatExcluded()) {
return;
}
prepareInferredBiomes(region, preparedRegions);
@@ -244,7 +246,7 @@ public class IrisComplex implements DataProvider {
ProceduralStream.of((x, z) -> focusRegion,
Interpolated.of(a -> 0D, a -> focusRegion))
: regionStyleStream
.selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()))
.selectRarity(compatRegionPool(engine))
.cache2D("regionStream", engine, cacheSize);
regionStream = focusRegion != null ? proceduralRegionStream : proceduralRegionStream
.convertAware2D((region, x, z) -> {
@@ -1083,11 +1085,30 @@ public class IrisComplex implements DataProvider {
private KList<IrisBiome> loadInferredBiomes(KList<String> keys, InferredType type) {
KList<IrisBiome> inferred = new KList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
// Excluded biomes cannot generate on this Minecraft version, so they leave the selection pool.
if (biome.isCompatExcluded()) {
continue;
}
inferred.add(biome.withInferredType(type));
}
return inferred;
}
/** The dimension's region pool with regions the version-content gate excluded removed. */
private KList<IrisRegion> compatRegionPool(Engine engine) {
KList<IrisRegion> pool = new KList<>();
for (IrisRegion region : data.getRegionLoader().loadAll(engine.getDimension().getRegions())) {
if (!region.isCompatExcluded()) {
pool.add(region);
}
}
return pool;
}
private static <T extends IrisRegistrant> T compatUsable(T registrant) {
return registrant == null || registrant.isCompatExcluded() ? null : registrant;
}
private void registerGenerators(IrisBiome biome) {
generatorBiomes.add(biome);
biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this)));
@@ -83,6 +83,9 @@ final class MarkerSpawnScanner {
IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName());
continue;
}
if (m.isCompatExcluded()) {
continue;
}
m.setReferenceMarker(mark);
// This is so fucking incorrect its a joke
@@ -182,6 +185,9 @@ final class MarkerSpawnScanner {
IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName());
continue;
}
if (spawner.isCompatExcluded()) {
continue;
}
spawner.setReferenceMarker(mark);
data.spawners.add(spawner);
}
@@ -299,7 +299,8 @@ final class WorldEntitySpawner {
}
//@builder
Predicate<IrisSpawner> filter = i -> i.canSpawn(manager.getEngine(), c.getX(), c.getZ());
// Excluded spawners cannot spawn anything on this Minecraft version, so they never enter the selection pool.
Predicate<IrisSpawner> filter = i -> !i.isCompatExcluded() && i.canSpawn(manager.getEngine(), c.getX(), c.getZ());
ChunkCounter counter = new ChunkCounter(c.getEntities());
IrisBiome biome = EngineBukkitOps.getSurfaceBiome(manager.getEngine(), c);
@@ -502,7 +502,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisRegion region = getRegion(x, z);
for (IrisObjectPlacement i : region.getObjects()) {
if (i.getPlace().contains(object)) {
if (i.compatPlace(getData()).contains(object)) {
return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z);
}
}
@@ -510,7 +510,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisBiome biome = getSurfaceBiome(x, z);
for (IrisObjectPlacement i : biome.getObjects()) {
if (i.getPlace().contains(object)) {
if (i.compatPlace(getData()).contains(object)) {
return new PlacedObject(i, getData().getObjectLoader().load(object), id, x, z);
}
}
@@ -526,19 +526,20 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return false;
}
IrisData data = getData();
Set<String> biomeKeys = getDimension().getReachableBiomes(this).stream()
.filter((i) -> containsObjectPlacement(i.getObjects(), normalizedObjectKey))
.filter((i) -> containsObjectPlacement(i.getObjects(), normalizedObjectKey, data))
.map(IrisRegistrant::getLoadKey)
.collect(Collectors.toSet());
Set<String> regionKeys = getDimension().getAllRegions(this).stream()
.filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains)
|| containsObjectPlacement(i.getObjects(), normalizedObjectKey))
|| containsObjectPlacement(i.getObjects(), normalizedObjectKey, data))
.map(IrisRegistrant::getLoadKey)
.collect(Collectors.toSet());
return !regionKeys.isEmpty();
}
private static boolean containsObjectPlacement(KList<IrisObjectPlacement> placements, String normalizedObjectKey) {
private static boolean containsObjectPlacement(KList<IrisObjectPlacement> placements, String normalizedObjectKey, IrisData data) {
if (placements == null || placements.isEmpty() || normalizedObjectKey.isBlank()) {
return false;
}
@@ -548,7 +549,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
continue;
}
for (String placedObject : placement.getPlace()) {
for (String placedObject : placement.compatPlace(data)) {
String normalizedPlacedObject = normalizeObjectPlacementKey(placedObject);
if (!normalizedPlacedObject.isBlank() && normalizedPlacedObject.equals(normalizedObjectKey)) {
return true;
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
@@ -410,7 +411,7 @@ public final class HintedLocator<T> implements Locator<T> {
continue;
}
if (placesObject(region.getSurfaceObjects(), objectKey)) {
if (placesObject(region.getSurfaceObjects(), objectKey, engine.getData())) {
directRegions.add(region.getLoadKey());
}
@@ -511,9 +512,9 @@ public final class HintedLocator<T> implements Locator<T> {
};
}
private static boolean placesObject(KList<IrisObjectPlacement> placements, String objectKey) {
private static boolean placesObject(KList<IrisObjectPlacement> placements, String objectKey, IrisData data) {
for (IrisObjectPlacement placement : placements) {
if (placement != null && placement.getPlace().contains(objectKey)) {
if (placement != null && placement.compatPlace(data).contains(objectKey)) {
return true;
}
}
@@ -637,7 +638,7 @@ public final class HintedLocator<T> implements Locator<T> {
continue;
}
if (placesObject(biome.getSurfaceObjects(), objectKey)) {
if (placesObject(biome.getSurfaceObjects(), objectKey, engine.getData())) {
return true;
}
@@ -507,6 +507,10 @@ public final class IrisStructureLocator {
throw new IllegalStateException("Iris structure placement references missing structure '"
+ selectedKey + "'");
}
if (structure.isCompatExcluded()) {
// Gated out on this Minecraft version; the pack report already says why.
return null;
}
StructureGraphCompilation compilation = StructureGraphCatalog.compile(engine.getData(), structure);
if (!compilation.isAssemblyViable()) {
throw new IllegalStateException("Iris structure '" + selectedKey
@@ -1179,6 +1179,11 @@ public final class StructureAssembler {
if (pool.getPieces() == null) {
throw assemblyFailure("pool has no piece list");
}
// A pool the version-content gate excluded has nothing left to place: treat it as empty so the branch
// terminates or falls back, instead of failing the whole assembly.
if (pool.isCompatExcluded()) {
return List.of();
}
List<IrisJigsawPieceEntry> eligible = new ArrayList<>();
List<IrisJigsawPieceEntry> required = new ArrayList<>();
for (IrisJigsawPieceEntry entry : pool.getPieces()) {
@@ -1199,6 +1204,9 @@ public final class StructureAssembler {
if (piece == null) {
throw assemblyFailure("pool references missing piece '" + pieceKey + "'");
}
if (piece.isCompatExcluded()) {
continue;
}
int placements = placementCounts.getOrDefault(pieceKey, 0);
if (!pieceEnabled(piece)
|| terminalOnly && !piece.resolvedRules().isTerminal()
@@ -1,5 +1,7 @@
package art.arcane.iris.engine.hydrology.policy;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
@@ -17,7 +19,8 @@ public final class RiverPolicyResolver {
return resolve(
dimension == null ? null : dimension.getRiverPolicy(),
region == null ? null : region.getRiverPolicy(),
biome == null ? null : biome.getRiverPolicy()
biome == null ? null : biome.getRiverPolicy(),
loaderOf(biome, region, dimension)
);
}
@@ -25,14 +28,37 @@ public final class RiverPolicyResolver {
IrisRiverPolicy dimensionPolicy,
IrisRiverPolicy regionPolicy,
IrisRiverPolicy biomePolicy
) {
return resolve(dimensionPolicy, regionPolicy, biomePolicy, null);
}
/**
* @param data pack the policies came from, used to drop river biome references the version-content gate excluded.
* Null skips that filtering.
*/
public static EffectiveRiverPolicy resolve(
IrisRiverPolicy dimensionPolicy,
IrisRiverPolicy regionPolicy,
IrisRiverPolicy biomePolicy,
IrisData data
) {
State state = new State();
state.apply(dimensionPolicy);
state.apply(regionPolicy);
state.apply(biomePolicy);
state.apply(dimensionPolicy, data);
state.apply(regionPolicy, data);
state.apply(biomePolicy, data);
return state.build();
}
private static IrisData loaderOf(IrisRegistrant... registrants) {
for (IrisRegistrant registrant : registrants) {
if (registrant != null && registrant.getLoader() != null) {
return registrant.getLoader();
}
}
return null;
}
private static final class State {
private IrisRiverPlacementMode placement = IrisRiverPlacementMode.NATURAL;
private IrisRiverRoutingMode routing = IrisRiverRoutingMode.ALLOW;
@@ -50,7 +76,7 @@ public final class RiverPolicyResolver {
private double routingMultiplier = 1D;
private double bankMultiplier = 1D;
private void apply(IrisRiverPolicy policy) {
private void apply(IrisRiverPolicy policy, IrisData data) {
if (policy == null) {
return;
}
@@ -67,19 +93,19 @@ public final class RiverPolicyResolver {
profiles = List.copyOf(policy.getProfiles());
}
if (policy.getSurfaceBiomes() != null) {
surfaceBiomes = List.copyOf(policy.getSurfaceBiomes());
surfaceBiomes = List.copyOf(policy.compatBiomes(policy.getSurfaceBiomes(), data, "surfaceBiomes"));
}
if (policy.getMouthBiomes() != null) {
mouthBiomes = List.copyOf(policy.getMouthBiomes());
mouthBiomes = List.copyOf(policy.compatBiomes(policy.getMouthBiomes(), data, "mouthBiomes"));
}
if (policy.getShoreBiomes() != null) {
shoreBiomes = List.copyOf(policy.getShoreBiomes());
shoreBiomes = List.copyOf(policy.compatBiomes(policy.getShoreBiomes(), data, "shoreBiomes"));
}
if (policy.getBankBiomes() != null) {
bankBiomes = List.copyOf(policy.getBankBiomes());
bankBiomes = List.copyOf(policy.compatBiomes(policy.getBankBiomes(), data, "bankBiomes"));
}
if (policy.getFloodedCaveBiomes() != null) {
floodedCaveBiomes = List.copyOf(policy.getFloodedCaveBiomes());
floodedCaveBiomes = List.copyOf(policy.compatBiomes(policy.getFloodedCaveBiomes(), data, "floodedCaveBiomes"));
}
if (policy.getSurfacePools() != null) {
surfacePools = List.copyOf(policy.getSurfacePools());
@@ -171,6 +171,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (placement == null || columns == null || columns.isEmpty()) {
return;
}
if (placement.isCompatExcluded(data)) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
for (int i = 0; i < density; i++) {
@@ -217,6 +221,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (placement == null || columns.isEmpty()) {
return;
}
if (placement.isCompatExcluded(data)) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
@@ -302,6 +310,10 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
if (placement == null || columns.isEmpty()) {
return;
}
if (placement.isCompatExcluded(data)) {
return;
}
int density = placement.getDensity(rng, minX, minZ, data);
double perAttempt = placement.getChance();
@@ -417,7 +417,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (IrisObjectPlacement i : surfaceBiome.getSurfaceObjects()) {
biomeSurfaceChecked++;
boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005));
boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData());
if (traceRegen) {
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=biome-surface"
@@ -450,7 +450,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue;
}
biomeCaveChecked++;
boolean chance = rng.chance(i.getChance());
boolean chance = rng.chance(i.getChance()) && !i.isCompatExcluded(getData());
if (traceRegen) {
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=biome-cave"
@@ -480,7 +480,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (IrisObjectPlacement i : region.getSurfaceObjects()) {
regionSurfaceChecked++;
boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005));
boolean chance = rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData());
if (traceRegen) {
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=region-surface"
@@ -513,7 +513,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
continue;
}
regionCaveChecked++;
boolean chance = rng.chance(i.getChance());
boolean chance = rng.chance(i.getChance()) && !i.isCompatExcluded(getData());
if (traceRegen) {
IrisLogging.debug("Regen object placer chance: chunk=" + x + "," + z
+ " scope=region-cave"
@@ -1182,7 +1182,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
boolean forcePlace = dimension.isUpperObjectsForcePlace();
if (upperBiome != null) {
for (IrisObjectPlacement i : upperBiome.getSurfaceObjects()) {
if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) {
if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005)) || i.isCompatExcluded(getData())) {
continue;
}
try {
@@ -1197,7 +1197,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (upperRegion != null) {
for (IrisObjectPlacement i : upperRegion.getSurfaceObjects()) {
if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005))) {
if (!rng.chance(i.getChance() + rng.d(-0.005, 0.005)) || i.isCompatExcluded(getData())) {
continue;
}
try {
@@ -1635,13 +1635,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8);
Set<String> v = new KSet<>();
for (IrisObjectPlacement i : biome.getSurfaceObjects()) {
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) {
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) {
v.addAll(guessPlacedKeys(rng, x, z, i));
}
}
for (IrisObjectPlacement i : region.getSurfaceObjects()) {
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005))) {
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) {
v.addAll(guessPlacedKeys(rng, x, z, i));
}
}
@@ -0,0 +1,219 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.volmlib.util.collection.KList;
import java.util.ArrayList;
import java.util.List;
/**
* Registrant reference pools filtered by the version-content gate. A reference to a registrant the gate EXCLUDED is
* dropped from every pool that could pick it and reported once against the containing registrant; a container whose
* required pool ends up empty cascades to EXCLUDED through {@link #cascade}. Cascade and drop findings reuse the
* registry and key of the cause so the console groups everything under the one missing content key.
* <p>
* Surviving entries keep their declaration order, so only the removed entries change what a pool picks.
*/
final class CompatPools {
private CompatPools() {
}
static boolean excluded(IrisRegistrant registrant) {
return registrant != null && registrant.isCompatExcluded();
}
/**
* Loads a registrant reference pool, skipping entries that fail to load and entries the gate excluded. Each
* excluded reference produces one DROPPED finding in {@code sink} (when given) and in the pack report.
*
* @param field the JSON field the keys came from, used for the finding detail ({@code landBiomes[2] key})
*/
static <T extends IrisRegistrant> KList<T> load(ResourceLoader<T> loader,
List<String> keys,
IrisData data,
String subjectType,
String subjectKey,
String field,
List<CompatFinding> sink) {
KList<T> kept = new KList<>();
if (loader == null || keys == null) {
return kept;
}
for (int index = 0; index < keys.size(); index++) {
String key = keys.get(index);
T loaded = loader.load(key);
if (loaded == null) {
continue;
}
if (loaded.isCompatExcluded()) {
drop(data, loaded, subjectType, subjectKey, field + "[" + index + "] " + key, sink);
continue;
}
kept.add(loaded);
}
return kept;
}
/** The surviving keys of a reference pool, in declaration order. */
static <T extends IrisRegistrant> KList<String> surviving(ResourceLoader<T> loader,
List<String> keys,
IrisData data,
String subjectType,
String subjectKey,
String field) {
KList<String> kept = new KList<>();
if (keys == null) {
return kept;
}
if (loader == null) {
kept.addAll(keys);
return kept;
}
for (int index = 0; index < keys.size(); index++) {
String key = keys.get(index);
T loaded = loader.load(key);
if (loaded != null && loaded.isCompatExcluded()) {
drop(data, loaded, subjectType, subjectKey, field + "[" + index + "] " + key, null);
continue;
}
kept.add(key);
}
return kept;
}
/**
* Records one DROPPED finding for a reference to an excluded registrant, reusing the registry and key of the
* child's exclusion cause. Returns null when the child carries no cause to attribute the drop to.
*/
static CompatFinding drop(IrisData data,
IrisRegistrant child,
String subjectType,
String subjectKey,
String detail,
List<CompatFinding> sink) {
CompatFinding cause = causeOf(child == null ? null : child.getCompat());
if (cause == null) {
return null;
}
CompatFinding finding = new CompatFinding(cause.registry(), cause.key(), CompatAction.DROPPED,
subjectType, subjectKey, detail);
record(data, finding);
if (sink != null) {
sink.add(finding);
}
return finding;
}
/**
* The EXCLUDED status for a container whose required pool lost every entry. Returns {@code base} untouched when no
* cause was collected, so a container that never declared the pool is not excluded for being empty.
*/
static CompatStatus cascade(IrisData data,
CompatStatus base,
List<CompatFinding> causes,
String subjectType,
String subjectKey,
String detail) {
if (causes == null || causes.isEmpty()) {
return base;
}
CompatFinding cause = causes.getFirst();
CompatFinding finding = new CompatFinding(cause.registry(), cause.key(), CompatAction.EXCLUDED,
subjectType, subjectKey, detail);
record(data, finding);
List<CompatFinding> reasons = new ArrayList<>(base == null ? List.of() : base.reasons());
reasons.add(finding);
return CompatStatus.excludedBy(reasons);
}
/** The findings on a status that dropped an entry, used as the cause set of a cascade. */
static List<CompatFinding> droppedReasons(CompatStatus status) {
List<CompatFinding> dropped = new ArrayList<>();
if (status == null) {
return dropped;
}
for (CompatFinding reason : status.reasons()) {
if (reason.action() == CompatAction.DROPPED) {
dropped.add(reason);
}
}
return dropped;
}
static void record(IrisData data, CompatFinding finding) {
if (data == null || finding == null) {
return;
}
try {
data.getCompatReport().record(finding);
} catch (Throwable ignored) {
// A report is best effort: never let reporting break a pool build.
}
}
/** The finding that explains an exclusion: the EXCLUDED reason if there is one, otherwise the first reason. */
private static CompatFinding causeOf(CompatStatus status) {
if (status == null) {
return null;
}
CompatFinding fallback = null;
for (CompatFinding reason : status.reasons()) {
if (reason.action() == CompatAction.EXCLUDED) {
return reason;
}
if (fallback == null) {
fallback = reason;
}
}
return fallback;
}
}
@@ -559,13 +559,9 @@ public class IrisBiome extends IrisRegistrant implements IRare {
public KList<IrisBiome> getRealChildren(DataProvider g) {
return realChildren.aquire(() ->
{
KList<IrisBiome> realChildren = new KList<>();
for (String i : getChildren()) {
realChildren.add(g.getData().getBiomeLoader().load(i));
}
return realChildren;
IrisData data = g == null ? null : g.getData();
return CompatPools.load(data == null ? null : data.getBiomeLoader(), getChildren(), data,
"biome", getLoadKey(), "children", null);
});
}
@@ -18,6 +18,8 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.KeyStatus;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.DependsOn;
@@ -132,6 +134,16 @@ public class IrisBiomeCustom {
}
public String generateJson(IDataFixer fixer) {
return generateJson(fixer, null);
}
/**
* @param gate version-content gate used to keep spawns whose entity type is missing on this server out of the
* generated datapack (the game rejects a datapack that names an unknown entity). Null consults the
* live platform registries; an unreadable registry keeps every spawn.
*/
public String generateJson(IDataFixer fixer, ContentGate gate) {
ContentGate contentGate = gate == null ? ContentGate.forData(null) : gate;
JSONObject effects = new JSONObject();
effects.put("sky_color", parseColor(getSkyColor()));
effects.put("fog_color", parseColor(getFogColor()));
@@ -196,6 +208,10 @@ public class IrisBiomeCustom {
IrisLogging.warn("Skipping custom biome spawn with null entity type in biome " + getId());
continue;
}
if (contentGate.entity(key) == KeyStatus.MISSING) {
IrisLogging.debug("Dropping custom biome spawn " + key + " in biome " + getId() + ": entity missing on this server");
continue;
}
IrisBiomeCustomSpawnType group = i.getGroup() == null ? IrisBiomeCustomSpawnType.MISC : i.getGroup();
JSONArray g = groups.computeIfAbsent(group, (k) -> new JSONArray());
JSONObject o = new JSONObject();
@@ -63,7 +63,14 @@ public class IrisDimensionCarvingEntry {
return null;
}
return data.getBiomeLoader().load(biomeKey.trim());
IrisBiome loaded = data.getBiomeLoader().load(biomeKey.trim());
if (loaded != null && loaded.isCompatExcluded()) {
CompatPools.drop(data, loaded, "carving entry", getId(), "biome " + biomeKey.trim(), null);
return null;
}
return loaded;
});
}
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.engine.data.cache.AtomicCache;
@@ -147,7 +148,17 @@ public class IrisEntitySpawn implements IRare {
}
public IrisEntity getRealEntity(Engine g) {
return ent.aquire(() -> g.getData().getEntityLoader().load(getEntity()));
return ent.aquire(() -> resolveEntity(g.getData()));
}
/** The referenced entity, or null when it does not load or the version-content gate excluded it. */
IrisEntity resolveEntity(IrisData data) {
if (data == null || data.getEntityLoader() == null) {
return null;
}
IrisEntity entity = data.getEntityLoader().load(getEntity());
return entity == null || entity.isCompatExcluded() ? null : entity;
}
public Entity spawn(Engine g, Location at) {
@@ -358,6 +358,11 @@ public class IrisFloatingChildBiomes implements IRare {
return parent;
}
if (loaded.isCompatExcluded()) {
CompatPools.drop(data, loaded, "floating child biome", parent.getLoadKey(), "biome " + biome, null);
return parent;
}
return loaded;
});
}
@@ -18,6 +18,12 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
@@ -81,6 +87,42 @@ public class IrisJigsawPiece extends IrisRegistrant {
return rules == null ? new IrisJigsawPieceRules() : rules;
}
/**
* A piece is one object stamped verbatim - nothing rewrites its blocks - so a palette key the server does not
* have (after the pack's declared fallbacks) takes the whole piece out of every pool that lists it.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded() || gate == null || !gate.ready()) {
return base;
}
IrisData data = getLoader();
if (data == null || object == null || object.isBlank()) {
return base;
}
for (String paletteKey : IrisObjectIO.readPaletteKeysCached(data.getObjectLoader().findFile(object.trim()))) {
String key = ContentGate.baseKey(ContentGate.normalizeState(paletteKey));
if (key == null || gate.resolveBlock(key) != null) {
continue;
}
CompatFinding finding = new CompatFinding(CompatRegistry.BLOCK, key, CompatAction.EXCLUDED,
"jigsaw piece", getLoadKey(), "object " + object.trim());
gate.report().record(finding);
KList<CompatFinding> reasons = new KList<>(base.reasons());
reasons.add(finding);
return CompatStatus.excludedBy(reasons);
}
return base;
}
@Override
public String getFolderName() {
return "jigsaw-pieces";
@@ -18,6 +18,12 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
@@ -51,6 +57,75 @@ public class IrisJigsawPool extends IrisRegistrant {
return structureRequiresCaps || mandatoryFallback;
}
/**
* Excluded pieces drop out of the pool; a pool with nothing left to place is excluded in turn so every connector
* that targets it terminates instead of failing assembly.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded() || gate == null || !gate.ready()) {
return base;
}
IrisData data = getLoader();
if (data == null || pieces == null || pieces.isEmpty()) {
return base;
}
KList<CompatFinding> reasons = new KList<>(base.reasons());
CompatFinding cause = null;
int usable = 0;
for (int i = 0; i < pieces.size(); i++) {
IrisJigsawPieceEntry entry = pieces.get(i);
if (entry == null) {
continue;
}
String key = entry.getPiece() == null ? "" : entry.getPiece().trim();
// An empty membership terminates a branch on purpose, and a piece key that does not resolve at all is a
// pack error the loader already reports - neither is a version-content problem.
if (entry.isEmpty() || key.isEmpty()) {
usable++;
continue;
}
IrisJigsawPiece piece = data.load(IrisJigsawPiece.class, key, false);
if (piece == null || !piece.isCompatExcluded()) {
usable++;
continue;
}
CompatFinding reason = piece.getCompat().reasons().isEmpty() ? null : piece.getCompat().reasons().getFirst();
CompatFinding dropped = new CompatFinding(
reason == null ? CompatRegistry.BLOCK : reason.registry(),
reason == null ? key : reason.key(),
CompatAction.DROPPED, "jigsaw pool", getLoadKey(), "pieces[" + i + "] " + key);
gate.report().record(dropped);
reasons.add(dropped);
if (cause == null) {
cause = dropped;
}
}
if (usable > 0 || cause == null) {
return new CompatStatus(false, reasons);
}
CompatFinding excluded = new CompatFinding(cause.registry(), cause.key(), CompatAction.EXCLUDED,
"jigsaw pool", getLoadKey(), "no pieces remain");
gate.report().record(excluded);
reasons.add(excluded);
return CompatStatus.excludedBy(reasons);
}
@Override
public String getFolderName() {
return "jigsaw-pools";
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
@@ -57,9 +58,25 @@ public class IrisLootReference {
return tt.aquire(() ->
{
KList<IrisLootTable> t = new KList<>();
IrisData data = g == null ? null : g.getData();
if (data == null || data.getLootLoader() == null) {
return t;
}
for (String i : tables) {
t.add(g.getData().getLootLoader().load(i));
IrisLootTable table = data.getLootLoader().load(i);
if (table == null) {
continue;
}
if (table.isCompatExcluded()) {
CompatPools.drop(data, table, "loot reference", table.getLoadKey(), "tables " + i, null);
continue;
}
t.add(table);
}
return t;
@@ -18,6 +18,8 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.framework.LootResolver;
import art.arcane.iris.engine.object.annotations.ArrayType;
@@ -103,6 +105,25 @@ public class IrisLootTable extends IrisRegistrant {
return lootf;
}
/**
* Cascade: the gate walker drops loot entries whose item is missing on this server. A table with nothing left to
* roll leaves every reference pool that could pick it.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
return cascadeEmptyLoot(super.evaluateCompat(gate));
}
/** EXCLUDED when the walker dropped every loot entry; the walker's verdict is passed in as {@code base}. */
CompatStatus cascadeEmptyLoot(CompatStatus base) {
if (base.excluded() || !loot.isEmpty()) {
return base;
}
return CompatPools.cascade(getLoader(), base, CompatPools.droppedReasons(base),
"loot", getLoadKey(), "no loot entries remain");
}
@Override
public String getFolderName() {
return "loot";
@@ -18,6 +18,9 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
@@ -104,6 +107,49 @@ public class IrisMod extends IrisRegistrant {
@Desc("Replace noise styles with other styles")
private KList<IrisModNoiseStyleReplacer> styleReplacers = new KList<>();
/**
* A pack modification only references other registrants, so it never cascades: excluded biome and region
* references are dropped from its lists and reported, and the modification keeps applying whatever is left.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
IrisData data = getLoader();
if (data == null) {
return base;
}
removeBiomes = biomes(data, removeBiomes, "removeBiomes");
removeRegions = regions(data, removeRegions, "removeRegions");
injectRegions = regions(data, injectRegions, "injectRegions");
for (int index = 0; index < biomeInjectors.size(); index++) {
IrisModBiomeInjector injector = biomeInjectors.get(index);
injector.setInject(biomes(data, injector.getInject(), "biomeInjectors[" + index + "].inject"));
}
for (int index = 0; index < biomeReplacers.size(); index++) {
IrisModBiomeReplacer replacer = biomeReplacers.get(index);
replacer.setFind(biomes(data, replacer.getFind(), "biomeReplacers[" + index + "].find"));
}
for (int index = 0; index < regionReplacers.size(); index++) {
IrisModRegionReplacer replacer = regionReplacers.get(index);
replacer.setFind(regions(data, replacer.getFind(), "regionReplacers[" + index + "].find"));
}
return base;
}
private KList<String> biomes(IrisData data, KList<String> keys, String field) {
return CompatPools.surviving(data.getBiomeLoader(), keys, data, "mod", getLoadKey(), field);
}
private KList<String> regions(IrisData data, KList<String> keys, String field) {
return CompatPools.surviving(data.getRegionLoader(), keys, data, "mod", getLoadKey(), field);
}
@Override
public String getFolderName() {
return "mods";
@@ -63,7 +63,7 @@ public class IrisObjectMarker {
KList<PlatformBlockState> b = new KList<>();
for (IrisBlockData i : mark) {
PlatformBlockState bx = i.getBlockData(rdata);
PlatformBlockState bx = i.getBlockDataOrPlaceholder(rdata);
if (bx != null) {
b.add(bx);
@@ -20,6 +20,12 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.cache.LazyBoundedCache;
@@ -181,6 +187,274 @@ public class IrisObjectPlacement {
@SerializedName(value = "forcePlace", alternate = {"force"})
private boolean forcePlace = false;
private transient AtomicCache<TableCache> cache = new AtomicCache<>();
private transient AtomicCache<CompatStatus> compatCache = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile KList<String> compatPlaceCache;
/**
* Version-content gate verdict for this placement on the running server. The placement's own generated blocks
* (every {@code edit[].replace} palette option) exclude the whole placement when one of them is unavailable; each
* object in {@code place} is then dropped when its .iob palette carries a key that neither resolves nor is
* rewritten by a matching {@code edit} rule with chance 1. Match sides ({@code edit[].find}, {@code markers[].mark})
* never count. Cached per placement.
*/
public CompatStatus evaluateCompat(IrisData data) {
return compatCache.aquire(() -> computeCompat(data));
}
/** True when nothing in this placement can generate on the running server. */
public boolean isCompatExcluded(IrisData data) {
return evaluateCompat(data).excluded();
}
/** The {@code place} pool with objects that cannot generate on the running server removed. */
public KList<String> compatPlace(IrisData data) {
evaluateCompat(data);
KList<String> survivors = compatPlaceCache;
return survivors == null ? place : survivors;
}
private CompatStatus computeCompat(IrisData data) {
if (data == null) {
compatPlaceCache = place;
return CompatStatus.OK;
}
ContentGate gate = data.getContentGate();
PackCompatReport report = data.getCompatReport();
if (gate == null || !gate.ready()) {
// Registry not bound or not ready: UNKNOWN is never MISSING, so nothing is gated.
report.markIncomplete("registry not ready while evaluating object placements");
compatPlaceCache = place;
return CompatStatus.OK;
}
String subject = compatSubject();
KList<CompatFinding> reasons = new KList<>();
CompatFinding blocked = evaluatePlacementBlocks(data, gate, subject, reasons);
record(report, reasons);
if (blocked != null) {
compatPlaceCache = new KList<>();
return CompatStatus.excludedBy(reasons);
}
KList<String> survivors = new KList<>(place.size());
CompatFinding firstDrop = null;
for (int i = 0; i < place.size(); i++) {
String objectKey = place.get(i);
KList<CompatFinding> objectReasons = new KList<>();
KList<String> missing = unplaceableKeys(objectKey, data, gate, subject + " place[" + i + "]", objectReasons);
record(report, objectReasons);
reasons.addAll(objectReasons);
if (missing.isEmpty()) {
survivors.add(objectKey);
continue;
}
// One finding per missing key, so the operator sees every fallback the object needs at once.
for (String key : missing) {
CompatFinding dropped = new CompatFinding(CompatRegistry.BLOCK, key, CompatAction.DROPPED,
"object", objectKey, subject + " place[" + i + "]");
report.record(dropped);
reasons.add(dropped);
if (firstDrop == null) {
firstDrop = dropped;
}
}
}
if (firstDrop != null && survivors.isEmpty()) {
CompatFinding excluded = new CompatFinding(firstDrop.registry(), firstDrop.key(), CompatAction.EXCLUDED,
"placement", subject, "no objects remain");
report.record(excluded);
reasons.add(excluded);
compatPlaceCache = new KList<>();
return CompatStatus.excludedBy(reasons);
}
compatPlaceCache = survivors.size() == place.size() ? place : survivors;
return new CompatStatus(false, reasons);
}
/** The subject a finding names: the single object, or the whole place list when there is more than one. */
private String compatSubject() {
if (place.isEmpty()) {
return "";
}
return place.size() == 1 ? place.getFirst() : place.toString(", ");
}
/**
* The placement's own composition blocks: the {@code edit[].replace} palettes. {@code edit[].find} and
* {@code markers[].mark} are match sides and never count - matching a block the server does not have is harmless.
*
* @return the finding that excludes this placement, or null when every declared block resolves
*/
private CompatFinding evaluatePlacementBlocks(IrisData data, ContentGate gate, String subject, KList<CompatFinding> reasons) {
CompatFinding blocked = null;
for (int i = 0; i < edit.size(); i++) {
IrisObjectReplace rule = edit.get(i);
if (rule == null || rule.getReplace() == null) {
continue;
}
KList<IrisBlockData> palette = rule.getReplace().getPalette();
for (int j = 0; j < palette.size(); j++) {
CompatFinding finding = evaluateBlockEntry(palette.get(j), data, gate, subject,
"edit[" + i + "].replace.palette[" + j + "]", reasons);
if (finding != null && blocked == null) {
blocked = finding;
}
}
}
return blocked;
}
/** Records what happened to one declared block entry; returns the finding when it excludes the placement. */
private CompatFinding evaluateBlockEntry(IrisBlockData block, IrisData data, ContentGate gate, String subject,
String detail, KList<CompatFinding> reasons) {
if (block == null) {
return null;
}
BlockCompat resolution = resolveBlockEntry(block, data, gate, new KList<>());
if (resolution.missing()) {
CompatFinding finding = new CompatFinding(CompatRegistry.BLOCK, resolution.key(), CompatAction.EXCLUDED,
"placement", subject, detail);
reasons.add(finding);
return finding;
}
if (resolution.substituted()) {
reasons.add(new CompatFinding(CompatRegistry.BLOCK, resolution.key(), CompatAction.SUBSTITUTED,
"placement", subject, detail + " (using " + resolution.replacement() + ")"));
}
return null;
}
/**
* Every distinct palette key of {@code objectKey} that cannot reach the world, in palette order; empty when the
* object still places. A key survives when the gate resolves it (dimension fallbacks count) or when an
* {@code edit} rule with chance 1 matches it - that rule's replace palette was already verified by
* {@link #evaluatePlacementBlocks}. A rule below chance 1 does not cover the key: the original block can still be
* rolled.
*/
private KList<String> unplaceableKeys(String objectKey, IrisData data, ContentGate gate, String detail, KList<CompatFinding> reasons) {
KList<String> missing = new KList<>();
// Palette header only - no IrisObject is built and no state is resolved. A legacy (V1) object reads as an
// empty palette and is therefore never dropped; V1 has no palette block to scan without a full load.
for (String paletteKey : IrisObjectIO.readPaletteKeysCached(data.getObjectLoader().findFile(objectKey))) {
String base = ContentGate.baseKey(ContentGate.normalizeState(paletteKey));
if (base == null) {
continue;
}
// Existence is a registry question about the block, never about its properties: a property that was
// renamed on a block the server still has is out of scope for the gate.
ContentGate.BlockResolution resolution = gate.resolveBlock(base);
if (resolution != null) {
if (resolution.substituted()) {
reasons.add(new CompatFinding(CompatRegistry.BLOCK, base, CompatAction.SUBSTITUTED,
"object", objectKey, detail + " (using " + resolution.resolvedKey() + ")"));
}
continue;
}
if (coveredByEdit(paletteKey)) {
continue;
}
missing.addIfMissing(base);
}
return missing;
}
/** True when an {@code edit} rule certainly rewrites this palette state before it is written. */
private boolean coveredByEdit(String paletteKey) {
for (IrisObjectReplace rule : edit) {
if (rule == null || rule.getChance() < 1F) {
continue;
}
if (rule.matchesState(paletteKey)) {
return true;
}
}
return false;
}
/**
* Mirrors {@code IrisBlockData.getBlockData} against the gate: pack custom block first, then the registry with
* dimension fallbacks, then the entry's own {@code backup}.
*/
private static BlockCompat resolveBlockEntry(IrisBlockData block, IrisData data, ContentGate gate, KList<IrisBlockData> seen) {
for (IrisBlockData visited : seen) {
if (visited == block) {
return new BlockCompat(String.valueOf(block.getBlock()), null, true, false);
}
}
seen.add(block);
String declared = block.getBlock();
IrisBlockData custom = data.getBlockLoader() == null ? null : data.getBlockLoader().load(declared, false);
if (custom != null && custom != block) {
return resolveBlockEntry(custom, data, gate, seen);
}
String base = ContentGate.baseKey(ContentGate.normalizeState(declared));
if (base == null) {
return new BlockCompat(String.valueOf(declared), null, true, false);
}
ContentGate.BlockResolution resolution = gate.resolveBlock(base);
if (resolution != null) {
return new BlockCompat(base, resolution.resolvedKey(), false, resolution.substituted());
}
IrisBlockData backup = block.getBackup();
if (backup != null) {
BlockCompat viaBackup = resolveBlockEntry(backup, data, gate, seen);
if (!viaBackup.missing()) {
return new BlockCompat(base, viaBackup.replacement(), false, true);
}
}
return new BlockCompat(base, null, true, false);
}
private static void record(PackCompatReport report, KList<CompatFinding> findings) {
for (CompatFinding finding : findings) {
report.record(finding);
}
}
/** One block entry's verdict: the key asked for, what will actually generate, and how it got there. */
private record BlockCompat(String key, String replacement, boolean missing, boolean substituted) {
}
public IrisObjectPlacement toPlacement(String... place) {
IrisObjectPlacement p = new IrisObjectPlacement();
@@ -244,11 +518,14 @@ public class IrisObjectPlacement {
}
public IrisObject getObject(DataProvider g, RNG random) {
if (place.isEmpty()) {
IrisData data = g.getData();
KList<String> pool = compatPlace(data);
if (pool.isEmpty()) {
return null;
}
return g.getData().getObjectLoader().load(place.get(random.nextInt(place.size())));
return data.getObjectLoader().load(pool.get(random.nextInt(pool.size())));
}
public boolean matches(IrisTreeSize size, TreeType type) {
@@ -298,6 +575,9 @@ public class IrisObjectPlacement {
IrisLogging.warn("Couldn't find loot table " + loot.getName());
continue;
}
if (table.isCompatExcluded()) {
continue;
}
if (loot.getFilter().isEmpty()) //Table applies to all containers
{
@@ -424,7 +424,7 @@ final class IrisObjectPlacementRunner {
IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker());
int markerSalt = markerIndex++;
if (marker == null) {
if (marker == null || marker.isCompatExcluded()) {
continue;
}
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.ArrayType;
@@ -35,6 +36,9 @@ import lombok.NoArgsConstructor;
import art.arcane.iris.spi.PlatformBlockState;
import lombok.experimental.Accessors;
import java.util.LinkedHashMap;
import java.util.Map;
@Snippet("object-block-replacer")
@Accessors(chain = true)
@NoArgsConstructor
@@ -65,7 +69,7 @@ public class IrisObjectReplace {
KList<PlatformBlockState> b = new KList<>();
for (IrisBlockData i : find) {
PlatformBlockState bx = i.getBlockData(rdata);
PlatformBlockState bx = i.getBlockDataOrPlaceholder(rdata);
if (bx != null) {
b.add(bx);
@@ -79,4 +83,89 @@ public class IrisObjectReplace {
public PlatformBlockState getReplace(RNG seed, double x, double y, double z, IrisData rdata) {
return getReplace().get(seed, x, y, z, rdata);
}
/**
* Key-level mirror of the runtime find match, for the version-content gate: it has to decide whether a palette
* key the server does not have is rewritten by this rule, and a missing key has no resolved state to compare.
* Non-exact compares the base block; exact requires every property named by the find entry to match, the same
* partial-match contract {@code PlatformBlockState.matches} uses at runtime.
*/
public boolean matchesState(String stateKey) {
String state = ContentGate.normalizeState(stateKey);
if (state == null) {
return false;
}
String stateBase = ContentGate.baseKey(state);
for (IrisBlockData f : find) {
if (f == null) {
continue;
}
String findState = ContentGate.normalizeState(declaredState(f));
if (findState == null || !ContentGate.baseKey(findState).equals(stateBase)) {
continue;
}
if (!exact || propertiesMatch(findState, state)) {
return true;
}
}
return false;
}
/** The find entry as one state string; {@code block} already carrying properties wins over the data map. */
private static String declaredState(IrisBlockData block) {
String declared = block.getBlock();
if (declared == null) {
return null;
}
return declared.indexOf('[') < 0 ? declared + block.computeProperties() : declared;
}
/** Every property the find side names must be present with the same value on the candidate state. */
private static boolean propertiesMatch(String findState, String candidateState) {
Map<String, String> required = properties(findState);
if (required.isEmpty()) {
return true;
}
Map<String, String> candidate = properties(candidateState);
for (Map.Entry<String, String> entry : required.entrySet()) {
if (!entry.getValue().equals(candidate.get(entry.getKey()))) {
return false;
}
}
return true;
}
private static Map<String, String> properties(String state) {
int open = state.indexOf('[');
int close = state.lastIndexOf(']');
if (open < 0 || close < open) {
return Map.of();
}
Map<String, String> properties = new LinkedHashMap<>();
for (String part : state.substring(open + 1, close).split(",")) {
int split = part.indexOf('=');
if (split > 0) {
properties.put(part.substring(0, split).trim(), part.substring(split + 1).trim());
}
}
return properties;
}
}
@@ -19,6 +19,9 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
@@ -44,6 +47,8 @@ import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@@ -321,6 +326,11 @@ public class IrisRegion extends IrisRegistrant implements IRare {
continue;
}
if (biome.isCompatExcluded()) {
CompatPools.drop(g.getData(), biome, "region", getLoadKey(), "biomes " + i, null);
continue;
}
names.add(biome.getCarvingBiome());
b.put(biome.getLoadKey(), biome);
names.addAll(biome.getChildren());
@@ -345,55 +355,26 @@ public class IrisRegion extends IrisRegistrant implements IRare {
}
public KList<IrisBiome> getRealCaveBiomes(DataProvider g) {
return realCaveBiomes.aquire(() ->
{
KList<IrisBiome> realCaveBiomes = new KList<>();
for (String i : getCaveBiomes()) {
realCaveBiomes.add(g.getData().getBiomeLoader().load(i));
}
return realCaveBiomes;
});
return realCaveBiomes.aquire(() -> resolvePool(g, getCaveBiomes(), "caveBiomes", null));
}
public KList<IrisBiome> getRealShoreBiomes(DataProvider g) {
return realShoreBiomes.aquire(() ->
{
KList<IrisBiome> realShoreBiomes = new KList<>();
for (String i : getShoreBiomes()) {
realShoreBiomes.add(g.getData().getBiomeLoader().load(i));
}
return realShoreBiomes;
});
return realShoreBiomes.aquire(() -> resolvePool(g, getShoreBiomes(), "shoreBiomes", null));
}
public KList<IrisBiome> getRealSeaBiomes(DataProvider g) {
return realSeaBiomes.aquire(() ->
{
KList<IrisBiome> realSeaBiomes = new KList<>();
for (String i : getSeaBiomes()) {
realSeaBiomes.add(g.getData().getBiomeLoader().load(i));
}
return realSeaBiomes;
});
return realSeaBiomes.aquire(() -> resolvePool(g, getSeaBiomes(), "seaBiomes", null));
}
public KList<IrisBiome> getRealLandBiomes(DataProvider g) {
return realLandBiomes.aquire(() ->
{
KList<IrisBiome> realLandBiomes = new KList<>();
return realLandBiomes.aquire(() -> resolvePool(g, getLandBiomes(), "landBiomes", null));
}
for (String i : getLandBiomes()) {
realLandBiomes.add(g.getData().getBiomeLoader().load(i));
}
return realLandBiomes;
});
/** A biome reference pool with unloadable and gate-excluded biomes removed; drops are reported once. */
private KList<IrisBiome> resolvePool(DataProvider g, KList<String> keys, String field, List<CompatFinding> sink) {
IrisData data = g == null ? null : g.getData();
return CompatPools.load(data == null ? null : data.getBiomeLoader(), keys, data,
"region", getLoadKey(), field, sink);
}
public KList<IrisBiome> getAllAnyBiomes() {
@@ -468,6 +449,42 @@ public class IrisRegion extends IrisRegistrant implements IRare {
}
/**
* Cascade: a region that lost every land biome to the gate cannot generate, so it leaves the dimension's region
* pool too. Loading biomes here is safe - biomes never load regions.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded()) {
return base;
}
IrisData data = getLoader();
if (data == null) {
return base;
}
for (int index = 0; index < structures.size(); index++) {
structures.get(index).reportExcludedCaveBiomes(data, "region", getLoadKey(),
"structures[" + index + "].caveBiomes");
}
if (getLandBiomes().isEmpty()) {
return base;
}
List<CompatFinding> drops = new ArrayList<>();
if (!resolvePool(() -> data, getLandBiomes(), "landBiomes", drops).isEmpty()) {
return base;
}
return CompatPools.cascade(data, base, drops, "region", getLoadKey(), "no land biomes remain");
}
@Override
public String getFolderName() {
return "regions";
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
import art.arcane.iris.engine.object.annotations.MinNumber;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.annotations.RegistryListResource;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
@@ -83,6 +84,19 @@ public class IrisRiverPolicy {
@Desc("Multiplier applied to the eroded bank width outside the shore band.")
private Double bankMultiplier = null;
/**
* A declared river biome selection with biomes the version-content gate excluded removed. A declared-but-now-empty
* selection keeps its "explicitly clears the inherited selection" meaning; null (undeclared) stays null.
*/
public KList<String> compatBiomes(KList<String> declared, IrisData data, String field) {
if (declared == null) {
return null;
}
return CompatPools.surviving(data == null ? null : data.getBiomeLoader(), declared, data,
"river policy", "", field);
}
public KSet<String> getAllBiomeIds() {
KSet<String> biomeIds = new KSet<>();
addAll(biomeIds, surfaceBiomes);
@@ -18,6 +18,10 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.ArrayType;
@@ -30,6 +34,9 @@ import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.ArrayList;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@NoArgsConstructor
@@ -123,6 +130,64 @@ public class IrisSpawner extends IrisRegistrant {
engine.getEngineData().getChunk(x, z).getCooldown(this).spawn(engine);
}
/**
* Cascade: a spawn whose entity the gate excluded can never spawn, so it leaves this spawner's lists; a spawner
* with nothing left to spawn leaves every pool that references it. Loading entities here is safe - entities never
* load spawners.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded()) {
return base;
}
IrisData data = getLoader();
if (data == null || data.getEntityLoader() == null) {
return base;
}
boolean declared = !spawns.isEmpty() || !initialSpawns.isEmpty();
List<CompatFinding> drops = new ArrayList<>();
spawns = dropExcludedSpawns(data, spawns, "spawns", drops);
initialSpawns = dropExcludedSpawns(data, initialSpawns, "initialSpawns", drops);
if (!declared || !spawns.isEmpty() || !initialSpawns.isEmpty()) {
return base;
}
return CompatPools.cascade(data, base, drops, "spawner", getLoadKey(), "no entity spawns remain");
}
private KList<IrisEntitySpawn> dropExcludedSpawns(IrisData data,
KList<IrisEntitySpawn> declared,
String field,
List<CompatFinding> drops) {
KList<IrisEntitySpawn> kept = new KList<>();
for (int index = 0; index < declared.size(); index++) {
IrisEntitySpawn spawn = declared.get(index);
if (spawn == null) {
continue;
}
IrisEntity entity = data.getEntityLoader().load(spawn.getEntity());
if (entity != null && entity.isCompatExcluded()) {
CompatPools.drop(data, entity, "spawner", getLoadKey(),
field + "[" + index + "] " + spawn.getEntity(), drops);
continue;
}
kept.add(spawn);
}
return kept;
}
@Override
public String getFolderName() {
return "spawners";
@@ -18,6 +18,12 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.compat.CompatAction;
import art.arcane.iris.core.compat.CompatFinding;
import art.arcane.iris.core.compat.CompatRegistry;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
@@ -126,6 +132,42 @@ public class IrisStructure extends IrisRegistrant {
return placement;
}
/**
* The start pool is the only mandatory entry point into an assembly: when it has nothing left to place, the
* structure cannot generate on this server and every placement that lists it skips it.
*/
@Override
public CompatStatus evaluateCompat(ContentGate gate) {
CompatStatus base = super.evaluateCompat(gate);
if (base.excluded() || gate == null || !gate.ready()) {
return base;
}
IrisData data = getLoader();
if (data == null || startPool == null || startPool.isBlank()) {
return base;
}
IrisJigsawPool pool = data.load(IrisJigsawPool.class, startPool.trim(), false);
if (pool == null || !pool.isCompatExcluded()) {
return base;
}
CompatFinding reason = pool.getCompat().reasons().isEmpty() ? null : pool.getCompat().reasons().getLast();
CompatFinding excluded = new CompatFinding(
reason == null ? CompatRegistry.BLOCK : reason.registry(),
reason == null ? startPool.trim() : reason.key(),
CompatAction.EXCLUDED, "structure", getLoadKey(),
"start pool " + startPool.trim() + " is unavailable");
gate.report().record(excluded);
KList<CompatFinding> reasons = new KList<>(base.reasons());
reasons.add(excluded);
return CompatStatus.excludedBy(reasons);
}
@Override
public String getFolderName() {
return "structures";
@@ -18,6 +18,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.iris.engine.object.annotations.MaxNumber;
@@ -147,4 +148,24 @@ public class IrisStructurePlacement {
IrisStructureAnchorMode activeAnchor = resolvedAnchor();
return activeAnchor == IrisStructureAnchorMode.HEIGHT_BAND || activeAnchor.isCave();
}
/**
* Reports cave biome references the version-content gate excluded. The list is deliberately not filtered: it is an
* allowlist whose emptiness means "any cave biome", and it feeds the placement determinism signature. An excluded
* biome never generates, so the entry is inert - this only makes it visible in the pack compat report.
*/
void reportExcludedCaveBiomes(IrisData data, String subjectType, String subjectKey, String field) {
if (data == null || data.getBiomeLoader() == null || caveBiomes == null) {
return;
}
for (int index = 0; index < caveBiomes.size(); index++) {
String key = caveBiomes.get(index);
IrisBiome biome = data.getBiomeLoader().load(key);
if (biome != null && biome.isCompatExcluded()) {
CompatPools.drop(data, biome, subjectType, subjectKey, field + "[" + index + "] " + key, null);
}
}
}
}