From 7767d6d30a1cc5054e6837f855ace423914e3f63 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 14 Oct 2026 21:35:38 -0400 Subject: [PATCH] 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 --- .../pack/ingest/ImportedFeatureWriter.java | 32 ++++- .../iris/pack/ingest/ModFeatureIngester.java | 28 ++++ .../arcane/iris/pack/ingest/ModIngester.java | 2 +- .../arcane/iris/pack/ingest/TreeFeatures.java | 120 ++++++++++++++++++ .../IrisImportedFeatureBiomeRule.java | 91 +++++++++++++ .../nativegen/IrisImportedFeatureControl.java | 21 +++ .../ingest/ImportedFeatureWriterTest.java | 6 +- .../iris/pack/ingest/IngestTreeScopeTest.java | 117 +++++++++++++++++ .../schema/SchemaBuilderIdentityTest.java | 2 +- .../IrisImportedFeatureControlTest.java | 41 ++++++ 10 files changed, 454 insertions(+), 6 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/pack/ingest/TreeFeatures.java create mode 100644 core/src/main/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureBiomeRule.java create mode 100644 core/src/test/java/art/arcane/iris/pack/ingest/IngestTreeScopeTest.java diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/ImportedFeatureWriter.java b/core/src/main/java/art/arcane/iris/pack/ingest/ImportedFeatureWriter.java index 24c5ddb5a..6e2113461 100644 --- a/core/src/main/java/art/arcane/iris/pack/ingest/ImportedFeatureWriter.java +++ b/core/src/main/java/art/arcane/iris/pack/ingest/ImportedFeatureWriter.java @@ -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 nativeKeys, Set unselectedNamespaces) { + static IrisImportedFeatureControl write(JsonObject dimension, Set nativeKeys, Set unselectedNamespaces, + Set vanillaScopedKeys) { JsonElement existing = dimension.get("importedFeatures"); JsonObject imported = existing != null && existing.isJsonObject() ? existing.getAsJsonObject() : new JsonObject(); Set 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 biomeRules(JsonArray rules) { + KList 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(); diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/ModFeatureIngester.java b/core/src/main/java/art/arcane/iris/pack/ingest/ModFeatureIngester.java index e87ab8d55..863e9dd7c 100644 --- a/core/src/main/java/art/arcane/iris/pack/ingest/ModFeatureIngester.java +++ b/core/src/main/java/art/arcane/iris/pack/ingest/ModFeatureIngester.java @@ -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 plans = new TreeMap<>(); private final Set nativeKeys = new TreeSet<>(); + private final Set vanillaScopedKeys = new TreeSet<>(); ModFeatureIngester(IngestPackModel model, WorldgenCatalogIndex index, Set namespaces) { this.model = model; this.index = index; + this.trees = new TreeFeatures(index); Map> declaring = new TreeMap<>(); Map> steps = new TreeMap<>(); for (CatalogBiome biome : index.catalog().biomes()) { @@ -76,6 +81,10 @@ final class ModFeatureIngester { return nativeKeys; } + Set vanillaScopedKeys() { + return vanillaScopedKeys; + } + Map 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 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 declaring, Set steps) { Optional feature = index.placedFeature(key); if (feature.isEmpty()) { diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/ModIngester.java b/core/src/main/java/art/arcane/iris/pack/ingest/ModIngester.java index b5f850ffd..a5ade28d4 100644 --- a/core/src/main/java/art/arcane/iris/pack/ingest/ModIngester.java +++ b/core/src/main/java/art/arcane/iris/pack/ingest/ModIngester.java @@ -89,7 +89,7 @@ public final class ModIngester { List 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 hostBiomes = new TreeSet<>(adopted.keySet()); hostBiomes.addAll(model.reachableBiomes().values()); diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/TreeFeatures.java b/core/src/main/java/art/arcane/iris/pack/ingest/TreeFeatures.java new file mode 100644 index 000000000..3217f8a8d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/pack/ingest/TreeFeatures.java @@ -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 . + */ + +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 TREE_TYPES = Set.of("minecraft:tree", "minecraft:fallen_tree", "minecraft:huge_brown_mushroom", + "minecraft:huge_red_mushroom", "minecraft:huge_fungus"); + private static final List 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 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 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); + } +} diff --git a/core/src/main/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureBiomeRule.java b/core/src/main/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureBiomeRule.java new file mode 100644 index 000000000..a0fb2fef3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureBiomeRule.java @@ -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 . + */ + +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 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 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 disabledExact = new KList<>(); + + public boolean appliesTo(String biomeKey, Set 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); + } +} diff --git a/core/src/main/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureControl.java b/core/src/main/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureControl.java index 5f3ad666d..6e8124cd0 100644 --- a/core/src/main/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureControl.java +++ b/core/src/main/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureControl.java @@ -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 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 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 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 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()); diff --git a/core/src/test/java/art/arcane/iris/pack/ingest/ImportedFeatureWriterTest.java b/core/src/test/java/art/arcane/iris/pack/ingest/ImportedFeatureWriterTest.java index bb77d5296..760290b80 100644 --- a/core/src/test/java/art/arcane/iris/pack/ingest/ImportedFeatureWriterTest.java +++ b/core/src/test/java/art/arcane/iris/pack/ingest/ImportedFeatureWriterTest.java @@ -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"))); diff --git a/core/src/test/java/art/arcane/iris/pack/ingest/IngestTreeScopeTest.java b/core/src/test/java/art/arcane/iris/pack/ingest/IngestTreeScopeTest.java new file mode 100644 index 000000000..8527399cb --- /dev/null +++ b/core/src/test/java/art/arcane/iris/pack/ingest/IngestTreeScopeTest.java @@ -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 . + */ + +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 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 injected = List.of(ModIngestFixture.MOD + ":oak_bushes", ModIngestFixture.MOD + ":simple_trees", + ModIngestFixture.MOD + ":inline_trees", ModIngestFixture.MOD + ":mossy_rocks"); + List 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> steps = new ArrayList<>(biome.featureSteps()); + List 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 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("[]")); + } +} diff --git a/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java b/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java index 6908389af..05de3a92c 100644 --- a/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java +++ b/core/src/test/java/art/arcane/iris/pack/schema/SchemaBuilderIdentityTest.java @@ -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> SCHEMA_ROOTS = List.of( IrisDimension.class, diff --git a/core/src/test/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureControlTest.java b/core/src/test/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureControlTest.java index efb594c06..5352f947b 100644 --- a/core/src/test/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureControlTest.java +++ b/core/src/test/java/art/arcane/iris/structure/nativegen/IrisImportedFeatureControlTest.java @@ -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()