mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.framework;
|
||||
|
||||
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.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.structure.StructureAssemblyResult;
|
||||
import art.arcane.iris.engine.framework.structure.StructureAssemblyStatus;
|
||||
import art.arcane.iris.engine.object.IrisDirection;
|
||||
import art.arcane.iris.engine.object.IrisJigsawConnector;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPieceEntry;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPool;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.JigsawJoint;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class StructureAssemblerCompatTest {
|
||||
@BeforeClass
|
||||
public static void bindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
PlatformBlockState block = mock(PlatformBlockState.class);
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.block(anyString())).thenReturn(block);
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.registries()).thenReturn(registries);
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void unbindPlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anExcludedPieceIsNeverDrawnFromItsPool() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisJigsawPiece good = piece("good-object");
|
||||
IrisJigsawPiece bad = excluded(piece("bad-object"));
|
||||
when(data.load(IrisJigsawPool.class, "start", false)).thenReturn(pool("good-piece", "bad-piece"));
|
||||
when(data.load(IrisJigsawPool.class, "terminal", false)).thenReturn(new IrisJigsawPool());
|
||||
when(data.load(IrisJigsawPiece.class, "good-piece", false)).thenReturn(good);
|
||||
when(data.load(IrisJigsawPiece.class, "bad-piece", false)).thenReturn(bad);
|
||||
when(data.load(IrisObject.class, "good-object", false)).thenReturn(new IrisObject(1, 1, 1));
|
||||
when(data.load(IrisObject.class, "bad-object", false)).thenReturn(new IrisObject(1, 1, 1));
|
||||
|
||||
for (int seed = 0; seed < 32; seed++) {
|
||||
StructureAssemblyResult result = StructureAssembler
|
||||
.forData(data, structure("start"), new IrisPosition(0, 0, 0))
|
||||
.assemble(new RNG(seed));
|
||||
|
||||
assertTrue(result.detail(), result.hasOutput());
|
||||
assertTrue(result.pieces().toString(), result.pieces().stream()
|
||||
.allMatch(placed -> "good-object".equals(placed.getPiece().getObject())));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPoolWithOnlyExcludedPiecesStopsTheAssemblyWithoutThrowing() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
IrisJigsawPool startPool = pool("bad-piece");
|
||||
startPool.setCompat(CompatStatus.excludedBy(List.of(finding(CompatAction.EXCLUDED))));
|
||||
when(data.load(IrisJigsawPool.class, "start", false)).thenReturn(startPool);
|
||||
when(data.load(IrisJigsawPool.class, "terminal", false)).thenReturn(new IrisJigsawPool());
|
||||
when(data.load(IrisJigsawPiece.class, "bad-piece", false)).thenReturn(excluded(piece("bad-object")));
|
||||
when(data.load(IrisObject.class, "bad-object", false)).thenReturn(new IrisObject(1, 1, 1));
|
||||
|
||||
StructureAssemblyResult result = StructureAssembler
|
||||
.forData(data, structure("start"), new IrisPosition(0, 0, 0))
|
||||
.assemble(new RNG(3));
|
||||
|
||||
assertFalse(result.hasOutput());
|
||||
assertEquals(StructureAssemblyStatus.FAILED_RULES, result.status());
|
||||
}
|
||||
|
||||
private static IrisJigsawPiece excluded(IrisJigsawPiece piece) {
|
||||
piece.setCompat(CompatStatus.excludedBy(List.of(finding(CompatAction.EXCLUDED))));
|
||||
return piece;
|
||||
}
|
||||
|
||||
private static CompatFinding finding(CompatAction action) {
|
||||
return new CompatFinding(CompatRegistry.BLOCK, "minecraft:sulfur", action, "jigsaw piece", "bad-piece", "object bad-object");
|
||||
}
|
||||
|
||||
private static IrisJigsawPiece piece(String object) {
|
||||
IrisJigsawPiece piece = new IrisJigsawPiece().setObject(object).setRotatable(false);
|
||||
piece.getConnectors().add(new IrisJigsawConnector()
|
||||
.setPosition(new IrisPosition())
|
||||
.setDirection(IrisDirection.EAST_POSITIVE_X)
|
||||
.setPool("terminal")
|
||||
.setName("start")
|
||||
.setTargetName("door")
|
||||
.setJoint(JigsawJoint.ROLLABLE));
|
||||
return piece;
|
||||
}
|
||||
|
||||
private static IrisJigsawPool pool(String... pieces) {
|
||||
IrisJigsawPool pool = new IrisJigsawPool();
|
||||
for (String piece : pieces) {
|
||||
pool.getPieces().add(new IrisJigsawPieceEntry().setPiece(piece).setWeight(1));
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
private static IrisStructure structure(String startPool) {
|
||||
return new IrisStructure().setStartPool(startPool).setMaxDepth(1).setMaxSizeChunks(8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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.PackCompatReport;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Shared fixtures for the version-content pool cascade tests. The gate walker (lane L1) is not exercised here: the
|
||||
* tests stamp {@code compat} on a registrant directly, which is exactly what the walker stores.
|
||||
*/
|
||||
final class CompatFixtures {
|
||||
static final String MISSING_BLOCK = "minecraft:sulfur";
|
||||
static final String MISSING_ENTITY = "minecraft:camel";
|
||||
|
||||
private CompatFixtures() {
|
||||
}
|
||||
|
||||
static IrisData data(PackCompatReport report) {
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getCompatReport()).thenReturn(report);
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Marks a registrant excluded the way the gate walker would for a missing composed block. */
|
||||
static <T extends IrisRegistrant> T excludeBlock(T registrant) {
|
||||
return exclude(registrant, CompatRegistry.BLOCK, MISSING_BLOCK);
|
||||
}
|
||||
|
||||
/** Marks a registrant excluded the way the gate walker would for a missing entity type. */
|
||||
static <T extends IrisRegistrant> T excludeEntity(T registrant) {
|
||||
return exclude(registrant, CompatRegistry.ENTITY, MISSING_ENTITY);
|
||||
}
|
||||
|
||||
static <T extends IrisRegistrant> T exclude(T registrant, CompatRegistry registry, String key) {
|
||||
registrant.setCompat(CompatStatus.excludedBy(List.of(new CompatFinding(registry, key,
|
||||
CompatAction.EXCLUDED, registrant.getTypeName().toLowerCase(java.util.Locale.ROOT),
|
||||
registrant.getLoadKey(), "composition"))));
|
||||
return registrant;
|
||||
}
|
||||
|
||||
static IrisBiome biome(String key) {
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setLoadKey(key);
|
||||
return biome;
|
||||
}
|
||||
|
||||
static IrisRegion region(String key) {
|
||||
IrisRegion region = new IrisRegion();
|
||||
region.setLoadKey(key);
|
||||
return region;
|
||||
}
|
||||
|
||||
static IrisEntity entity(String key) {
|
||||
IrisEntity entity = new IrisEntity();
|
||||
entity.setLoadKey(key);
|
||||
return entity;
|
||||
}
|
||||
|
||||
static IrisLootTable lootTable(String key) {
|
||||
IrisLootTable table = new IrisLootTable();
|
||||
table.setLoadKey(key);
|
||||
return table;
|
||||
}
|
||||
|
||||
static CompatFinding find(PackCompatReport report, CompatAction action, String subjectType, String subjectKey) {
|
||||
for (CompatFinding finding : report.findings()) {
|
||||
if (finding.action() == action
|
||||
&& finding.subjectType().equals(subjectType)
|
||||
&& finding.subjectKey().equals(subjectKey)) {
|
||||
return finding;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* 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.engine.framework.MeteredCache;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformEntityType;
|
||||
import art.arcane.iris.spi.PlatformItem;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import org.mockito.Answers;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Binds a platform whose block registry holds exactly the keys a compat test declares, so the gate can be exercised
|
||||
* without a server. Also writes the .iob and JSON fixtures those tests need.
|
||||
*/
|
||||
final class CompatTestPlatform {
|
||||
private final IrisPlatform previous;
|
||||
|
||||
private CompatTestPlatform(IrisPlatform previous) {
|
||||
this.previous = previous;
|
||||
}
|
||||
|
||||
static CompatTestPlatform bind(File dataFolder, List<String> blockKeys) {
|
||||
IrisPlatform previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
IrisPlatforms.unbind();
|
||||
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
|
||||
when(platform.dataFolder()).thenReturn(dataFolder);
|
||||
when(platform.registries()).thenReturn(new FixedRegistries(blockKeys));
|
||||
IrisPlatforms.bind(platform);
|
||||
IrisServices.register(PreservationRegistry.class, new NoOpPreservationRegistry());
|
||||
return new CompatTestPlatform(previous);
|
||||
}
|
||||
|
||||
void unbind() {
|
||||
IrisServices.clear();
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
|
||||
/** A minimal V2 .iob under {@code <pack>/objects/<key>.iob} carrying only the given palette. */
|
||||
static File writeObject(File pack, String key, String... palette) throws IOException {
|
||||
File file = new File(pack, "objects/" + key + ".iob");
|
||||
file.getParentFile().mkdirs();
|
||||
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
|
||||
out.writeInt(1);
|
||||
out.writeInt(1);
|
||||
out.writeInt(1);
|
||||
out.writeUTF("Iris V2 IOB;");
|
||||
out.writeShort(palette.length);
|
||||
for (String entry : palette) {
|
||||
out.writeUTF(entry);
|
||||
}
|
||||
out.writeInt(0);
|
||||
out.writeInt(0);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
static File write(File pack, String path, String content) throws IOException {
|
||||
File file = new File(pack, path);
|
||||
file.getParentFile().mkdirs();
|
||||
Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
|
||||
return file;
|
||||
}
|
||||
|
||||
private record FixedRegistries(List<String> blocks) implements PlatformRegistries {
|
||||
private static String base(String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = key.trim().toLowerCase(Locale.ROOT);
|
||||
int props = trimmed.indexOf('[');
|
||||
String head = props < 0 ? trimmed : trimmed.substring(0, props);
|
||||
return head.indexOf(':') < 0 ? "minecraft:" + head : head;
|
||||
}
|
||||
|
||||
private boolean has(String key) {
|
||||
String normalized = base(key);
|
||||
if (normalized == null) {
|
||||
return false;
|
||||
}
|
||||
for (String block : blocks) {
|
||||
if (base(block).equals(normalized)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState block(String key) {
|
||||
PlatformBlockState state = blockOrNull(key, false);
|
||||
return state == null ? air() : state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState blockOrNull(String key) {
|
||||
return blockOrNull(key, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState blockOrNull(String key, boolean warn) {
|
||||
return has(key) ? StateCache.of(base(key)) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState air() {
|
||||
return StateCache.of("minecraft:air");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) {
|
||||
return ore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiome biome(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformItem item(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformEntityType entity(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockKeys() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> biomeKeys() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> structureKeys() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> itemKeys() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> entityKeys() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockTypeKeys() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> enchantmentKeys() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> potionEffectKeys() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> lootTableKeys() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
/** One interned mock per key so {@code matches} and identity behave like a real adapter. */
|
||||
private static final class StateCache {
|
||||
private static final Map<String, PlatformBlockState> STATES = new LinkedHashMap<>();
|
||||
|
||||
private StateCache() {
|
||||
}
|
||||
|
||||
static synchronized PlatformBlockState of(String key) {
|
||||
return STATES.computeIfAbsent(key, k -> {
|
||||
PlatformBlockState state = mock(PlatformBlockState.class);
|
||||
when(state.key()).thenReturn(k);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NoOpPreservationRegistry implements PreservationRegistry {
|
||||
@Override
|
||||
public void register(Thread thread) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(ExecutorService service) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerCache(MeteredCache cache) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dereference() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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.PackCompatReport;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.MISSING_BLOCK;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.biome;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.excludeBlock;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.find;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisBiomeCompatChildrenTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData dataWith(PackCompatReport report, IrisBiome... biomes) {
|
||||
IrisData data = CompatFixtures.data(report);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
for (IrisBiome biome : biomes) {
|
||||
when(biomeLoader.load(biome.getLoadKey())).thenReturn(biome);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realChildrenDropsExcludedChildAndRecordsFinding() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome parent = biome("land/plains");
|
||||
IrisBiome hills = biome("land/hills");
|
||||
IrisBiome sulfur = excludeBlock(biome("land/sulfur-flats"));
|
||||
parent.setChildren(new KList<>("land/hills", "land/sulfur-flats"));
|
||||
IrisData data = dataWith(report, parent, hills, sulfur);
|
||||
parent.setLoader(data);
|
||||
|
||||
KList<IrisBiome> children = parent.getRealChildren(() -> data);
|
||||
|
||||
assertEquals(1, children.size());
|
||||
assertEquals("land/hills", children.get(0).getLoadKey());
|
||||
CompatFinding dropped = find(report, CompatAction.DROPPED, "biome", "land/plains");
|
||||
assertNotNull(dropped);
|
||||
assertEquals(MISSING_BLOCK, dropped.key());
|
||||
assertEquals("children[1] land/sulfur-flats", dropped.detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realChildrenSkipsUnloadableChild() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome parent = biome("land/plains");
|
||||
parent.setChildren(new KList<>("land/gone"));
|
||||
IrisData data = dataWith(report, parent);
|
||||
parent.setLoader(data);
|
||||
|
||||
assertTrue(parent.getRealChildren(() -> data).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floatingChildBiomeFallsBackToParentWhenTargetExcluded() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome parent = biome("land/plains");
|
||||
IrisBiome sulfur = excludeBlock(biome("sky/sulfur-isles"));
|
||||
IrisData data = dataWith(report, parent, sulfur);
|
||||
parent.setLoader(data);
|
||||
IrisFloatingChildBiomes floating = new IrisFloatingChildBiomes().setBiome("sky/sulfur-isles");
|
||||
|
||||
assertSame(parent, floating.getRealBiome(parent, data));
|
||||
CompatFinding dropped = find(report, CompatAction.DROPPED, "floating child biome", "land/plains");
|
||||
assertNotNull(dropped);
|
||||
assertEquals(MISSING_BLOCK, dropped.key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void carvingEntryResolvesNullWhenBiomeExcluded() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-grotto"));
|
||||
IrisData data = dataWith(report, sulfur);
|
||||
IrisDimensionCarvingEntry entry = new IrisDimensionCarvingEntry()
|
||||
.setId("deep-band")
|
||||
.setBiome("cave/sulfur-grotto");
|
||||
|
||||
assertNull(entry.getRealBiome(data));
|
||||
CompatFinding dropped = find(report, CompatAction.DROPPED, "carving entry", "deep-band");
|
||||
assertNotNull(dropped);
|
||||
assertEquals(MISSING_BLOCK, dropped.key());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import art.arcane.iris.core.compat.ContentGate;
|
||||
import art.arcane.iris.core.compat.PackCompatReport;
|
||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisBiomeCustomCompatTest {
|
||||
private static final IDataFixer PASSTHROUGH = new IDataFixer() {
|
||||
@Override
|
||||
public JSONObject resolve(IDataFixer.Dimension dimension, art.arcane.iris.engine.object.IrisDimensionTypeOptions options) {
|
||||
return new JSONObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fixDimension(IDataFixer.Dimension dimension, JSONObject json) {
|
||||
}
|
||||
};
|
||||
|
||||
private static ContentGate gateWithEntities(String... present) {
|
||||
List<String> entities = new ArrayList<>();
|
||||
entities.add("minecraft:pig");
|
||||
entities.addAll(List.of(present));
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.blockKeys()).thenReturn(List.of("minecraft:stone"));
|
||||
when(registries.entityKeys()).thenReturn(entities);
|
||||
return new ContentGate(registries, Map.of(), new PackCompatReport());
|
||||
}
|
||||
|
||||
private static IrisBiomeCustom customBiome() {
|
||||
IrisBiomeCustom biome = new IrisBiomeCustom();
|
||||
biome.setId("sulfur_grotto");
|
||||
biome.setSpawnRarity(1);
|
||||
biome.setSpawns(new KList<>(
|
||||
new IrisBiomeCustomSpawn().setType("minecraft:zombie"),
|
||||
new IrisBiomeCustomSpawn().setType("minecraft:camel")));
|
||||
return biome;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateJsonOmitsSpawnWhoseEntityTypeIsMissing() {
|
||||
JSONObject json = new JSONObject(customBiome().generateJson(PASSTHROUGH, gateWithEntities("minecraft:zombie")));
|
||||
JSONArray monsters = json.getJSONObject("spawners").getJSONArray("misc");
|
||||
|
||||
assertEquals(1, monsters.length());
|
||||
assertEquals("minecraft:zombie", monsters.getJSONObject(0).getString("type"));
|
||||
assertFalse(json.getJSONObject("spawners").toString().contains("minecraft:camel"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateJsonKeepsEverySpawnWhenAllEntitiesArePresent() {
|
||||
JSONObject json = new JSONObject(customBiome()
|
||||
.generateJson(PASSTHROUGH, gateWithEntities("minecraft:zombie", "minecraft:camel")));
|
||||
|
||||
assertEquals(2, json.getJSONObject("spawners").getJSONArray("misc").length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateJsonKeepsEverySpawnWhenRegistryCannotBeConsulted() {
|
||||
// Registry not ready (modded early boot): UNKNOWN must never behave like MISSING.
|
||||
PlatformRegistries registries = mock(PlatformRegistries.class);
|
||||
when(registries.blockKeys()).thenReturn(List.of());
|
||||
when(registries.entityKeys()).thenReturn(List.of());
|
||||
ContentGate gate = new ContentGate(registries, Map.of(), new PackCompatReport());
|
||||
|
||||
JSONObject json = new JSONObject(customBiome().generateJson(PASSTHROUGH, gate));
|
||||
|
||||
assertEquals(2, json.getJSONObject("spawners").getJSONArray("misc").length());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generateJsonWritesNoSpawnersWhenEveryEntityIsMissing() {
|
||||
JSONObject json = new JSONObject(customBiome().generateJson(PASSTHROUGH, gateWithEntities()));
|
||||
|
||||
assertEquals(0, json.getJSONObject("spawners").length());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris.engine.object;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* The gate walker drops a {@code vanillaDerivative} whose biome key is missing (policy table: entry DROPPED, falls back
|
||||
* to derivative). This pins the fallback the drop relies on; the drop itself is simulated by clearing the field, which
|
||||
* is exactly what the walker does.
|
||||
*/
|
||||
public class IrisBiomeDerivativeCompatTest {
|
||||
@Test
|
||||
public void droppedVanillaDerivativeFallsBackToDerivative() {
|
||||
IrisBiome biome = CompatFixtures.biome("cave/sulfur-grotto");
|
||||
biome.setDerivative("minecraft:dripstone_caves");
|
||||
biome.setVanillaDerivative("minecraft:sulfur_caves");
|
||||
|
||||
assertEquals("minecraft:sulfur_caves", biome.getVanillaDerivativeKey());
|
||||
|
||||
biome.setVanillaDerivative(null);
|
||||
|
||||
assertEquals("minecraft:dripstone_caves", biome.getVanillaDerivativeKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankVanillaDerivativeFallsBackToDerivative() {
|
||||
IrisBiome biome = CompatFixtures.biome("cave/sulfur-grotto");
|
||||
biome.setDerivative("minecraft:dripstone_caves");
|
||||
biome.setVanillaDerivative("");
|
||||
|
||||
assertEquals("minecraft:dripstone_caves", biome.getVanillaDerivativeKey());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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.compat.ContentGate;
|
||||
import art.arcane.iris.core.compat.PackCompatReport;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.MISSING_BLOCK;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.excludeBlock;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.find;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.region;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisDimensionCompatRegionTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData dataWith(PackCompatReport report, IrisRegion... regions) {
|
||||
IrisData data = CompatFixtures.data(report);
|
||||
ResourceLoader<IrisRegion> regionLoader = mock(ResourceLoader.class);
|
||||
when(data.getRegionLoader()).thenReturn(regionLoader);
|
||||
for (IrisRegion region : regions) {
|
||||
when(regionLoader.load(region.getLoadKey())).thenReturn(region);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private static ContentGate gate(PackCompatReport report) {
|
||||
return new ContentGate(null, Map.of(), report);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allRegionsDropsExcludedRegionAndRecordsFinding() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisRegion plains = region("overworld");
|
||||
IrisRegion sulfur = excludeBlock(region("sulfur-lands"));
|
||||
IrisData data = dataWith(report, plains, sulfur);
|
||||
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("overworld", "sulfur-lands"));
|
||||
dimension.setLoadKey("overworld");
|
||||
dimension.setLoader(data);
|
||||
|
||||
KList<IrisRegion> pool = dimension.getAllRegions(() -> data);
|
||||
|
||||
assertEquals(1, pool.size());
|
||||
assertEquals("overworld", pool.get(0).getLoadKey());
|
||||
CompatFinding dropped = find(report, CompatAction.DROPPED, "dimension", "overworld");
|
||||
assertNotNull(dropped);
|
||||
assertEquals(MISSING_BLOCK, dropped.key());
|
||||
assertEquals("regions[1] sulfur-lands", dropped.detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatExcludesDimensionWithNoRegionsLeft() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisRegion sulfur = excludeBlock(region("sulfur-lands"));
|
||||
IrisData data = dataWith(report, sulfur);
|
||||
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("sulfur-lands"));
|
||||
dimension.setLoadKey("sulfurworld");
|
||||
dimension.setLoader(data);
|
||||
|
||||
CompatStatus status = dimension.evaluateCompat(gate(report));
|
||||
|
||||
assertTrue(status.excluded());
|
||||
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "dimension", "sulfurworld");
|
||||
assertNotNull(cascade);
|
||||
assertEquals(MISSING_BLOCK, cascade.key());
|
||||
assertEquals("no regions remain", cascade.detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatKeepsDimensionWithSurvivingRegion() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisRegion plains = region("overworld");
|
||||
IrisRegion sulfur = excludeBlock(region("sulfur-lands"));
|
||||
IrisData data = dataWith(report, plains, sulfur);
|
||||
IrisDimension dimension = new IrisDimension().setRegions(new KList<>("overworld", "sulfur-lands"));
|
||||
dimension.setLoadKey("overworld");
|
||||
dimension.setLoader(data);
|
||||
|
||||
assertFalse(dimension.evaluateCompat(gate(report)).excluded());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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.IrisSettings;
|
||||
import art.arcane.iris.core.compat.CompatAction;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisJigsawCompatTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private CompatTestPlatform platform;
|
||||
private IrisSettings previousSettings;
|
||||
private IrisData data;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
platform = CompatTestPlatform.bind(temporaryFolder.getRoot(),
|
||||
List.of("minecraft:stone", "minecraft:cobblestone", "minecraft:air"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePlatform() {
|
||||
if (data != null) {
|
||||
data.close();
|
||||
data = null;
|
||||
}
|
||||
if (platform != null) {
|
||||
platform.unbind();
|
||||
platform = null;
|
||||
}
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPieceWhoseObjectNeedsAMissingBlockIsExcluded() throws Exception {
|
||||
File pack = pack("piece");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pieces/bad.json", "{\"object\":\"bad\"}");
|
||||
data = IrisData.get(pack);
|
||||
|
||||
IrisJigsawPiece piece = data.load(IrisJigsawPiece.class, "bad", false);
|
||||
|
||||
assertNotNull(piece);
|
||||
assertTrue(piece.isCompatExcluded());
|
||||
assertTrue(data.getCompatReport().findings().stream()
|
||||
.anyMatch(finding -> "minecraft:sulfur".equals(finding.key())
|
||||
&& finding.action() == CompatAction.EXCLUDED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPieceWhoseObjectResolvesIsKept() throws Exception {
|
||||
File pack = pack("piece-ok");
|
||||
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pieces/good.json", "{\"object\":\"good\"}");
|
||||
data = IrisData.get(pack);
|
||||
|
||||
IrisJigsawPiece piece = data.load(IrisJigsawPiece.class, "good", false);
|
||||
|
||||
assertNotNull(piece);
|
||||
assertFalse(piece.isCompatExcluded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aPoolKeepsGeneratingWhileOnePieceSurvives() throws Exception {
|
||||
File pack = pack("pool-partial");
|
||||
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pieces/good.json", "{\"object\":\"good\"}");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pieces/bad.json", "{\"object\":\"bad\"}");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pools/pool.json",
|
||||
"{\"pieces\":[{\"piece\":\"good\",\"weight\":1},{\"piece\":\"bad\",\"weight\":1}]}");
|
||||
data = IrisData.get(pack);
|
||||
|
||||
IrisJigsawPool pool = data.load(IrisJigsawPool.class, "pool", false);
|
||||
|
||||
assertNotNull(pool);
|
||||
assertFalse(pool.isCompatExcluded());
|
||||
assertTrue(data.getCompatReport().findings().stream()
|
||||
.anyMatch(finding -> finding.action() == CompatAction.DROPPED
|
||||
&& "jigsaw pool".equals(finding.subjectType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pieceExclusionCascadesToThePoolAndTheStructure() throws Exception {
|
||||
File pack = pack("cascade");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pieces/bad.json", "{\"object\":\"bad\"}");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pools/pool.json",
|
||||
"{\"pieces\":[{\"piece\":\"bad\",\"weight\":1}]}");
|
||||
CompatTestPlatform.write(pack, "structures/keep.json", "{\"startPool\":\"pool\"}");
|
||||
data = IrisData.get(pack);
|
||||
|
||||
IrisJigsawPool pool = data.load(IrisJigsawPool.class, "pool", false);
|
||||
IrisStructure structure = data.load(IrisStructure.class, "keep", false);
|
||||
|
||||
assertNotNull(pool);
|
||||
assertNotNull(structure);
|
||||
assertTrue(pool.isCompatExcluded());
|
||||
assertTrue(structure.isCompatExcluded());
|
||||
assertTrue(data.getCompatReport().findings().stream()
|
||||
.anyMatch(finding -> "structure".equals(finding.subjectType())
|
||||
&& finding.action() == CompatAction.EXCLUDED
|
||||
&& "minecraft:sulfur".equals(finding.key())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aStructureOverAHealthyPoolIsKept() throws Exception {
|
||||
File pack = pack("structure-ok");
|
||||
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pieces/good.json", "{\"object\":\"good\"}");
|
||||
CompatTestPlatform.write(pack, "jigsaw-pools/pool.json",
|
||||
"{\"pieces\":[{\"piece\":\"good\",\"weight\":1}]}");
|
||||
CompatTestPlatform.write(pack, "structures/keep.json", "{\"startPool\":\"pool\"}");
|
||||
data = IrisData.get(pack);
|
||||
|
||||
IrisStructure structure = data.load(IrisStructure.class, "keep", false);
|
||||
|
||||
assertNotNull(structure);
|
||||
assertFalse(structure.isCompatExcluded());
|
||||
assertTrue(data.getCompatReport().isEmpty());
|
||||
}
|
||||
|
||||
private File pack(String name) throws Exception {
|
||||
return temporaryFolder.newFolder(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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.compat.PackCompatReport;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.find;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.lootTable;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisLootTableCompatTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData dataWith(PackCompatReport report, IrisLootTable... tables) {
|
||||
IrisData data = CompatFixtures.data(report);
|
||||
ResourceLoader<IrisLootTable> lootLoader = mock(ResourceLoader.class);
|
||||
when(data.getLootLoader()).thenReturn(lootLoader);
|
||||
for (IrisLootTable table : tables) {
|
||||
when(lootLoader.load(table.getLoadKey())).thenReturn(table);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private static ContentGate gate(PackCompatReport report) {
|
||||
return new ContentGate(null, Map.of(), report);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatExcludesTableWhoseEntriesWereAllDropped() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisLootTable table = lootTable("chest/sulfur-cache");
|
||||
table.setLoader(CompatFixtures.data(report));
|
||||
// The gate walker drops every entry with a missing item and hands the cascade its reasons.
|
||||
CompatStatus walked = new CompatStatus(false, List.of(new CompatFinding(CompatRegistry.ITEM,
|
||||
"minecraft:sulfur_dust", CompatAction.DROPPED, "loot", "chest/sulfur-cache", "loot[0]")));
|
||||
|
||||
CompatStatus status = table.cascadeEmptyLoot(walked);
|
||||
|
||||
assertTrue(status.excluded());
|
||||
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "loot", "chest/sulfur-cache");
|
||||
assertNotNull(cascade);
|
||||
assertEquals("minecraft:sulfur_dust", cascade.key());
|
||||
assertEquals(CompatRegistry.ITEM, cascade.registry());
|
||||
assertEquals("no loot entries remain", cascade.detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatKeepsTableWithSurvivingEntries() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisLootTable table = lootTable("chest/mixed");
|
||||
table.setLoader(CompatFixtures.data(report));
|
||||
table.setLoot(new KList<>(new IrisLoot()));
|
||||
CompatStatus walked = new CompatStatus(false, List.of(new CompatFinding(CompatRegistry.ITEM,
|
||||
"minecraft:sulfur_dust", CompatAction.DROPPED, "loot", "chest/mixed", "loot[1]")));
|
||||
|
||||
assertFalse(table.cascadeEmptyLoot(walked).excluded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatKeepsTableThatNeverDeclaredLoot() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisLootTable table = lootTable("chest/empty");
|
||||
table.setLoader(CompatFixtures.data(report));
|
||||
|
||||
assertFalse(table.evaluateCompat(gate(report)).excluded());
|
||||
assertTrue(report.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lootReferenceSkipsExcludedAndUnloadableTables() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisLootTable kept = lootTable("chest/common");
|
||||
IrisLootTable excluded = CompatFixtures.exclude(lootTable("chest/sulfur-cache"),
|
||||
CompatRegistry.ITEM, "minecraft:sulfur_dust");
|
||||
IrisData data = dataWith(report, kept, excluded);
|
||||
IrisLootReference reference = new IrisLootReference()
|
||||
.setTables(new KList<>("chest/common", "chest/sulfur-cache", "chest/gone"));
|
||||
|
||||
KList<IrisLootTable> tables = reference.getLootTables(() -> data);
|
||||
|
||||
assertEquals(1, tables.size());
|
||||
assertEquals("chest/common", tables.get(0).getLoadKey());
|
||||
CompatFinding dropped = find(report, CompatAction.DROPPED, "loot reference", "chest/sulfur-cache");
|
||||
assertNotNull(dropped);
|
||||
assertEquals("minecraft:sulfur_dust", dropped.key());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* 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.IrisSettings;
|
||||
import art.arcane.iris.core.compat.CompatAction;
|
||||
import art.arcane.iris.core.compat.CompatFinding;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisObjectPlacementCompatTest {
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private CompatTestPlatform platform;
|
||||
private IrisSettings previousSettings;
|
||||
private IrisData data;
|
||||
|
||||
@Before
|
||||
public void bindPlatform() {
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
platform = CompatTestPlatform.bind(temporaryFolder.getRoot(),
|
||||
List.of("minecraft:stone", "minecraft:cobblestone", "minecraft:air", "minecraft:oak_log"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePlatform() {
|
||||
if (data != null) {
|
||||
data.close();
|
||||
data = null;
|
||||
}
|
||||
if (platform != null) {
|
||||
platform.unbind();
|
||||
platform = null;
|
||||
}
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingPaletteKeyDropsTheObjectAndRecordsAFinding() throws Exception {
|
||||
File pack = pack("drop");
|
||||
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:stone", "minecraft:sulfur");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("good", "bad");
|
||||
|
||||
assertFalse(placement.isCompatExcluded(data));
|
||||
assertEquals(new KList<String>().qadd("good"), placement.compatPlace(data));
|
||||
|
||||
List<CompatFinding> findings = data.getCompatReport().findings();
|
||||
assertEquals(findings.toString(), 1, findings.size());
|
||||
assertEquals("minecraft:sulfur", findings.getFirst().key());
|
||||
assertEquals(CompatAction.DROPPED, findings.getFirst().action());
|
||||
assertEquals("object", findings.getFirst().subjectType());
|
||||
assertEquals("bad", findings.getFirst().subjectKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void editRuleWithFullChanceKeepsTheObject() throws Exception {
|
||||
File pack = pack("edit-keeps");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("bad");
|
||||
placement.getEdit().add(replace(1F, false, "minecraft:sulfur", "minecraft:stone"));
|
||||
|
||||
assertFalse(placement.isCompatExcluded(data));
|
||||
assertEquals(new KList<String>().qadd("bad"), placement.compatPlace(data));
|
||||
assertTrue(data.getCompatReport().findings().toString(), data.getCompatReport().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void editRuleBelowFullChanceDoesNotCoverTheMissingKey() throws Exception {
|
||||
File pack = pack("edit-chance");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("bad");
|
||||
placement.getEdit().add(replace(0.5F, false, "minecraft:sulfur", "minecraft:stone"));
|
||||
|
||||
assertTrue(placement.isCompatExcluded(data));
|
||||
assertTrue(placement.compatPlace(data).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exactEditRuleOnlyCoversTheMatchingState() throws Exception {
|
||||
File pack = pack("edit-exact");
|
||||
CompatTestPlatform.writeObject(pack, "axisY", "minecraft:sulfur[axis=y]");
|
||||
CompatTestPlatform.writeObject(pack, "axisX", "minecraft:sulfur[axis=x]");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("axisY", "axisX");
|
||||
IrisObjectReplace rule = replace(1F, true, "minecraft:sulfur", "minecraft:stone");
|
||||
rule.getFind().getFirst().getData().put("axis", "y");
|
||||
placement.getEdit().add(rule);
|
||||
|
||||
assertEquals(new KList<String>().qadd("axisY"), placement.compatPlace(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonExactEditRuleCoversEveryStateOfTheSameBlock() throws Exception {
|
||||
File pack = pack("edit-non-exact");
|
||||
CompatTestPlatform.writeObject(pack, "axisX", "minecraft:sulfur[axis=x]");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("axisX");
|
||||
placement.getEdit().add(replace(1F, false, "minecraft:sulfur", "minecraft:stone"));
|
||||
|
||||
assertFalse(placement.isCompatExcluded(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingReplacePaletteOptionExcludesTheWholePlacement() throws Exception {
|
||||
File pack = pack("replace-missing");
|
||||
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("good");
|
||||
placement.getEdit().add(replace(1F, false, "minecraft:stone", "minecraft:sulfur"));
|
||||
|
||||
assertTrue(placement.isCompatExcluded(data));
|
||||
assertTrue(placement.compatPlace(data).isEmpty());
|
||||
|
||||
List<CompatFinding> findings = data.getCompatReport().findings();
|
||||
assertEquals(findings.toString(), 1, findings.size());
|
||||
assertEquals(CompatAction.EXCLUDED, findings.getFirst().action());
|
||||
assertEquals("placement", findings.getFirst().subjectType());
|
||||
assertTrue(findings.getFirst().detail(), findings.getFirst().detail().contains("edit[0].replace.palette[0]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingFindKeyOnTheMatchSideIsIgnored() throws Exception {
|
||||
File pack = pack("find-missing");
|
||||
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("good");
|
||||
placement.getEdit().add(replace(1F, false, "minecraft:sulfur", "minecraft:cobblestone"));
|
||||
|
||||
assertFalse(placement.isCompatExcluded(data));
|
||||
assertTrue(data.getCompatReport().findings().toString(), data.getCompatReport().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingMarkerBlockIsMatchOnly() throws Exception {
|
||||
File pack = pack("marker-missing");
|
||||
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("good");
|
||||
IrisObjectMarker marker = new IrisObjectMarker();
|
||||
marker.setMarker("thing");
|
||||
marker.getMark().add(new IrisBlockData("minecraft:sulfur"));
|
||||
placement.getMarkers().add(marker);
|
||||
|
||||
assertFalse(placement.isCompatExcluded(data));
|
||||
assertEquals(new KList<String>().qadd("good"), placement.compatPlace(data));
|
||||
assertTrue(data.getCompatReport().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dimensionFallbackRescuesTheObjectAndRecordsASubstitution() throws Exception {
|
||||
File pack = pack("fallback");
|
||||
CompatTestPlatform.write(pack, "dimensions/main.json",
|
||||
"{\"blockFallbacks\":{\"minecraft:sulfur\":\"minecraft:cobblestone\"}}");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("bad");
|
||||
|
||||
assertFalse(placement.isCompatExcluded(data));
|
||||
assertEquals(new KList<String>().qadd("bad"), placement.compatPlace(data));
|
||||
|
||||
List<CompatFinding> findings = data.getCompatReport().findings();
|
||||
assertEquals(findings.toString(), 1, findings.size());
|
||||
assertEquals(CompatAction.SUBSTITUTED, findings.getFirst().action());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockBackupRescuesAReplacePaletteEntry() throws Exception {
|
||||
File pack = pack("backup");
|
||||
CompatTestPlatform.writeObject(pack, "good", "minecraft:stone");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("good");
|
||||
IrisObjectReplace rule = replace(1F, false, "minecraft:stone", "minecraft:sulfur");
|
||||
rule.getReplace().getPalette().getFirst().setBackup(new IrisBlockData("minecraft:cobblestone"));
|
||||
placement.getEdit().add(rule);
|
||||
|
||||
assertFalse(placement.isCompatExcluded(data));
|
||||
assertEquals(CompatAction.SUBSTITUTED, data.getCompatReport().findings().getFirst().action());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyObjectDroppedExcludesThePlacement() throws Exception {
|
||||
File pack = pack("all-dropped");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("bad");
|
||||
|
||||
assertTrue(placement.isCompatExcluded(data));
|
||||
assertTrue(placement.compatPlace(data).isEmpty());
|
||||
assertTrue(data.getCompatReport().findings().stream()
|
||||
.anyMatch(finding -> finding.action() == CompatAction.EXCLUDED
|
||||
&& "placement".equals(finding.subjectType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void survivingPoolKeepsAuthoredOrderAndGetObjectOnlyDrawsSurvivors() throws Exception {
|
||||
File pack = pack("draw");
|
||||
CompatTestPlatform.writeObject(pack, "a", "minecraft:stone");
|
||||
CompatTestPlatform.writeObject(pack, "bad", "minecraft:sulfur");
|
||||
CompatTestPlatform.writeObject(pack, "b", "minecraft:cobblestone");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("a", "bad", "b");
|
||||
|
||||
assertEquals(new KList<String>().qadd("a").qadd("b"), placement.compatPlace(data));
|
||||
|
||||
IrisData loaded = data;
|
||||
DataProvider provider = () -> loaded;
|
||||
for (int seed = 0; seed < 64; seed++) {
|
||||
IrisObject drawn = placement.getObject(provider, new RNG(seed));
|
||||
assertTrue("Draw must never return the dropped object",
|
||||
drawn != null && !"bad".equals(drawn.getLoadKey()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aCompletePackLeavesThePoolIdentical() throws Exception {
|
||||
File pack = pack("complete");
|
||||
CompatTestPlatform.writeObject(pack, "a", "minecraft:stone");
|
||||
CompatTestPlatform.writeObject(pack, "b", "minecraft:cobblestone");
|
||||
data = IrisData.get(pack);
|
||||
IrisObjectPlacement placement = placement("a", "b");
|
||||
|
||||
assertTrue("A complete pack must hand back the authored pool untouched",
|
||||
placement.getPlace() == placement.compatPlace(data));
|
||||
assertTrue(data.getCompatReport().isEmpty());
|
||||
}
|
||||
|
||||
private File pack(String name) throws Exception {
|
||||
return temporaryFolder.newFolder(name);
|
||||
}
|
||||
|
||||
private static IrisObjectPlacement placement(String... place) {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setPlace(new KList<>(place));
|
||||
return placement;
|
||||
}
|
||||
|
||||
private static IrisObjectReplace replace(float chance, boolean exact, String find, String to) {
|
||||
IrisObjectReplace rule = new IrisObjectReplace();
|
||||
rule.setChance(chance);
|
||||
rule.setExact(exact);
|
||||
rule.getFind().add(new IrisBlockData(find));
|
||||
rule.getReplace().qclear();
|
||||
rule.getReplace().add(to);
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
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.compat.PackCompatReport;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.MISSING_BLOCK;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.biome;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.excludeBlock;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.find;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisRegionCompatPoolTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData dataWith(PackCompatReport report, IrisBiome... biomes) {
|
||||
IrisData data = CompatFixtures.data(report);
|
||||
ResourceLoader<IrisBiome> biomeLoader = mock(ResourceLoader.class);
|
||||
when(data.getBiomeLoader()).thenReturn(biomeLoader);
|
||||
for (IrisBiome biome : biomes) {
|
||||
when(biomeLoader.load(biome.getLoadKey())).thenReturn(biome);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private static ContentGate gate(PackCompatReport report) {
|
||||
return new ContentGate(null, Map.of(), report);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realLandBiomesDropsExcludedReferenceAndRecordsFinding() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome plains = biome("land/plains");
|
||||
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-grotto"));
|
||||
IrisData data = dataWith(report, plains, sulfur);
|
||||
IrisRegion region = CompatFixtures.region("overworld");
|
||||
region.setLoader(data);
|
||||
region.setLandBiomes(new KList<>("land/plains", "cave/sulfur-grotto"));
|
||||
|
||||
KList<IrisBiome> pool = region.getRealLandBiomes(() -> data);
|
||||
|
||||
assertEquals(1, pool.size());
|
||||
assertEquals("land/plains", pool.get(0).getLoadKey());
|
||||
CompatFinding dropped = find(report, CompatAction.DROPPED, "region", "overworld");
|
||||
assertNotNull(dropped);
|
||||
assertEquals(CompatRegistry.BLOCK, dropped.registry());
|
||||
assertEquals(MISSING_BLOCK, dropped.key());
|
||||
assertEquals("landBiomes[1] cave/sulfur-grotto", dropped.detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realPoolsSkipUnloadableReferences() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome shore = biome("shore/sand");
|
||||
IrisData data = dataWith(report, shore);
|
||||
IrisRegion region = CompatFixtures.region("overworld");
|
||||
region.setLoader(data);
|
||||
region.setSeaBiomes(new KList<>("sea/gone"));
|
||||
region.setShoreBiomes(new KList<>("shore/sand"));
|
||||
region.setCaveBiomes(new KList<>("cave/gone"));
|
||||
|
||||
assertTrue(region.getRealSeaBiomes(() -> data).isEmpty());
|
||||
assertEquals(1, region.getRealShoreBiomes(() -> data).size());
|
||||
assertTrue(region.getRealCaveBiomes(() -> data).isEmpty());
|
||||
assertTrue(report.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void naturalBiomesSkipExcludedBiomesAndTheirChildren() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome plains = biome("land/plains");
|
||||
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-grotto"));
|
||||
sulfur.setChildren(new KList<>("cave/sulfur-deep"));
|
||||
IrisBiome sulfurDeep = biome("cave/sulfur-deep");
|
||||
IrisData data = dataWith(report, plains, sulfur, sulfurDeep);
|
||||
IrisRegion region = CompatFixtures.region("overworld");
|
||||
region.setLoader(data);
|
||||
region.setLandBiomes(new KList<>("land/plains"));
|
||||
region.setCaveBiomes(new KList<>("cave/sulfur-grotto"));
|
||||
|
||||
KList<IrisBiome> natural = region.getNaturalBiomes(() -> data);
|
||||
|
||||
assertEquals(1, natural.size());
|
||||
assertEquals("land/plains", natural.get(0).getLoadKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatExcludesRegionWithNoLandBiomesLeft() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-flats"));
|
||||
IrisData data = dataWith(report, sulfur);
|
||||
IrisRegion region = CompatFixtures.region("sulfur-lands");
|
||||
region.setLoader(data);
|
||||
region.setLandBiomes(new KList<>("cave/sulfur-flats"));
|
||||
|
||||
CompatStatus status = region.evaluateCompat(gate(report));
|
||||
|
||||
assertTrue(status.excluded());
|
||||
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "region", "sulfur-lands");
|
||||
assertNotNull(cascade);
|
||||
assertEquals(MISSING_BLOCK, cascade.key());
|
||||
assertEquals(CompatRegistry.BLOCK, cascade.registry());
|
||||
assertEquals("no land biomes remain", cascade.detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatKeepsRegionWithSurvivingLandBiome() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisBiome plains = biome("land/plains");
|
||||
IrisBiome sulfur = excludeBlock(biome("cave/sulfur-flats"));
|
||||
IrisData data = dataWith(report, plains, sulfur);
|
||||
IrisRegion region = CompatFixtures.region("overworld");
|
||||
region.setLoader(data);
|
||||
region.setLandBiomes(new KList<>("land/plains", "cave/sulfur-flats"));
|
||||
|
||||
assertFalse(region.evaluateCompat(gate(report)).excluded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatDoesNotExcludeRegionThatNeverDeclaredLandBiomes() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisData data = dataWith(report);
|
||||
IrisRegion region = CompatFixtures.region("empty");
|
||||
region.setLoader(data);
|
||||
|
||||
assertFalse(region.evaluateCompat(gate(report)).excluded());
|
||||
assertTrue(report.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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.compat.PackCompatReport;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.MISSING_ENTITY;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.entity;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.excludeEntity;
|
||||
import static art.arcane.iris.engine.object.CompatFixtures.find;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisSpawnerCompatTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
private static IrisData dataWith(PackCompatReport report, IrisEntity... entities) {
|
||||
IrisData data = CompatFixtures.data(report);
|
||||
ResourceLoader<IrisEntity> entityLoader = mock(ResourceLoader.class);
|
||||
when(data.getEntityLoader()).thenReturn(entityLoader);
|
||||
for (IrisEntity entity : entities) {
|
||||
when(entityLoader.load(entity.getLoadKey())).thenReturn(entity);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private static ContentGate gate(PackCompatReport report) {
|
||||
return new ContentGate(null, Map.of(), report);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatDropsSpawnsWhoseEntityIsExcluded() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisEntity zombie = entity("zombie");
|
||||
IrisEntity camel = excludeEntity(entity("camel"));
|
||||
IrisData data = dataWith(report, zombie, camel);
|
||||
IrisSpawner spawner = new IrisSpawner();
|
||||
spawner.setLoadKey("desert");
|
||||
spawner.setLoader(data);
|
||||
spawner.setSpawns(new KList<>(
|
||||
new IrisEntitySpawn().setEntity("zombie"),
|
||||
new IrisEntitySpawn().setEntity("camel")));
|
||||
|
||||
CompatStatus status = spawner.evaluateCompat(gate(report));
|
||||
|
||||
assertFalse(status.excluded());
|
||||
assertEquals(1, spawner.getSpawns().size());
|
||||
assertEquals("zombie", spawner.getSpawns().get(0).getEntity());
|
||||
CompatFinding dropped = find(report, CompatAction.DROPPED, "spawner", "desert");
|
||||
assertNotNull(dropped);
|
||||
assertEquals(CompatRegistry.ENTITY, dropped.registry());
|
||||
assertEquals(MISSING_ENTITY, dropped.key());
|
||||
assertEquals("spawns[1] camel", dropped.detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatExcludesSpawnerWithNoSpawnsLeft() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisEntity camel = excludeEntity(entity("camel"));
|
||||
IrisData data = dataWith(report, camel);
|
||||
IrisSpawner spawner = new IrisSpawner();
|
||||
spawner.setLoadKey("desert");
|
||||
spawner.setLoader(data);
|
||||
spawner.setSpawns(new KList<>(new IrisEntitySpawn().setEntity("camel")));
|
||||
spawner.setInitialSpawns(new KList<>(new IrisEntitySpawn().setEntity("camel")));
|
||||
|
||||
CompatStatus status = spawner.evaluateCompat(gate(report));
|
||||
|
||||
assertTrue(status.excluded());
|
||||
assertTrue(spawner.getSpawns().isEmpty());
|
||||
assertTrue(spawner.getInitialSpawns().isEmpty());
|
||||
CompatFinding cascade = find(report, CompatAction.EXCLUDED, "spawner", "desert");
|
||||
assertNotNull(cascade);
|
||||
assertEquals(MISSING_ENTITY, cascade.key());
|
||||
assertEquals("no entity spawns remain", cascade.detail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void evaluateCompatKeepsSpawnerThatNeverDeclaredSpawns() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisData data = dataWith(report);
|
||||
IrisSpawner spawner = new IrisSpawner();
|
||||
spawner.setLoadKey("empty");
|
||||
spawner.setLoader(data);
|
||||
|
||||
assertFalse(spawner.evaluateCompat(gate(report)).excluded());
|
||||
assertTrue(report.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entitySpawnResolvesNoEntityWhenExcluded() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisEntity camel = excludeEntity(entity("camel"));
|
||||
IrisData data = dataWith(report, camel);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getData()).thenReturn(data);
|
||||
|
||||
assertNull(new IrisEntitySpawn().setEntity("camel").getRealEntity(engine));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entitySpawnResolvesPresentEntity() {
|
||||
PackCompatReport report = new PackCompatReport();
|
||||
IrisEntity zombie = entity("zombie");
|
||||
IrisData data = dataWith(report, zombie);
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getData()).thenReturn(data);
|
||||
|
||||
assertSame(zombie, new IrisEntitySpawn().setEntity("zombie").getRealEntity(engine));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user