Scoped mod trees per biome

- importedFeatures gains biomeRules, which deny placed feature families or exact keys only in registry biomes matched by key, namespace, or tag; Iris custom biomes match through their vanilla derivative
- Mod ingest classifies tree-like passthrough features from the catalog feature JSON, including trees nested in random selectors, fallen trees, and huge mushrooms
- Tree-like mod features that a mod injects into vanilla biomes the pack already uses are denied in minecraft biomes, so they no longer double the pack's own trees while adopted mod biomes keep them
- Studio schema digest updated for the new rule shape
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-14 21:35:38 -04:00
1 parent 0bc235afb6
commit 7767d6d30a
10 files changed
+454 -6

No files matched your search

@@ -19,9 +19,11 @@
package art.arcane.iris.pack.ingest;
import art.arcane.iris.generation.decoration.IrisDecorationStep;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureBiomeRule;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationStatus;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -37,7 +39,8 @@ final class ImportedFeatureWriter {
private ImportedFeatureWriter() {
}
static IrisImportedFeatureControl write(JsonObject dimension, Set<String> nativeKeys, Set<String> unselectedNamespaces) {
static IrisImportedFeatureControl write(JsonObject dimension, Set<String> nativeKeys, Set<String> unselectedNamespaces,
Set<String> vanillaScopedKeys) {
JsonElement existing = dimension.get("importedFeatures");
JsonObject imported = existing != null && existing.isJsonObject() ? existing.getAsJsonObject() : new JsonObject();
Set<String> disabled = new LinkedHashSet<>(IngestJson.strings(imported.get("disabled")));
@@ -52,11 +55,23 @@ final class ImportedFeatureWriter {
imported.addProperty("enabled", true);
imported.add("disabled", IngestJson.stringArray(disabled));
imported.add("disabledExact", IngestJson.stringArray(disabledExact));
JsonElement existingRules = imported.get("biomeRules");
JsonArray rules = existingRules != null && existingRules.isJsonArray() ? existingRules.getAsJsonArray() : new JsonArray();
if (!vanillaScopedKeys.isEmpty()) {
JsonObject rule = new JsonObject();
rule.add("biomes", IngestJson.stringArray(List.of(VANILLA_PREFIX)));
rule.add("disabledExact", IngestJson.stringArray(vanillaScopedKeys));
rules.add(rule);
}
if (!rules.isEmpty()) {
imported.add("biomeRules", rules);
}
dimension.add("importedFeatures", imported);
IrisImportedFeatureControl control = new IrisImportedFeatureControl();
control.setEnabled(true);
control.setDisabled(new KList<>(disabled));
control.setDisabledExact(new KList<>(disabledExact));
control.setBiomeRules(biomeRules(rules));
control.setSteps(steps(imported.get("steps")));
control.setDisabledSteps(steps(imported.get("disabledSteps")));
return control;
@@ -106,6 +121,21 @@ final class ImportedFeatureWriter {
return "suppressed by importedFeatures";
}
private static KList<IrisImportedFeatureBiomeRule> biomeRules(JsonArray rules) {
KList<IrisImportedFeatureBiomeRule> parsed = new KList<>();
for (JsonElement entry : rules) {
if (!entry.isJsonObject()) {
continue;
}
JsonObject rule = entry.getAsJsonObject();
parsed.add(new IrisImportedFeatureBiomeRule()
.setBiomes(new KList<>(IngestJson.strings(rule.get("biomes"))))
.setDisabled(new KList<>(IngestJson.strings(rule.get("disabled"))))
.setDisabledExact(new KList<>(IngestJson.strings(rule.get("disabledExact")))));
}
return parsed;
}
private static boolean enabled(JsonObject imported) {
JsonElement value = imported.get("enabled");
return value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isBoolean() && value.getAsBoolean();
@@ -36,14 +36,19 @@ import java.util.TreeMap;
import java.util.TreeSet;
final class ModFeatureIngester {
private static final String VANILLA_NAMESPACE = "minecraft";
private final IngestPackModel model;
private final WorldgenCatalogIndex index;
private final TreeFeatures trees;
private final Map<String, FeaturePlan> plans = new TreeMap<>();
private final Set<String> nativeKeys = new TreeSet<>();
private final Set<String> vanillaScopedKeys = new TreeSet<>();
ModFeatureIngester(IngestPackModel model, WorldgenCatalogIndex index, Set<String> namespaces) {
this.model = model;
this.index = index;
this.trees = new TreeFeatures(index);
Map<String, Set<String>> declaring = new TreeMap<>();
Map<String, Set<Integer>> steps = new TreeMap<>();
for (CatalogBiome biome : index.catalog().biomes()) {
@@ -76,6 +81,10 @@ final class ModFeatureIngester {
return nativeKeys;
}
Set<String> vanillaScopedKeys() {
return vanillaScopedKeys;
}
Map<String, FeaturePlan> plans() {
return plans;
}
@@ -106,11 +115,30 @@ final class ModFeatureIngester {
nativeKeys.add(plan.key());
continue;
}
if (treeInPackVanillaBiomes(plan)) {
vanillaScopedKeys.add(plan.key());
items.add(new ModIngestItem(namespace, plan.key(), "feature", ModIngestTier.PASSTHROUGH, "importedFeatures",
plan.reason() + "; denied in minecraft biomes the pack already decorates, so its own trees are not doubled"));
continue;
}
items.add(new ModIngestItem(namespace, plan.key(), "feature", ModIngestTier.PASSTHROUGH, "importedFeatures", plan.reason()));
}
return items;
}
private boolean treeInPackVanillaBiomes(FeaturePlan plan) {
Optional<CatalogPlacedFeature> feature = index.placedFeature(plan.key());
if (feature.isEmpty() || !trees.isTree(feature.get())) {
return false;
}
for (String biome : plan.declaring()) {
if (WorldgenCatalogIndex.namespaceOf(biome).equals(VANILLA_NAMESPACE) && model.reachableBiomes().containsValue(biome)) {
return true;
}
}
return false;
}
private FeaturePlan plan(String key, Set<String> declaring, Set<Integer> steps) {
Optional<CatalogPlacedFeature> feature = index.placedFeature(key);
if (feature.isEmpty()) {
@@ -89,7 +89,7 @@ public final class ModIngester {
List<ModIngestItem> featureItems = features.apply(adopted);
JsonObject dimension = pack.edit(model.dimensionPath());
IrisImportedFeatureControl control = ImportedFeatureWriter.write(dimension, features.nativeKeys(),
unselectedFeatureNamespaces(request.catalog(), namespaces));
unselectedFeatureNamespaces(request.catalog(), namespaces), features.vanillaScopedKeys());
featureItems = ImportedFeatureWriter.applySuppression(featureItems, features.plans(), control);
Set<String> hostBiomes = new TreeSet<>(adopted.keySet());
hostBiomes.addAll(model.reachableBiomes().values());
@@ -0,0 +1,120 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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.pack.ingest;
import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class TreeFeatures {
private static final Set<String> TREE_TYPES = Set.of("minecraft:tree", "minecraft:fallen_tree", "minecraft:huge_brown_mushroom",
"minecraft:huge_red_mushroom", "minecraft:huge_fungus");
private static final List<String> NESTED_FIELDS = List.of("feature", "features", "default", "feature_true", "feature_false");
private static final String TAG_PREFIX = "#";
private static final int MAX_DEPTH = 8;
private final WorldgenCatalogIndex index;
private final Map<String, CatalogPlacedFeature> byConfigured;
TreeFeatures(WorldgenCatalogIndex index) {
this.index = index;
this.byConfigured = new HashMap<>();
for (CatalogPlacedFeature feature : index.catalog().placedFeatures()) {
if (feature.configuredFeature() != null) {
byConfigured.putIfAbsent(VanillaJson.namespaced(feature.configuredFeature()), feature);
}
}
}
boolean isTree(CatalogPlacedFeature feature) {
return configured(feature.featureType(), feature.config(), 0);
}
static boolean treeType(String type) {
String namespaced = VanillaJson.namespaced(type);
String path = namespaced.substring(namespaced.indexOf(':') + 1);
return TREE_TYPES.contains(namespaced) || path.endsWith("tree") || path.startsWith("huge_");
}
private boolean configured(String type, JsonElement config, int depth) {
if (type != null && treeType(type)) {
return true;
}
if (depth >= MAX_DEPTH || config == null || !config.isJsonObject()) {
return false;
}
JsonObject object = config.getAsJsonObject();
for (String field : NESTED_FIELDS) {
if (placed(object.get(field), depth + 1)) {
return true;
}
}
return false;
}
private boolean placed(JsonElement holder, int depth) {
if (holder == null || holder.isJsonNull() || depth > MAX_DEPTH) {
return false;
}
if (holder.isJsonArray()) {
for (JsonElement entry : holder.getAsJsonArray()) {
if (placed(entry, depth)) {
return true;
}
}
return false;
}
if (holder.isJsonPrimitive()) {
String key = holder.getAsString();
if (key.startsWith(TAG_PREFIX)) {
return false;
}
Optional<CatalogPlacedFeature> feature = index.placedFeature(VanillaJson.namespaced(key));
return feature.isPresent() && configured(feature.get().featureType(), feature.get().config(), depth);
}
JsonObject object = holder.getAsJsonObject();
if (object.has("chance")) {
return placed(object.get("feature"), depth);
}
return configuredHolder(object.get("feature"), depth);
}
private boolean configuredHolder(JsonElement holder, int depth) {
if (holder == null || holder.isJsonNull()) {
return false;
}
if (holder.isJsonPrimitive()) {
CatalogPlacedFeature feature = byConfigured.get(VanillaJson.namespaced(holder.getAsString()));
return feature != null && configured(feature.featureType(), feature.config(), depth);
}
if (!holder.isJsonObject()) {
return false;
}
JsonObject object = holder.getAsJsonObject();
JsonElement type = object.get("type");
return configured(type != null && type.isJsonPrimitive() ? type.getAsString() : null, object.get("config"), depth);
}
}
@@ -0,0 +1,91 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.structure.nativegen;
import art.arcane.iris.pack.schema.annotation.ArrayType;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.Description;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@Description("Denies native placed features in specific registry biomes only. Iris custom biomes match through the vanilla derivative that supplies their features, so a rule for 'minecraft:forest' also covers Iris biomes derived from it.")
@Data
public class IrisImportedFeatureBiomeRule {
private static final String TAG_PREFIX = "#";
@ArrayType(type = String.class, min = 1)
@Description("Registry biomes this rule applies to: an exact key such as 'minecraft:forest', a namespace such as 'minecraft:', or a biome tag such as '#minecraft:is_forest'.")
private KList<String> biomes = new KList<>();
@ArrayType(type = String.class, min = 1)
@Description("Placed feature families denied in the matched biomes, matched like importedFeatures.disabled on namespace, slash, or underscore boundaries.")
private KList<String> disabled = new KList<>();
@ArrayType(type = String.class, min = 1)
@Description("Exact placed feature keys denied in the matched biomes after trimming and case normalization.")
private KList<String> disabledExact = new KList<>();
public boolean appliesTo(String biomeKey, Set<String> biomeTags) {
String normalizedBiome = normalize(biomeKey);
for (String entry : Objects.requireNonNull(biomes, "importedFeatures.biomeRules.biomes must not be null")) {
String normalizedEntry = normalize(entry);
if (normalizedEntry.isEmpty()) {
continue;
}
if (normalizedEntry.startsWith(TAG_PREFIX)) {
if (biomeTags.contains(normalizedEntry.substring(TAG_PREFIX.length()))) {
return true;
}
continue;
}
if (normalizedEntry.endsWith(":") ? normalizedBiome.startsWith(normalizedEntry) : normalizedBiome.equals(normalizedEntry)) {
return true;
}
}
return false;
}
public boolean denies(String featureKey) {
for (String entry : Objects.requireNonNull(disabled, "importedFeatures.biomeRules.disabled must not be null")) {
if (IrisImportedStructureControl.matchesKey(entry, featureKey)) {
return true;
}
}
String normalizedFeature = normalize(featureKey);
for (String entry : Objects.requireNonNull(disabledExact, "importedFeatures.biomeRules.disabledExact must not be null")) {
if (!normalize(entry).isEmpty() && normalize(entry).equals(normalizedFeature)) {
return true;
}
}
return false;
}
private static String normalize(String value) {
return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
}
}
@@ -31,6 +31,7 @@ import lombok.experimental.Accessors;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
@Accessors(chain = true)
@NoArgsConstructor
@@ -49,6 +50,10 @@ public class IrisImportedFeatureControl implements NativeImportedFeatureControl
@Description("Exact placed feature keys to deny after trimming and case normalization. Unlike 'disabled', entries never match key families, so 'minecraft:ore_iron' does not disable 'minecraft:ore_iron_small'.")
private KList<String> disabledExact = new KList<>();
@ArrayType(type = IrisImportedFeatureBiomeRule.class, min = 1)
@Description("Per-biome denials applied on top of 'disabled' and 'disabledExact'. Each rule names registry biomes and the placed features it denies in those biomes only, so a mod tree injected into vanilla forests can be denied there while the mod's own biomes keep it.")
private KList<IrisImportedFeatureBiomeRule> biomeRules = new KList<>();
@ArrayType(type = IrisDecorationStep.class, min = 1)
@Description("Restrict feature generation to these decoration steps only. Empty (the default) means every step is eligible. Use this to import ores without importing vegetation: set it to UNDERGROUND_ORES.")
private KList<IrisDecorationStep> steps = new KList<>();
@@ -124,6 +129,22 @@ public class IrisImportedFeatureControl implements NativeImportedFeatureControl
return shouldGenerateStep(IrisDecorationStep.byOrdinal(step));
}
@Override
public boolean hasBiomeFeatureRules() {
return biomeRules != null && !biomeRules.isEmpty();
}
@Override
public boolean shouldGenerateInBiome(String biomeKey, Set<String> biomeTags, String featureKey) {
for (IrisImportedFeatureBiomeRule rule : Objects.requireNonNull(
biomeRules, "importedFeatures.biomeRules must not be null")) {
if (rule != null && rule.appliesTo(biomeKey, biomeTags) && rule.denies(featureKey)) {
return false;
}
}
return true;
}
@Override
public boolean hasFeatureFilter() {
return (disabled != null && !disabled.isEmpty()) || (disabledExact != null && !disabledExact.isEmpty());
@@ -35,7 +35,7 @@ public class ImportedFeatureWriterTest {
public void sourceThatAlreadyRanImportedFeaturesKeepsVanillaAndOtherModsGenerating() {
JsonObject dimension = dimension("{\"importedFeatures\": {\"enabled\": true, \"disabled\": [\"minecraft:ore_diamond\"]}}");
IrisImportedFeatureControl control = ImportedFeatureWriter.write(dimension, Set.of("examplemod:ore_tin"), Set.of("othermod"));
IrisImportedFeatureControl control = ImportedFeatureWriter.write(dimension, Set.of("examplemod:ore_tin"), Set.of("othermod"), Set.of());
JsonObject imported = dimension.getAsJsonObject("importedFeatures");
assertEquals(List.of("minecraft:ore_diamond"), ModIngestFixture.strings(imported.get("disabled")));
@@ -50,7 +50,7 @@ public class ImportedFeatureWriterTest {
public void sourceWithoutImportedFeaturesOnlyRunsTheSelectedModsPassthrough() {
JsonObject dimension = dimension("{}");
IrisImportedFeatureControl control = ImportedFeatureWriter.write(dimension, Set.of("examplemod:ore_tin"), Set.of("othermod"));
IrisImportedFeatureControl control = ImportedFeatureWriter.write(dimension, Set.of("examplemod:ore_tin"), Set.of("othermod"), Set.of());
JsonObject imported = dimension.getAsJsonObject("importedFeatures");
assertTrue(imported.get("enabled").getAsBoolean());
@@ -66,7 +66,7 @@ public class ImportedFeatureWriterTest {
public void existingExactEntriesAreKept() {
JsonObject dimension = dimension("{\"importedFeatures\": {\"enabled\": false, \"disabledExact\": [\"examplemod:old\"]}}");
ImportedFeatureWriter.write(dimension, Set.of("examplemod:ore_tin"), Set.of());
ImportedFeatureWriter.write(dimension, Set.of("examplemod:ore_tin"), Set.of(), Set.of());
assertEquals(List.of("examplemod:old", "examplemod:ore_tin"),
ModIngestFixture.strings(dimension.getAsJsonObject("importedFeatures").get("disabledExact")));
@@ -0,0 +1,117 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 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.pack.ingest;
import art.arcane.iris.pack.ingest.catalog.CatalogBiome;
import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IngestTreeScopeTest {
private static final int VEGETAL_STEP = ModIngestFixture.VEGETAL_STEP;
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private ModIngestReport report;
private JsonObject imported;
@Before
public void ingest() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
Path target = root.resolve("packs").resolve("trees");
report = ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(ModIngestFixture.MOD), Set.of(), false));
imported = ModIngestFixture.readJson(target.resolve("dimensions/trees.json")).getAsJsonObject("importedFeatures");
}
@Test
public void modTreesInjectedIntoThePacksVanillaBiomesAreDeniedThereOnly() {
JsonArray rules = imported.getAsJsonArray("biomeRules");
assertEquals(1, rules.size());
JsonObject rule = rules.get(0).getAsJsonObject();
assertEquals(List.of("minecraft:"), ModIngestFixture.strings(rule.get("biomes")));
assertEquals(List.of("examplemod:inline_trees", "examplemod:oak_bushes", "examplemod:simple_trees"),
ModIngestFixture.strings(rule.get("disabledExact")));
ModIngestItem bushes = ModIngestFixture.item(report, "examplemod:oak_bushes");
assertEquals(ModIngestTier.PASSTHROUGH, bushes.tier());
assertTrue(bushes.reason(), bushes.reason().contains("minecraft biomes"));
}
@Test
public void nonTreeDecorationAndModOnlyTreesKeepRunningEverywhere() {
List<String> denied = ModIngestFixture.strings(imported.getAsJsonArray("biomeRules").get(0).getAsJsonObject().get("disabledExact"));
assertEquals(ModIngestTier.PASSTHROUGH, ModIngestFixture.item(report, "examplemod:mossy_rocks").tier());
assertTrue(denied.stream().noneMatch(key -> key.equals("examplemod:mossy_rocks")));
assertTrue(denied.stream().noneMatch(key -> key.equals("examplemod:maple_trees")));
}
private static WorldgenCatalog catalog() {
WorldgenCatalog fixture = ModIngestFixture.catalog();
List<String> injected = List.of(ModIngestFixture.MOD + ":oak_bushes", ModIngestFixture.MOD + ":simple_trees",
ModIngestFixture.MOD + ":inline_trees", ModIngestFixture.MOD + ":mossy_rocks");
List<CatalogBiome> biomes = new ArrayList<>();
for (CatalogBiome biome : fixture.biomes()) {
if (!biome.key().equals("minecraft:forest") && !biome.key().equals(ModIngestFixture.MOD + ":maple_woods")) {
biomes.add(biome);
continue;
}
List<List<String>> steps = new ArrayList<>(biome.featureSteps());
List<String> vegetal = new ArrayList<>(steps.get(VEGETAL_STEP));
vegetal.addAll(injected);
steps.set(VEGETAL_STEP, vegetal);
biomes.add(new CatalogBiome(biome.key(), biome.temperature(), biome.downfall(), true, biome.tags(), steps, biome.spawns(), null));
}
List<CatalogPlacedFeature> features = new ArrayList<>(fixture.placedFeatures());
features.add(feature("oak_bushes", "minecraft:random_selector", """
{"features": [{"chance": 0.3, "feature": "examplemod:maple_trees"}], "default": "examplemod:mossy_rocks"}
"""));
features.add(feature("simple_trees", "minecraft:simple_random_selector", """
{"features": ["examplemod:mossy_rocks", "examplemod:maple_trees"]}
"""));
features.add(feature("inline_trees", "minecraft:random_boolean_selector", """
{"feature_true": {"feature": {"type": "minecraft:tree", "config": {}}, "placement": []}, "feature_false": "examplemod:mossy_rocks"}
"""));
features.add(feature("mossy_rocks", "minecraft:forest_rock", "{\"state\": \"minecraft:mossy_cobblestone\"}"));
return new WorldgenCatalog(fixture.format(), fixture.minecraftVersion(), fixture.platform(), fixture.capturedAtEpochMillis(),
fixture.contentFingerprint(), fixture.namespaces(), biomes, features, fixture.structures(),
fixture.dimensionTypes(), fixture.levelStems());
}
private static CatalogPlacedFeature feature(String path, String type, String config) {
String key = ModIngestFixture.MOD + ":" + path;
return new CatalogPlacedFeature(key, key, type, JsonParser.parseString(config), JsonParser.parseString("[]"));
}
}
@@ -83,7 +83,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "5841ff4a1ad2813e4ad45f941fd40e5083a6b1b29a61c522df9efa12eb04f8c0";
private static final String EXPECTED_SCHEMA_DIGEST = "c99b0db94bfd6f316738beb87401edde5b52eed11bfa419148bb2e8c08b0d7a9";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
@@ -23,6 +23,8 @@ import art.arcane.iris.generation.decoration.IrisDecorationStep;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
@@ -111,6 +113,45 @@ public class IrisImportedFeatureControlTest {
assertThrows(NullPointerException.class, () -> control.shouldGenerate("examplemod:ore_tin"));
}
@Test
public void biomeRulesDenyFeaturesOnlyInMatchingBiomes() {
IrisImportedFeatureBiomeRule rule = new IrisImportedFeatureBiomeRule()
.setBiomes(keys("minecraft:"))
.setDisabledExact(keys("terralith:trees/oak"))
.setDisabled(keys("terralith:bush"));
IrisImportedFeatureControl control = new IrisImportedFeatureControl()
.setEnabled(true)
.setBiomeRules(new KList<>(rule));
assertTrue(control.hasBiomeFeatureRules());
assertFalse(control.shouldGenerateInBiome("minecraft:forest", Set.of(), "terralith:trees/oak"));
assertFalse(control.shouldGenerateInBiome("minecraft:forest", Set.of(), "terralith:bush/small"));
assertTrue(control.shouldGenerateInBiome("minecraft:forest", Set.of(), "terralith:trees/oak_large"));
assertTrue(control.shouldGenerateInBiome("minecraft:forest", Set.of(), "terralith:flowers/lavender"));
assertTrue(control.shouldGenerateInBiome("terralith:lavender_forest", Set.of(), "terralith:trees/oak"));
assertTrue(control.shouldGenerate("terralith:trees/oak"));
}
@Test
public void biomeRulesMatchExactKeysAndTags() {
IrisImportedFeatureBiomeRule rule = new IrisImportedFeatureBiomeRule()
.setBiomes(keys("minecraft:forest", "#minecraft:is_taiga"))
.setDisabledExact(keys("terralith:trees/oak"));
IrisImportedFeatureControl control = new IrisImportedFeatureControl()
.setEnabled(true)
.setBiomeRules(new KList<>(rule));
assertFalse(control.shouldGenerateInBiome("minecraft:forest", Set.of(), "terralith:trees/oak"));
assertTrue(control.shouldGenerateInBiome("minecraft:forest_hills", Set.of(), "terralith:trees/oak"));
assertFalse(control.shouldGenerateInBiome("minecraft:taiga", Set.of("minecraft:is_taiga"), "terralith:trees/oak"));
assertTrue(control.shouldGenerateInBiome("minecraft:plains", Set.of("minecraft:is_overworld"), "terralith:trees/oak"));
}
@Test
public void controlWithoutBiomeRulesReportsNone() {
assertFalse(new IrisImportedFeatureControl().setEnabled(true).hasBiomeFeatureRules());
}
@Test
public void stepAllowListNarrowsGenerationToListedStepsOnly() {
IrisImportedFeatureControl control = new IrisImportedFeatureControl()