mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
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:
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);
|
||||
}
|
||||
}
|
||||
+91
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user