mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
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:
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()
|
||||
|
||||
+36
-10
@@ -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());
|
||||
|
||||
+12
@@ -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();
|
||||
|
||||
|
||||
+8
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user