From 0bc235afb69d2c2d6752d2c06860d61d371ba8ea Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 14 Oct 2026 21:26:22 -0400 Subject: [PATCH] Scored ingest biome placement - Mod biomes pick their template across all regions by climate distance, biome category agreement from minecraft and c tags, a reuse penalty that spreads biomes over equally good templates, and the region's overall climate - Regions much rarer than the dimension's median region are treated as themed and only receive a mod biome when nothing else fits - Templates derived from void biomes no longer act as climate references - /iris ingest mods accepts excludeRegions= to keep mod biomes out of named regions; unknown region names are refused --- .../modded/command/ModIngestArguments.java | 33 +++- .../modded/command/ModdedCatalogCommands.java | 2 +- .../modded/command/ModdedCommandHelp.java | 2 +- .../localization/ModdedCatalogMessages.java | 2 +- .../command/ModIngestArgumentsTest.java | 6 +- .../command/ModdedCatalogCommandsTest.java | 2 +- .../iris/pack/ingest/BiomeCategories.java | 82 ++++++++++ .../iris/pack/ingest/IngestPackModel.java | 24 ++- .../iris/pack/ingest/ModBiomeAdopter.java | 79 ++++++--- .../iris/pack/ingest/ModBiomeClassifier.java | 2 +- .../iris/pack/ingest/ModIngestRequest.java | 4 +- .../arcane/iris/pack/ingest/ModIngester.java | 15 +- core/src/main/resources/languages/de_DE.toml | 2 +- core/src/main/resources/languages/es_ES.toml | 2 +- core/src/main/resources/languages/fi_FI.toml | 2 +- core/src/main/resources/languages/fr_FR.toml | 2 +- core/src/main/resources/languages/he_IL.toml | 2 +- core/src/main/resources/languages/it_IT.toml | 2 +- core/src/main/resources/languages/ja-JP.toml | 2 +- core/src/main/resources/languages/ko_KR.toml | 2 +- core/src/main/resources/languages/lt_LT.toml | 2 +- core/src/main/resources/languages/nl_NL.toml | 2 +- core/src/main/resources/languages/pl_PL.toml | 2 +- core/src/main/resources/languages/pt_PT.toml | 2 +- core/src/main/resources/languages/ru_RU.toml | 2 +- core/src/main/resources/languages/tr_TR.toml | 2 +- core/src/main/resources/languages/vi_VI.toml | 2 +- core/src/main/resources/languages/zh_CN.toml | 2 +- core/src/main/resources/languages/zh_TW.toml | 2 +- .../pack/ingest/IngestBiomePlacementTest.java | 154 ++++++++++++++++++ .../pack/ingest/IngestHeightBoundsTest.java | 2 +- .../ingest/IngestReservedNamespaceTest.java | 4 +- .../iris/pack/ingest/ModIngestFixture.java | 2 +- .../iris/pack/ingest/ModIngesterPackTest.java | 6 +- 34 files changed, 389 insertions(+), 66 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/pack/ingest/BiomeCategories.java create mode 100644 core/src/test/java/art/arcane/iris/pack/ingest/IngestBiomePlacementTest.java diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java index 03b885d11..785a4ba7d 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModIngestArguments.java @@ -26,21 +26,25 @@ import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; -record ModIngestArguments(String sourcePack, String targetPack, String dimension, Set namespaces, boolean replace) { - static final List OPTION_SUGGESTIONS = List.of("dimension=", "namespaces=", "replace=true"); +record ModIngestArguments(String sourcePack, String targetPack, String dimension, Set namespaces, + Set excludedRegions, boolean replace) { + static final List OPTION_SUGGESTIONS = List.of("dimension=", "namespaces=", "excludeRegions=", "replace=true"); private static final Pattern PACK = Pattern.compile("[A-Za-z0-9_.-]+"); private static final Pattern TARGET_PACK = Pattern.compile("[A-Za-z0-9_-]+"); private static final Pattern DIMENSION = Pattern.compile("[A-Za-z0-9_/.-]+"); private static final Pattern NAMESPACE = Pattern.compile("[a-z0-9_.-]+"); + private static final Pattern REGION = Pattern.compile("[A-Za-z0-9_/.-]+"); private static final String DIMENSION_KEY = "dimension"; private static final String NAMESPACES_KEY = "namespaces"; private static final String REPLACE_KEY = "replace"; + private static final String EXCLUDE_REGIONS_KEY = "excludeRegions"; ModIngestArguments { Objects.requireNonNull(sourcePack, "sourcePack"); Objects.requireNonNull(targetPack, "targetPack"); Objects.requireNonNull(dimension, "dimension"); namespaces = Set.copyOf(Objects.requireNonNull(namespaces, "namespaces")); + excludedRegions = Set.copyOf(Objects.requireNonNull(excludedRegions, "excludedRegions")); } static ModIngestArguments parse(String raw) { @@ -62,7 +66,8 @@ record ModIngestArguments(String sourcePack, String targetPack, String dimension continue; } String key = token.substring(0, equals); - if (!key.equals(DIMENSION_KEY) && !key.equals(NAMESPACES_KEY) && !key.equals(REPLACE_KEY)) { + if (!key.equals(DIMENSION_KEY) && !key.equals(NAMESPACES_KEY) && !key.equals(REPLACE_KEY) + && !key.equals(EXCLUDE_REGIONS_KEY)) { throw new IllegalArgumentException("unknown option '" + key + "'"); } if (options.putIfAbsent(key, token.substring(equals + 1)) != null) { @@ -88,7 +93,7 @@ record ModIngestArguments(String sourcePack, String targetPack, String dimension throw new IllegalArgumentException("invalid dimension key '" + dimension + "'"); } return new ModIngestArguments(source, target, dimension, namespaces(options.get(NAMESPACES_KEY)), - replace(options.get(REPLACE_KEY))); + regions(options.get(EXCLUDE_REGIONS_KEY)), replace(options.get(REPLACE_KEY))); } private static void requirePack(String pack) { @@ -117,6 +122,26 @@ record ModIngestArguments(String sourcePack, String targetPack, String dimension return namespaces; } + private static Set regions(String raw) { + if (raw == null) { + return Set.of(); + } + Set regions = new LinkedHashSet<>(); + for (String part : raw.split(",")) { + if (part.isEmpty()) { + continue; + } + if (!REGION.matcher(part).matches() || part.contains("..")) { + throw new IllegalArgumentException("invalid region '" + part + "'"); + } + regions.add(part); + } + if (regions.isEmpty()) { + throw new IllegalArgumentException("excludeRegions must not be empty"); + } + return regions; + } + private static boolean replace(String raw) { if (raw == null || raw.equals("false")) { return false; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java index c8560f906..c76330883 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCatalogCommands.java @@ -197,7 +197,7 @@ final class ModdedCatalogCommands { } Path targetPack = packsRoot.toPath().toAbsolutePath().normalize().resolve(arguments.targetPack()); ModIngestRequest request = new ModIngestRequest(sourcePack.toPath(), targetPack, arguments.dimension(), - catalog.get(), arguments.namespaces(), arguments.replace()); + catalog.get(), arguments.namespaces(), arguments.excludedRegions(), arguments.replace()); IrisModdedCommands.ok(source, IrisLanguage.plain(ModdedCatalogMessages.INGEST_STARTED, MessageArgument.untrusted("source", arguments.sourcePack()), MessageArgument.untrusted("target", arguments.targetPack()), diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java index 257fa0d23..7051db563 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java @@ -169,7 +169,7 @@ final class ModdedCommandHelp { )); SECTIONS.put("pk", SECTIONS.get("pack")); SECTIONS.put("ingest", List.of( - Entry.command("mods", " [dimension=] [namespaces=] [replace=true]", ModdedCatalogMessages.HELP_INGEST_MODS) + Entry.command("mods", " [dimension=] [namespaces=] [excludeRegions=] [replace=true]", ModdedCatalogMessages.HELP_INGEST_MODS) )); SECTIONS.put("world", List.of( Entry.command("enable", " [seed|random]", ModdedHelpMessages.COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK, "create"), diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java index 5482f9183..1af900009 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCatalogMessages.java @@ -77,7 +77,7 @@ public final class ModdedCatalogMessages { public static final TextKey INGEST_INVALID = TextKey.of( "iris.modded.catalog.ingest_invalid", "Invalid arguments: {reason}. Usage: /iris ingest mods [dimension=] " - + "[namespaces=] [replace=true]" + + "[namespaces=] [excludeRegions=] [replace=true]" ); public static final TextKey INGEST_PACK_MISSING = TextKey.of( "iris.modded.catalog.ingest_pack_missing", diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java index c90dcca25..f1d897761 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModIngestArgumentsTest.java @@ -36,15 +36,17 @@ public class ModIngestArgumentsTest { assertEquals("modded_overworld", arguments.targetPack()); assertEquals("overworld", arguments.dimension()); assertEquals(Set.of(), arguments.namespaces()); + assertEquals(Set.of(), arguments.excludedRegions()); assertFalse(arguments.replace()); } @Test public void keyedOptionalsAreParsedInAnyOrder() { ModIngestArguments arguments = ModIngestArguments.parse( - " overworld modded replace=true namespaces=biomesoplenty,terralith dimension=overworld_alt "); + " overworld modded replace=true namespaces=biomesoplenty,terralith excludeRegions=prismatics,estranged dimension=overworld_alt "); assertEquals("overworld_alt", arguments.dimension()); + assertEquals(Set.of("prismatics", "estranged"), arguments.excludedRegions()); assertEquals(Set.of("biomesoplenty", "terralith"), arguments.namespaces()); assertTrue(arguments.replace()); } @@ -72,6 +74,8 @@ public class ModIngestArgumentsTest { assertEquals("invalid dimension key 'a..b'", reason("overworld modded dimension=a..b")); assertEquals("invalid namespace 'Bad'", reason("overworld modded namespaces=ok,Bad")); assertEquals("namespaces must not be empty", reason("overworld modded namespaces=,")); + assertEquals("invalid region 'a/../b'", reason("overworld modded excludeRegions=a/../b")); + assertEquals("excludeRegions must not be empty", reason("overworld modded excludeRegions=,")); assertEquals("source and target must be different packs", reason("overworld overworld")); } diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedCatalogCommandsTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedCatalogCommandsTest.java index 434fd750b..251d84087 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedCatalogCommandsTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedCatalogCommandsTest.java @@ -62,7 +62,7 @@ public class ModdedCatalogCommandsTest { assertTrue(ModdedCommandHelp.documents("ingest", "mods")); List lines = ModdedCommandHelp.consoleLines("ingest"); assertTrue(lines.stream().anyMatch((String line) -> - line.startsWith("/iris ingest mods [dimension=] [namespaces=] [replace=true] - "))); + line.startsWith("/iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true] - "))); } private static CommandNode child(CommandNode parent, String name) { diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/BiomeCategories.java b/core/src/main/java/art/arcane/iris/pack/ingest/BiomeCategories.java new file mode 100644 index 000000000..d2b20c697 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/pack/ingest/BiomeCategories.java @@ -0,0 +1,82 @@ +/* + * 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 java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +final class BiomeCategories { + private static final Map> CATEGORY_TAGS = Map.ofEntries( + Map.entry("forest", Set.of("is_forest", "is_birch_forest", "is_dark_forest", "is_flower_forest", "is_tree/deciduous")), + Map.entry("taiga", Set.of("is_taiga", "is_tree/coniferous", "is_old_growth")), + Map.entry("jungle", Set.of("is_jungle", "is_tree/jungle")), + Map.entry("savanna", Set.of("is_savanna", "is_tree/savanna")), + Map.entry("badlands", Set.of("is_badlands")), + Map.entry("desert", Set.of("is_desert", "is_sandy")), + Map.entry("mountain", Set.of("is_mountain", "is_mountain/peak", "is_mountain/slope", "is_peak", "is_slope", "is_hill", "is_windswept", "is_plateau")), + Map.entry("snowy", Set.of("is_snowy", "is_icy")), + Map.entry("swamp", Set.of("is_swamp")), + Map.entry("mushroom", Set.of("is_mushroom")), + Map.entry("plains", Set.of("is_plains", "is_meadow")), + Map.entry("beach", Set.of("is_beach", "is_stony_shores")), + Map.entry("ocean", Set.of("is_ocean", "is_deep_ocean")), + Map.entry("cave", Set.of("is_cave", "is_underground"))); + + private static final double UNKNOWN_DISTANCE = 0.5D; + private static final String VOID_TAG = "is_void"; + + private BiomeCategories() { + } + + static Set of(List tags) { + Set paths = ModBiomeClassifier.conventionTags(tags); + Set categories = new TreeSet<>(); + for (Map.Entry> category : CATEGORY_TAGS.entrySet()) { + for (String tag : category.getValue()) { + if (paths.contains(tag)) { + categories.add(category.getKey()); + break; + } + } + } + return categories; + } + + static boolean isVoid(List tags) { + return ModBiomeClassifier.conventionTags(tags).contains(VOID_TAG); + } + + static double distance(Set first, Set second) { + if (first.isEmpty() || second.isEmpty()) { + return UNKNOWN_DISTANCE; + } + Set union = new HashSet<>(first); + union.addAll(second); + int shared = 0; + for (String category : first) { + if (second.contains(category)) { + shared++; + } + } + return (union.size() - shared) / (double) union.size(); + } +} diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/IngestPackModel.java b/core/src/main/java/art/arcane/iris/pack/ingest/IngestPackModel.java index c6fa1082b..ab47fc9c2 100644 --- a/core/src/main/java/art/arcane/iris/pack/ingest/IngestPackModel.java +++ b/core/src/main/java/art/arcane/iris/pack/ingest/IngestPackModel.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Set; final class IngestPackModel { private static final int DEFAULT_MIN_Y = -64; @@ -111,8 +112,21 @@ final class IngestPackModel { } IngestClimate biomeClimate(String biomeKey) { - String derivative = reachableBiomes.get(biomeKey); - return derivative == null ? null : index.biome(derivative).map(IngestClimate::of).orElse(null); + return climateReference(index, reachableBiomes.get(biomeKey)).map(IngestClimate::of).orElse(null); + } + + Set biomeCategories(String biomeKey) { + return climateReference(index, reachableBiomes.get(biomeKey)).map((CatalogBiome biome) -> BiomeCategories.of(biome.tags())).orElse(Set.of()); + } + + private static Optional climateReference(WorldgenCatalogIndex index, String derivative) { + if (derivative == null) { + return Optional.empty(); + } + Optional biome = index.biome(derivative); + return biome.isPresent() && !BiomeCategories.isVoid(biome.get().tags()) && !biome.get().key().equals(DEFAULT_DERIVATIVE) + ? biome + : Optional.empty(); } VanillaHeights.VerticalBounds verticalBounds() { @@ -138,10 +152,10 @@ final class IngestPackModel { } List landClimates = new ArrayList<>(); for (String biome : pools.get(BiomeRole.LAND)) { - Optional catalogBiome = index.biome(reachable.get(biome)); + Optional catalogBiome = climateReference(index, reachable.get(biome)); catalogBiome.ifPresent(value -> landClimates.add(IngestClimate.of(value))); } - return new RegionProfile(key, IngestClimate.average(landClimates), pools); + return new RegionProfile(key, IngestClimate.average(landClimates), (int) number(region.get("rarity"), 1D), pools); } private static void collectReachable(IngestPack pack, List regions, Map reachable) throws IOException { @@ -192,7 +206,7 @@ final class IngestPackModel { } } - record RegionProfile(String key, IngestClimate climate, Map> pools) { + record RegionProfile(String key, IngestClimate climate, int rarity, Map> pools) { List pool(BiomeRole role) { return pools.get(role); } diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/ModBiomeAdopter.java b/core/src/main/java/art/arcane/iris/pack/ingest/ModBiomeAdopter.java index 1ae4a5b0b..f0f4d91af 100644 --- a/core/src/main/java/art/arcane/iris/pack/ingest/ModBiomeAdopter.java +++ b/core/src/main/java/art/arcane/iris/pack/ingest/ModBiomeAdopter.java @@ -26,6 +26,10 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; @@ -36,15 +40,25 @@ final class ModBiomeAdopter { static final String INGEST_BIOME_FOLDER = "ingest/"; private static final List OVERRIDE_FIELDS = List.of("customDerivitives", "vanillaDerivative", "biomeScatter", "biomeSkyScatter", "children"); private static final int VEGETAL_STEP = IrisDecorationStep.VEGETAL_DECORATION.ordinal(); + private static final double CATEGORY_WEIGHT = 1D; + private static final double REGION_CLIMATE_WEIGHT = 0.25D; + private static final double REUSE_WEIGHT = 0.05D; + private static final double THEMED_REGION_PENALTY = 0.6D; + private static final int THEMED_RARITY_FACTOR = 5; private final IngestPackModel model; private final ModBiomeClassifier classifier; private final Set decoratorFeatures; + private final Set excludedRegions; + private final Set themedRegions; + private final Map templateUses = new HashMap<>(); - ModBiomeAdopter(IngestPackModel model, WorldgenCatalogIndex index, Set decoratorFeatures) { + ModBiomeAdopter(IngestPackModel model, WorldgenCatalogIndex index, Set decoratorFeatures, Set excludedRegions) { this.model = model; this.classifier = new ModBiomeClassifier(model.environment(), index.catalog()); this.decoratorFeatures = decoratorFeatures; + this.excludedRegions = Set.copyOf(excludedRegions); + this.themedRegions = themedRegions(model.regions()); } Map adopt(List modBiomes, List items) throws IOException { @@ -57,14 +71,15 @@ final class ModBiomeAdopter { continue; } BiomeRole role = classification.role(); - IngestClimate climate = IngestClimate.of(biome); - IngestPackModel.RegionProfile region = nearestRegion(role, climate); - if (region == null) { + Placement placement = place(role, IngestClimate.of(biome), BiomeCategories.of(biome.tags())); + if (placement == null) { items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "", - "no region in this dimension has a " + role.label() + " biome pool")); + "no region in this dimension accepts a " + role.label() + " biome")); continue; } - String template = nearestTemplate(region, role, climate); + IngestPackModel.RegionProfile region = placement.region(); + String template = placement.template(); + templateUses.merge(template, 1, Integer::sum); String irisKey = irisKey(biome.key()); JsonObject json = model.pack().require(IngestPack.biomePath(template)).deepCopy(); transform(json, biome, ownsVegetation(biome, namespace)); @@ -83,34 +98,47 @@ final class ModBiomeAdopter { return INGEST_BIOME_FOLDER + namespace + "/" + modBiomeKey.substring(colon + 1); } - private IngestPackModel.RegionProfile nearestRegion(BiomeRole role, IngestClimate climate) { - IngestPackModel.RegionProfile best = null; - double bestDistance = Double.POSITIVE_INFINITY; + private Placement place(BiomeRole role, IngestClimate climate, Set categories) { + Placement best = null; + double bestScore = Double.POSITIVE_INFINITY; for (IngestPackModel.RegionProfile region : model.regions()) { - if (region.pool(role).isEmpty()) { + if (excludedRegions.contains(region.key())) { continue; } - double distance = climate.distance(region.climate()); - if (best == null || distance < bestDistance) { - best = region; - bestDistance = distance; + double regionPenalty = (themedRegions.contains(region.key()) ? THEMED_REGION_PENALTY : 0D) + + (region.climate() == null ? 0D : REGION_CLIMATE_WEIGHT * climate.distance(region.climate())); + for (String template : region.pool(role)) { + IngestClimate templateClimate = model.biomeClimate(template); + double score = climate.distance(templateClimate == null ? region.climate() : templateClimate) + + CATEGORY_WEIGHT * BiomeCategories.distance(categories, model.biomeCategories(template)) + + REUSE_WEIGHT * templateUses.getOrDefault(template, 0) + + regionPenalty; + if (best == null || score < bestScore) { + best = new Placement(region, template); + bestScore = score; + } } } return best; } - private String nearestTemplate(IngestPackModel.RegionProfile region, BiomeRole role, IngestClimate climate) { - String best = null; - double bestDistance = Double.POSITIVE_INFINITY; - for (String candidate : region.pool(role)) { - IngestClimate candidateClimate = model.biomeClimate(candidate); - double distance = climate.distance(candidateClimate == null ? region.climate() : candidateClimate); - if (best == null || distance < bestDistance) { - best = candidate; - bestDistance = distance; + private static Set themedRegions(List regions) { + if (regions.isEmpty()) { + return Set.of(); + } + List rarities = new ArrayList<>(regions.size()); + for (IngestPackModel.RegionProfile region : regions) { + rarities.add(region.rarity()); + } + rarities.sort(Comparator.naturalOrder()); + int median = Math.max(1, rarities.get(rarities.size() / 2)); + Set themed = new HashSet<>(); + for (IngestPackModel.RegionProfile region : regions) { + if (region.rarity() >= median * THEMED_RARITY_FACTOR) { + themed.add(region.key()); } } - return best; + return themed; } private void transform(JsonObject json, CatalogBiome biome, boolean ownsVegetation) throws IOException { @@ -170,4 +198,7 @@ final class ModBiomeAdopter { record AdoptedBiome(String modKey, String irisKey, BiomeRole role) { } + + private record Placement(IngestPackModel.RegionProfile region, String template) { + } } diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/ModBiomeClassifier.java b/core/src/main/java/art/arcane/iris/pack/ingest/ModBiomeClassifier.java index 43293928d..e5e34359d 100644 --- a/core/src/main/java/art/arcane/iris/pack/ingest/ModBiomeClassifier.java +++ b/core/src/main/java/art/arcane/iris/pack/ingest/ModBiomeClassifier.java @@ -83,7 +83,7 @@ final class ModBiomeClassifier { return false; } - private static Set conventionTags(List tags) { + static Set conventionTags(List tags) { Set paths = new HashSet<>(); for (String tag : tags) { String key = VanillaJson.namespaced(tag); diff --git a/core/src/main/java/art/arcane/iris/pack/ingest/ModIngestRequest.java b/core/src/main/java/art/arcane/iris/pack/ingest/ModIngestRequest.java index 5849b6565..86d50cb16 100644 --- a/core/src/main/java/art/arcane/iris/pack/ingest/ModIngestRequest.java +++ b/core/src/main/java/art/arcane/iris/pack/ingest/ModIngestRequest.java @@ -25,12 +25,14 @@ import java.nio.file.Path; import java.util.Objects; import java.util.Set; -public record ModIngestRequest(Path sourcePack, Path targetPack, String dimensionKey, WorldgenCatalog catalog, Set namespaces, boolean replaceTarget) { +public record ModIngestRequest(Path sourcePack, Path targetPack, String dimensionKey, WorldgenCatalog catalog, Set namespaces, + Set excludedRegions, boolean replaceTarget) { public ModIngestRequest { Objects.requireNonNull(sourcePack, "sourcePack"); Objects.requireNonNull(targetPack, "targetPack"); Objects.requireNonNull(dimensionKey, "dimensionKey"); Objects.requireNonNull(catalog, "catalog"); namespaces = Set.copyOf(Objects.requireNonNull(namespaces, "namespaces")); + excludedRegions = Set.copyOf(Objects.requireNonNull(excludedRegions, "excludedRegions")); } } 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 99d209c30..b5f850ffd 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 @@ -83,8 +83,9 @@ public final class ModIngester { IngestPackModel model = IngestPackModel.load(pack, targetKey, index); ModFeatureIngester features = new ModFeatureIngester(model, index, namespaces); List biomeItems = new ArrayList<>(); - Map adopted = new ModBiomeAdopter(model, index, features.decoratorKeys()) - .adopt(modBiomes(index, namespaces), biomeItems); + requireKnownRegions(model, request.excludedRegions()); + Map adopted = new ModBiomeAdopter(model, index, features.decoratorKeys(), + request.excludedRegions()).adopt(modBiomes(index, namespaces), biomeItems); List featureItems = features.apply(adopted); JsonObject dimension = pack.edit(model.dimensionPath()); IrisImportedFeatureControl control = ImportedFeatureWriter.write(dimension, features.nativeKeys(), @@ -101,6 +102,16 @@ public final class ModIngester { return new ModIngestReport(target, items, IngestManifest.summary(targetKey, pack, reported, items)); } + private static void requireKnownRegions(IngestPackModel model, Set excludedRegions) { + Set unknown = new TreeSet<>(excludedRegions); + for (IngestPackModel.RegionProfile region : model.regions()) { + unknown.remove(region.key()); + } + if (!unknown.isEmpty()) { + throw new IllegalArgumentException("Excluded regions " + unknown + " are not regions of dimension " + model.dimensionPath() + "."); + } + } + private static Set reportedNamespaces(Set namespaces, List items) { Set reported = new TreeSet<>(); for (ModIngestItem item : items) { diff --git a/core/src/main/resources/languages/de_DE.toml b/core/src/main/resources/languages/de_DE.toml index 9b1c1648f..2ea59df22 100644 --- a/core/src/main/resources/languages/de_DE.toml +++ b/core/src/main/resources/languages/de_DE.toml @@ -472,7 +472,7 @@ detail_dimensions = "Dimensionstypen: {value}; Level-Stems: {value2}" refreshing = "Worldgen-Katalog wird erfasst..." refreshed = "Worldgen-Katalog in {duration} erfasst: {path}" refresh_failed = "Erfassung des Worldgen-Katalogs fehlgeschlagen: {reason}" -ingest_invalid = "Ungültige Argumente: {reason}. Verwendung: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Ungültige Argumente: {reason}. Verwendung: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Pack '{pack}' wurde unter {path} nicht gefunden." ingest_started = "Übernehme Mod-Worldgen aus '{source}' in '{target}' (Dimension {dimension})..." ingest_failed = "Mod-Übernahme fehlgeschlagen: {reason}" diff --git a/core/src/main/resources/languages/es_ES.toml b/core/src/main/resources/languages/es_ES.toml index 0d7e1fc03..a18aac02f 100644 --- a/core/src/main/resources/languages/es_ES.toml +++ b/core/src/main/resources/languages/es_ES.toml @@ -472,7 +472,7 @@ detail_dimensions = "Tipos de dimensión: {value}; level stems: {value2}" refreshing = "Capturando el catálogo de generación de mundo..." refreshed = "Catálogo de generación capturado en {duration}: {path}" refresh_failed = "Error al capturar el catálogo de generación: {reason}" -ingest_invalid = "Argumentos no válidos: {reason}. Uso: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Argumentos no válidos: {reason}. Uso: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "No se encontró el pack '{pack}' en {path}." ingest_started = "Incorporando la generación de mods de '{source}' a '{target}' (dimensión {dimension})..." ingest_failed = "La incorporación de mods falló: {reason}" diff --git a/core/src/main/resources/languages/fi_FI.toml b/core/src/main/resources/languages/fi_FI.toml index e0a66e70a..9f911f8ba 100644 --- a/core/src/main/resources/languages/fi_FI.toml +++ b/core/src/main/resources/languages/fi_FI.toml @@ -477,7 +477,7 @@ detail_dimensions = "Ulottuvuustyypit: {value}; level stemit: {value2}" refreshing = "Kerätään maailmangeneroinnin luetteloa..." refreshed = "Maailmangeneroinnin luettelo kerätty ajassa {duration}: {path}" refresh_failed = "Maailmangeneroinnin luettelon kerääminen epäonnistui: {reason}" -ingest_invalid = "Virheelliset argumentit: {reason}. Käyttö: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Virheelliset argumentit: {reason}. Käyttö: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Pakettia '{pack}' ei löytynyt kohteesta {path}." ingest_started = "Tuodaan modien maailmangenerointi paketista '{source}' pakettiin '{target}' (ulottuvuus {dimension})..." ingest_failed = "Modien tuonti epäonnistui: {reason}" diff --git a/core/src/main/resources/languages/fr_FR.toml b/core/src/main/resources/languages/fr_FR.toml index a16467f0a..6ebebe844 100644 --- a/core/src/main/resources/languages/fr_FR.toml +++ b/core/src/main/resources/languages/fr_FR.toml @@ -472,7 +472,7 @@ detail_dimensions = "Types de dimension : {value} ; level stems : {value2}" refreshing = "Capture du catalogue de génération..." refreshed = "Catalogue de génération capturé en {duration} : {path}" refresh_failed = "Échec de la capture du catalogue de génération : {reason}" -ingest_invalid = "Arguments invalides : {reason}. Utilisation : /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Arguments invalides : {reason}. Utilisation : /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Le pack '{pack}' est introuvable dans {path}." ingest_started = "Intégration de la génération des mods de '{source}' vers '{target}' (dimension {dimension})..." ingest_failed = "L'intégration des mods a échoué : {reason}" diff --git a/core/src/main/resources/languages/he_IL.toml b/core/src/main/resources/languages/he_IL.toml index edcff8a2d..b637973b4 100644 --- a/core/src/main/resources/languages/he_IL.toml +++ b/core/src/main/resources/languages/he_IL.toml @@ -477,7 +477,7 @@ detail_dimensions = "סוגי ממדים: {value}; level stems: {value2}" refreshing = "אוסף את קטלוג יצירת העולם..." refreshed = "קטלוג יצירת העולם נאסף תוך {duration}: {path}" refresh_failed = "איסוף קטלוג יצירת העולם נכשל: {reason}" -ingest_invalid = "ארגומנטים לא תקינים: {reason}. שימוש: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "ארגומנטים לא תקינים: {reason}. שימוש: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "החבילה '{pack}' לא נמצאה תחת {path}." ingest_started = "מייבא את יצירת העולם של המודים מ-'{source}' אל '{target}' (ממד {dimension})..." ingest_failed = "ייבוא המודים נכשל: {reason}" diff --git a/core/src/main/resources/languages/it_IT.toml b/core/src/main/resources/languages/it_IT.toml index 9fcbefa03..9b4ed9ede 100644 --- a/core/src/main/resources/languages/it_IT.toml +++ b/core/src/main/resources/languages/it_IT.toml @@ -472,7 +472,7 @@ detail_dimensions = "Tipi di dimensione: {value}; level stem: {value2}" refreshing = "Acquisizione del catalogo di generazione..." refreshed = "Catalogo di generazione acquisito in {duration}: {path}" refresh_failed = "Acquisizione del catalogo di generazione non riuscita: {reason}" -ingest_invalid = "Argomenti non validi: {reason}. Uso: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Argomenti non validi: {reason}. Uso: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Il pack '{pack}' non è stato trovato in {path}." ingest_started = "Importazione della generazione delle mod da '{source}' a '{target}' (dimensione {dimension})..." ingest_failed = "Importazione delle mod non riuscita: {reason}" diff --git a/core/src/main/resources/languages/ja-JP.toml b/core/src/main/resources/languages/ja-JP.toml index 28a415e22..beda621a2 100644 --- a/core/src/main/resources/languages/ja-JP.toml +++ b/core/src/main/resources/languages/ja-JP.toml @@ -475,7 +475,7 @@ detail_dimensions = "ディメンションタイプ: {value}、レベルステ refreshing = "ワールド生成カタログを取得しています..." refreshed = "ワールド生成カタログを {duration} で取得しました: {path}" refresh_failed = "ワールド生成カタログの取得に失敗しました: {reason}" -ingest_invalid = "引数が無効です: {reason}。使い方: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "引数が無効です: {reason}。使い方: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "パック '{pack}' が {path} に見つかりません。" ingest_started = "'{source}' から '{target}' へ Mod のワールド生成を取り込んでいます (ディメンション {dimension})..." ingest_failed = "Mod の取り込みに失敗しました: {reason}" diff --git a/core/src/main/resources/languages/ko_KR.toml b/core/src/main/resources/languages/ko_KR.toml index 44a7ea396..9885ebd03 100644 --- a/core/src/main/resources/languages/ko_KR.toml +++ b/core/src/main/resources/languages/ko_KR.toml @@ -475,7 +475,7 @@ detail_dimensions = "차원 유형: {value}; 레벨 스템: {value2}" refreshing = "월드 생성 카탈로그를 수집하는 중..." refreshed = "월드 생성 카탈로그를 {duration} 만에 수집했습니다: {path}" refresh_failed = "월드 생성 카탈로그 수집에 실패했습니다: {reason}" -ingest_invalid = "잘못된 인수: {reason}. 사용법: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "잘못된 인수: {reason}. 사용법: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "{path}에서 팩 '{pack}'을(를) 찾을 수 없습니다." ingest_started = "'{source}'에서 '{target}'(으)로 모드 월드 생성을 가져오는 중 (차원 {dimension})..." ingest_failed = "모드 가져오기에 실패했습니다: {reason}" diff --git a/core/src/main/resources/languages/lt_LT.toml b/core/src/main/resources/languages/lt_LT.toml index 4489fea6c..8e9a25ee7 100644 --- a/core/src/main/resources/languages/lt_LT.toml +++ b/core/src/main/resources/languages/lt_LT.toml @@ -477,7 +477,7 @@ detail_dimensions = "Dimensijų tipai: {value}; lygio stiebai: {value2}" refreshing = "Renkamas pasaulio generavimo katalogas..." refreshed = "Pasaulio generavimo katalogas surinktas per {duration}: {path}" refresh_failed = "Nepavyko surinkti pasaulio generavimo katalogo: {reason}" -ingest_invalid = "Netinkami argumentai: {reason}. Naudojimas: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Netinkami argumentai: {reason}. Naudojimas: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Paketas '{pack}' nerastas {path}." ingest_started = "Perkeliamas modų pasaulio generavimas iš '{source}' į '{target}' (dimensija {dimension})..." ingest_failed = "Modų perkėlimas nepavyko: {reason}" diff --git a/core/src/main/resources/languages/nl_NL.toml b/core/src/main/resources/languages/nl_NL.toml index 9b20d24d0..e73084b6c 100644 --- a/core/src/main/resources/languages/nl_NL.toml +++ b/core/src/main/resources/languages/nl_NL.toml @@ -472,7 +472,7 @@ detail_dimensions = "Dimensietypen: {value}; level stems: {value2}" refreshing = "Wereldgeneratiecatalogus wordt vastgelegd..." refreshed = "Wereldgeneratiecatalogus vastgelegd in {duration}: {path}" refresh_failed = "Vastleggen van de wereldgeneratiecatalogus mislukt: {reason}" -ingest_invalid = "Ongeldige argumenten: {reason}. Gebruik: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Ongeldige argumenten: {reason}. Gebruik: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Pack '{pack}' is niet gevonden in {path}." ingest_started = "Wereldgeneratie van mods wordt overgenomen van '{source}' naar '{target}' (dimensie {dimension})..." ingest_failed = "Overnemen van mods mislukt: {reason}" diff --git a/core/src/main/resources/languages/pl_PL.toml b/core/src/main/resources/languages/pl_PL.toml index 0bf012fd7..5b1b6658e 100644 --- a/core/src/main/resources/languages/pl_PL.toml +++ b/core/src/main/resources/languages/pl_PL.toml @@ -477,7 +477,7 @@ detail_dimensions = "Typy wymiarów: {value}; level stemy: {value2}" refreshing = "Zbieranie katalogu generowania świata..." refreshed = "Katalog generowania świata zebrany w {duration}: {path}" refresh_failed = "Nie udało się zebrać katalogu generowania świata: {reason}" -ingest_invalid = "Nieprawidłowe argumenty: {reason}. Użycie: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Nieprawidłowe argumenty: {reason}. Użycie: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Nie znaleziono paczki '{pack}' w {path}." ingest_started = "Przenoszenie generowania świata modów z '{source}' do '{target}' (wymiar {dimension})..." ingest_failed = "Przenoszenie modów nie powiodło się: {reason}" diff --git a/core/src/main/resources/languages/pt_PT.toml b/core/src/main/resources/languages/pt_PT.toml index 2ff63c010..a658ef6f1 100644 --- a/core/src/main/resources/languages/pt_PT.toml +++ b/core/src/main/resources/languages/pt_PT.toml @@ -472,7 +472,7 @@ detail_dimensions = "Tipos de dimensão: {value}; level stems: {value2}" refreshing = "A capturar o catálogo de geração de mundo..." refreshed = "Catálogo de geração capturado em {duration}: {path}" refresh_failed = "Falha ao capturar o catálogo de geração: {reason}" -ingest_invalid = "Argumentos inválidos: {reason}. Utilização: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Argumentos inválidos: {reason}. Utilização: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "O pack '{pack}' não foi encontrado em {path}." ingest_started = "A importar a geração de mods de '{source}' para '{target}' (dimensão {dimension})..." ingest_failed = "A importação de mods falhou: {reason}" diff --git a/core/src/main/resources/languages/ru_RU.toml b/core/src/main/resources/languages/ru_RU.toml index 8643a6ee3..92eed6c5c 100644 --- a/core/src/main/resources/languages/ru_RU.toml +++ b/core/src/main/resources/languages/ru_RU.toml @@ -477,7 +477,7 @@ detail_dimensions = "Типы измерений: {value}; level stem: {value2}" refreshing = "Сбор каталога генерации мира..." refreshed = "Каталог генерации мира собран за {duration}: {path}" refresh_failed = "Не удалось собрать каталог генерации мира: {reason}" -ingest_invalid = "Неверные аргументы: {reason}. Использование: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Неверные аргументы: {reason}. Использование: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Пак '{pack}' не найден в {path}." ingest_started = "Перенос генерации мира модов из '{source}' в '{target}' (измерение {dimension})..." ingest_failed = "Не удалось перенести моды: {reason}" diff --git a/core/src/main/resources/languages/tr_TR.toml b/core/src/main/resources/languages/tr_TR.toml index 5f4a7af1b..fb0432b89 100644 --- a/core/src/main/resources/languages/tr_TR.toml +++ b/core/src/main/resources/languages/tr_TR.toml @@ -477,7 +477,7 @@ detail_dimensions = "Boyut türleri: {value}; level stem'ler: {value2}" refreshing = "Dünya oluşturma kataloğu toplanıyor..." refreshed = "Dünya oluşturma kataloğu {duration} içinde toplandı: {path}" refresh_failed = "Dünya oluşturma kataloğu toplanamadı: {reason}" -ingest_invalid = "Geçersiz argümanlar: {reason}. Kullanım: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Geçersiz argümanlar: {reason}. Kullanım: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "'{pack}' paketi {path} altında bulunamadı." ingest_started = "Mod dünya oluşturması '{source}' paketinden '{target}' paketine aktarılıyor (boyut {dimension})..." ingest_failed = "Mod aktarımı başarısız oldu: {reason}" diff --git a/core/src/main/resources/languages/vi_VI.toml b/core/src/main/resources/languages/vi_VI.toml index 352aa6abf..46c1e9c0f 100644 --- a/core/src/main/resources/languages/vi_VI.toml +++ b/core/src/main/resources/languages/vi_VI.toml @@ -475,7 +475,7 @@ detail_dimensions = "Loại chiều: {value}; level stem: {value2}" refreshing = "Đang thu thập danh mục tạo thế giới..." refreshed = "Đã thu thập danh mục tạo thế giới trong {duration}: {path}" refresh_failed = "Thu thập danh mục tạo thế giới thất bại: {reason}" -ingest_invalid = "Đối số không hợp lệ: {reason}. Cách dùng: /iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "Đối số không hợp lệ: {reason}. Cách dùng: /iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "Không tìm thấy gói '{pack}' trong {path}." ingest_started = "Đang đưa phần tạo thế giới của mod từ '{source}' vào '{target}' (chiều {dimension})..." ingest_failed = "Đưa mod vào thất bại: {reason}" diff --git a/core/src/main/resources/languages/zh_CN.toml b/core/src/main/resources/languages/zh_CN.toml index 671316164..824f0e453 100644 --- a/core/src/main/resources/languages/zh_CN.toml +++ b/core/src/main/resources/languages/zh_CN.toml @@ -475,7 +475,7 @@ detail_dimensions = "维度类型:{value};level stem:{value2}" refreshing = "正在采集世界生成目录..." refreshed = "世界生成目录已在 {duration} 内采集完成:{path}" refresh_failed = "采集世界生成目录失败:{reason}" -ingest_invalid = "参数无效:{reason}。用法:/iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "参数无效:{reason}。用法:/iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "在 {path} 下未找到包 '{pack}'。" ingest_started = "正在将模组世界生成从 '{source}' 导入 '{target}'(维度 {dimension})..." ingest_failed = "模组导入失败:{reason}" diff --git a/core/src/main/resources/languages/zh_TW.toml b/core/src/main/resources/languages/zh_TW.toml index f2e59517d..994c4e83b 100644 --- a/core/src/main/resources/languages/zh_TW.toml +++ b/core/src/main/resources/languages/zh_TW.toml @@ -475,7 +475,7 @@ detail_dimensions = "維度類型:{value};level stem:{value2}" refreshing = "正在擷取世界生成目錄..." refreshed = "世界生成目錄已在 {duration} 內擷取完成:{path}" refresh_failed = "擷取世界生成目錄失敗:{reason}" -ingest_invalid = "參數無效:{reason}。用法:/iris ingest mods [dimension=] [namespaces=] [replace=true]" +ingest_invalid = "參數無效:{reason}。用法:/iris ingest mods [dimension=] [namespaces=] [excludeRegions=] [replace=true]" ingest_pack_missing = "在 {path} 下找不到包 '{pack}'。" ingest_started = "正在將模組世界生成從 '{source}' 匯入 '{target}'(維度 {dimension})..." ingest_failed = "模組匯入失敗:{reason}" diff --git a/core/src/test/java/art/arcane/iris/pack/ingest/IngestBiomePlacementTest.java b/core/src/test/java/art/arcane/iris/pack/ingest/IngestBiomePlacementTest.java new file mode 100644 index 000000000..a2a5c845f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/pack/ingest/IngestBiomePlacementTest.java @@ -0,0 +1,154 @@ +/* + * 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.CatalogDimensionType; +import art.arcane.iris.pack.ingest.catalog.CatalogNamespace; +import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class IngestBiomePlacementTest { + private static final String MOD = "modpack"; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void modBiomesAvoidThemedRegionsAndFollowTheirCategory() throws Exception { + ModIngestReport report = ingest(Set.of()); + + assertEquals("temperate", region(report, "lavender_fields")); + assertEquals("temperate", region(report, "redwood_forest")); + assertTrue(reason(report, "redwood_forest").contains("temperate/forest")); + assertEquals("hot", region(report, "brushland")); + assertTrue(reason(report, "brushland").contains("hot/savanna")); + for (int index = 0; index < 6; index++) { + assertEquals("temperate", region(report, "prairie_" + index)); + } + } + + @Test + public void repeatedClimatesSpreadOverEquallyGoodTemplates() throws Exception { + ModIngestReport report = ingest(Set.of()); + + Set templates = new TreeSet<>(); + for (int index = 0; index < 6; index++) { + String reason = reason(report, "prairie_" + index); + templates.add(reason.substring(reason.indexOf("from template ") + "from template ".length())); + } + assertEquals(Set.of("temperate/meadow", "temperate/plains"), templates); + } + + @Test + public void excludedRegionsNeverReceiveModBiomes() throws Exception { + ModIngestReport report = ingest(Set.of("temperate")); + + for (ModIngestItem item : report.items()) { + if (item.kind().equals("biome") && item.tier() == ModIngestTier.ADOPTED) { + assertFalse(item.reason(), item.reason().contains("in region temperate ")); + } + } + assertThrows(IllegalArgumentException.class, () -> ingest(Set.of("missing"))); + } + + private ModIngestReport ingest(Set excludedRegions) throws IOException { + Path root = folder.newFolder().toPath(); + Path source = root.resolve("source"); + ModIngestFixture.write(source, "dimensions/overworld.json", """ + {"name": "Overworld", "environment": "NORMAL", "regions": ["temperate", "hot", "prismatic"], "dimensionHeight": {"min": -64, "max": 320}} + """); + ModIngestFixture.write(source, "regions/temperate.json", """ + {"name": "Temperate", "landBiomes": ["temperate/plains", "temperate/meadow", "temperate/forest"], "seaBiomes": ["temperate/ocean"]} + """); + ModIngestFixture.write(source, "regions/hot.json", """ + {"name": "Hot", "landBiomes": ["hot/desert", "hot/savanna"]} + """); + ModIngestFixture.write(source, "regions/prismatic.json", """ + {"name": "Prismatic", "rarity": 20, "landBiomes": ["prismatic/white", "prismatic/red", "prismatic/blue"]} + """); + biome(source, "temperate/plains", "minecraft:plains"); + biome(source, "temperate/meadow", "minecraft:plains"); + biome(source, "temperate/forest", "minecraft:forest"); + biome(source, "temperate/ocean", "minecraft:ocean"); + biome(source, "hot/desert", "minecraft:desert"); + biome(source, "hot/savanna", "minecraft:savanna"); + biome(source, "prismatic/white", "minecraft:plains"); + biome(source, "prismatic/red", "minecraft:plains"); + biome(source, "prismatic/blue", "minecraft:plains"); + Path target = root.resolve("packs").resolve("placed"); + return ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(MOD), excludedRegions, false)); + } + + private static void biome(Path pack, String key, String derivative) throws IOException { + ModIngestFixture.write(pack, "biomes/" + key + ".json", "{\"name\": \"" + key + "\", \"derivative\": \"" + derivative + "\"}"); + } + + private static WorldgenCatalog catalog() { + List biomes = new ArrayList<>(); + biomes.add(biome("minecraft:plains", 0.8F, 0.4F, "c:is_plains")); + biomes.add(biome("minecraft:forest", 0.7F, 0.8F, "minecraft:is_forest")); + biomes.add(biome("minecraft:desert", 2.0F, 0.0F, "c:is_desert")); + biomes.add(biome("minecraft:savanna", 1.2F, 0.4F, "minecraft:is_savanna")); + biomes.add(biome("minecraft:ocean", 0.5F, 0.5F, "minecraft:is_ocean")); + biomes.add(biome(MOD + ":lavender_fields", 0.8F, 0.5F, "c:is_plains")); + biomes.add(biome(MOD + ":redwood_forest", 0.8F, 0.6F, "minecraft:is_forest")); + biomes.add(biome(MOD + ":brushland", 1.3F, 0.3F, "minecraft:is_savanna")); + for (int index = 0; index < 6; index++) { + biomes.add(biome(MOD + ":prairie_" + index, 0.8F, 0.4F, "c:is_plains")); + } + return new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1000L, "fingerprint-placement", + List.of(new CatalogNamespace(MOD, "Mod Pack", "1.0.0")), biomes, List.of(), List.of(), + List.of(new CatalogDimensionType("minecraft:overworld", -64, 384, 384, null)), List.of()); + } + + private static CatalogBiome biome(String key, float temperature, float downfall, String categoryTag) { + List> steps = new ArrayList<>(11); + for (int index = 0; index < 11; index++) { + steps.add(List.of()); + } + return new CatalogBiome(key, temperature, downfall, true, List.of("minecraft:is_overworld", categoryTag), steps, List.of(), null); + } + + private static String region(ModIngestReport report, String biome) { + String reason = reason(report, biome); + int start = reason.indexOf("in region ") + "in region ".length(); + return reason.substring(start, reason.indexOf(' ', start)); + } + + private static String reason(ModIngestReport report, String biome) { + ModIngestItem item = ModIngestFixture.item(report, MOD + ":" + biome); + assertEquals(ModIngestTier.ADOPTED, item.tier()); + return item.reason(); + } +} diff --git a/core/src/test/java/art/arcane/iris/pack/ingest/IngestHeightBoundsTest.java b/core/src/test/java/art/arcane/iris/pack/ingest/IngestHeightBoundsTest.java index 9a226a4a0..dc24ffe07 100644 --- a/core/src/test/java/art/arcane/iris/pack/ingest/IngestHeightBoundsTest.java +++ b/core/src/test/java/art/arcane/iris/pack/ingest/IngestHeightBoundsTest.java @@ -73,7 +73,7 @@ public class IngestHeightBoundsTest { } """.formatted(heightFields)); Path target = root.resolve("packs").resolve("modded"); - ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog, Set.of(ModIngestFixture.MOD), false)); + ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog, Set.of(ModIngestFixture.MOD), Set.of(), false)); JsonObject dimension = ModIngestFixture.readJson(target.resolve("dimensions/modded.json")); JsonObject stone = ModIngestFixture.findByPaletteBlock(dimension.getAsJsonArray("deposits"), "examplemod:tin_ore"); assertNotNull(stone); diff --git a/core/src/test/java/art/arcane/iris/pack/ingest/IngestReservedNamespaceTest.java b/core/src/test/java/art/arcane/iris/pack/ingest/IngestReservedNamespaceTest.java index 8182df02c..db03f6c78 100644 --- a/core/src/test/java/art/arcane/iris/pack/ingest/IngestReservedNamespaceTest.java +++ b/core/src/test/java/art/arcane/iris/pack/ingest/IngestReservedNamespaceTest.java @@ -45,7 +45,7 @@ public class IngestReservedNamespaceTest { ModIngestFixture.write(root.resolve("packs").resolve("underworld"), "dimensions/underworld.json", "{\"name\": \"Underworld\"}"); Path target = root.resolve("packs").resolve("modded"); - ModIngestReport report = ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(), false)); + ModIngestReport report = ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(), Set.of(), false)); assertNull(ModIngestFixture.item(report, "irisworldgen:packs/6f76/dimensions/6f76/biomes/dunes")); assertNull(ModIngestFixture.item(report, "overworld:dunes")); @@ -63,7 +63,7 @@ public class IngestReservedNamespaceTest { for (String namespace : List.of("irisworldgen", "overworld")) { assertThrows(namespace, IllegalArgumentException.class, () -> ModIngester.ingest( - new ModIngestRequest(source, target, "overworld", catalog(), Set.of(namespace), false))); + new ModIngestRequest(source, target, "overworld", catalog(), Set.of(namespace), Set.of(), false))); } } diff --git a/core/src/test/java/art/arcane/iris/pack/ingest/ModIngestFixture.java b/core/src/test/java/art/arcane/iris/pack/ingest/ModIngestFixture.java index 70a11b2e9..a384b6346 100644 --- a/core/src/test/java/art/arcane/iris/pack/ingest/ModIngestFixture.java +++ b/core/src/test/java/art/arcane/iris/pack/ingest/ModIngestFixture.java @@ -320,7 +320,7 @@ final class ModIngestFixture { } static ModIngestRequest request(Path source, Path target, Set namespaces, boolean replace) { - return new ModIngestRequest(source, target, "overworld", catalog(), namespaces, replace); + return new ModIngestRequest(source, target, "overworld", catalog(), namespaces, Set.of(), replace); } static JsonObject readJson(Path file) throws IOException { diff --git a/core/src/test/java/art/arcane/iris/pack/ingest/ModIngesterPackTest.java b/core/src/test/java/art/arcane/iris/pack/ingest/ModIngesterPackTest.java index 4bfa22e54..ea1d3fa92 100644 --- a/core/src/test/java/art/arcane/iris/pack/ingest/ModIngesterPackTest.java +++ b/core/src/test/java/art/arcane/iris/pack/ingest/ModIngesterPackTest.java @@ -158,7 +158,7 @@ public class ModIngesterPackTest { Map first = snapshot(target); assertThrows(IOException.class, () -> ModIngester.ingest(new ModIngestRequest( - source, target, "missing", ModIngestFixture.catalog(), Set.of(ModIngestFixture.MOD), true))); + source, target, "missing", ModIngestFixture.catalog(), Set.of(ModIngestFixture.MOD), Set.of(), true))); assertEquals(first, snapshot(target)); assertEquals(List.of("modded"), directoryNames(root.resolve("packs"))); @@ -184,7 +184,7 @@ public class ModIngesterPackTest { ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false)); IOException failure = assertThrows(IOException.class, () -> ModIngester.ingest(new ModIngestRequest( - target, root.resolve("packs").resolve("again"), "modded", ModIngestFixture.catalog(), Set.of(), false))); + target, root.resolve("packs").resolve("again"), "modded", ModIngestFixture.catalog(), Set.of(), Set.of(), false))); assertTrue(failure.getMessage(), failure.getMessage().contains("original pack")); assertFalse(Files.exists(root.resolve("packs").resolve("again"))); @@ -218,7 +218,7 @@ public class ModIngesterPackTest { fixture.capturedAtEpochMillis(), fixture.contentFingerprint(), namespaces, fixture.biomes(), fixture.placedFeatures(), fixture.structures(), fixture.dimensionTypes(), fixture.levelStems()); - ModIngestReport report = ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog, Set.of(), false)); + ModIngestReport report = ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog, Set.of(), Set.of(), false)); assertEquals(List.of("examplemod", "othermod"), ModIngestFixture.strings(ModIngestFixture.readJson(target.resolve("ingest/manifest.json")).get("namespaces")));