Merged ingest placement caps

- Templates host at most five land or three sea, shore and cave adoptions
- Themed regions only take mod biomes no common region accepts
This commit is contained in:
Brian Neumann-Fopiano
2026-10-15 10:11:03 -04:00
4 changed files with 239 additions and 30 deletions
@@ -19,17 +19,19 @@
package art.arcane.iris.pack.ingest;
enum BiomeRole {
LAND("landBiomes", "land"),
SEA("seaBiomes", "sea"),
SHORE("shoreBiomes", "shore"),
CAVE("caveBiomes", "cave");
LAND("landBiomes", "land", 5),
SEA("seaBiomes", "sea", 3),
SHORE("shoreBiomes", "shore", 3),
CAVE("caveBiomes", "cave", 3);
private final String poolField;
private final String label;
private final int templateCap;
BiomeRole(String poolField, String label) {
BiomeRole(String poolField, String label, int templateCap) {
this.poolField = poolField;
this.label = label;
this.templateCap = templateCap;
}
String poolField() {
@@ -39,4 +41,8 @@ enum BiomeRole {
String label() {
return label;
}
int templateCap() {
return templateCap;
}
}
@@ -45,7 +45,6 @@ final class ModBiomeAdopter {
private static final double REGION_CLIMATE_WEIGHT = 0.25D;
private static final double REGION_CATEGORY_WEIGHT = 0.25D;
private static final double REUSE_WEIGHT = 0.05D;
private static final double THEMED_REGION_PENALTY = 0.6D;
private static final double BARE_SURFACE_PENALTY = 3D;
private static final int THEMED_RARITY_FACTOR = 5;
@@ -77,8 +76,7 @@ final class ModBiomeAdopter {
boolean ownsVegetation = ownsVegetation(biome, namespace);
Placement placement = place(role, IngestClimate.of(biome), BiomeCategories.of(biome.tags()), ownsVegetation);
if (placement == null) {
items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "",
"no region in this dimension accepts a " + role.label() + " biome"));
items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "", unplacedReason(role)));
continue;
}
IngestPackModel.RegionProfile region = placement.region();
@@ -103,21 +101,30 @@ final class ModBiomeAdopter {
}
private Placement place(BiomeRole role, IngestClimate climate, Set<String> categories, boolean ownsVegetation) throws IOException {
Placement common = bestPlacement(role, climate, categories, ownsVegetation, false);
return common != null ? common : bestPlacement(role, climate, categories, ownsVegetation, true);
}
private Placement bestPlacement(BiomeRole role, IngestClimate climate, Set<String> categories, boolean ownsVegetation,
boolean themed) throws IOException {
Placement best = null;
double bestScore = Double.POSITIVE_INFINITY;
for (IngestPackModel.RegionProfile region : model.regions()) {
if (excludedRegions.contains(region.key())) {
if (excludedRegions.contains(region.key()) || themedRegions.contains(region.key()) != themed) {
continue;
}
IngestClimate regionClimate = regionClimate(region, role, categories);
double regionPenalty = (themedRegions.contains(region.key()) ? THEMED_REGION_PENALTY : 0D)
+ (regionClimate == null ? 0D : REGION_CLIMATE_WEIGHT * climate.distance(regionClimate))
double regionPenalty = (regionClimate == null ? 0D : REGION_CLIMATE_WEIGHT * climate.distance(regionClimate))
+ REGION_CATEGORY_WEIGHT * (1D - regionAffinity(region, role, categories));
for (String template : region.pool(role)) {
int uses = templateUses.getOrDefault(template, 0);
if (uses >= role.templateCap()) {
continue;
}
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)
+ REUSE_WEIGHT * uses
+ (ownsVegetation ? BARE_SURFACE_PENALTY * (1D - model.plantableShare(template)) : 0D)
+ regionPenalty;
if (best == null || score < bestScore) {
@@ -129,6 +136,15 @@ final class ModBiomeAdopter {
return best;
}
private String unplacedReason(BiomeRole role) {
for (IngestPackModel.RegionProfile region : model.regions()) {
if (!excludedRegions.contains(region.key()) && !region.pool(role).isEmpty()) {
return "every " + role.label() + " template already hosts " + role.templateCap() + " adopted biomes";
}
}
return "no region in this dimension accepts a " + role.label() + " biome";
}
private IngestClimate regionClimate(IngestPackModel.RegionProfile region, BiomeRole role, Set<String> categories) {
if (role != BiomeRole.LAND || categories.isEmpty()) {
return region.climate();
@@ -33,7 +33,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class IngestRegionAffinityTest {
private static final String MOD = "modisles";
@@ -43,18 +44,41 @@ public class IngestRegionAffinityTest {
@Test
public void modBiomeJoinsTheRegionWhoseTemplatesShareItsCategory() throws Exception {
Path root = folder.getRoot().toPath();
ModIngestReport report = ingest(1, false);
assertEquals("mushroom", region(report, "mirage_isles"));
}
@Test
public void rareThemedRegionNeverHostsAModBiomeWhileACommonRegionAcceptsIt() throws Exception {
ModIngestReport report = ingest(15, false);
String region = region(report, "mirage_isles");
assertNotEquals("mushroom", region);
assertNotEquals("magnetic", region);
}
@Test
public void themedRegionHostsAModBiomeWhenNoCommonRegionAcceptsItsRole() throws Exception {
ModIngestReport report = ingest(15, true);
assertEquals("mushroom", region(report, "spore_hollow"));
}
private ModIngestReport ingest(int themedRarity, boolean themedCaves) 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", "cold", "wet", "magnetic", "mushroom"],
"dimensionHeight": {"min": -64, "max": 320}}
""");
region(source, "temperate", 1, List.of("temperate/plains"));
region(source, "hot", 1, List.of("hot/desert"));
region(source, "cold", 1, List.of("cold/snow"));
region(source, "wet", 1, List.of("wet/swamp"));
region(source, "magnetic", 15, List.of("magnetic/mycelium", "magnetic/plains", "magnetic/frozen", "magnetic/marsh"));
region(source, "mushroom", 15, List.of("mushroom/fields", "mushroom/forest", "mushroom/crimson", "mushroom/warped"));
region(source, "temperate", 1, List.of("temperate/plains"), List.of());
region(source, "hot", 1, List.of("hot/desert"), List.of());
region(source, "cold", 1, List.of("cold/snow"), List.of());
region(source, "wet", 1, List.of("wet/swamp"), List.of());
region(source, "magnetic", themedRarity, List.of("magnetic/mycelium", "magnetic/plains", "magnetic/frozen", "magnetic/marsh"), List.of());
region(source, "mushroom", themedRarity, List.of("mushroom/fields", "mushroom/forest", "mushroom/crimson", "mushroom/warped"),
themedCaves ? List.of("mushroom/caves") : List.of());
biome(source, "temperate/plains", "minecraft:plains");
biome(source, "hot/desert", "minecraft:desert");
biome(source, "cold/snow", "minecraft:snowy_plains");
@@ -67,20 +91,30 @@ public class IngestRegionAffinityTest {
biome(source, "mushroom/forest", "minecraft:mushroom_fields");
biome(source, "mushroom/crimson", "minecraft:crimson_forest");
biome(source, "mushroom/warped", "minecraft:warped_forest");
biome(source, "mushroom/caves", "minecraft:lush_caves");
Path target = root.resolve("packs").resolve("isles");
ModIngestReport report = ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(MOD), Set.of(), false));
String reason = ModIngestFixture.item(report, MOD + ":mirage_isles").reason();
assertTrue(reason, reason.contains("in region mushroom "));
return ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(MOD), Set.of(), false));
}
private static void region(Path pack, String key, int rarity, List<String> land) throws IOException {
StringBuilder pool = new StringBuilder();
for (String biome : land) {
pool.append(pool.isEmpty() ? "" : ", ").append('"').append(biome).append('"');
private static String region(ModIngestReport report, String biome) {
ModIngestItem item = ModIngestFixture.item(report, MOD + ":" + biome);
assertEquals(item.reason(), ModIngestTier.ADOPTED, item.tier());
String reason = item.reason();
int start = reason.indexOf("in region ") + "in region ".length();
return reason.substring(start, reason.indexOf(' ', start));
}
private static void region(Path pack, String key, int rarity, List<String> land, List<String> caves) throws IOException {
ModIngestFixture.write(pack, "regions/" + key + ".json", "{\"name\": \"" + key + "\", \"rarity\": " + rarity
+ ", \"landBiomes\": " + quoted(land) + ", \"caveBiomes\": " + quoted(caves) + "}");
}
private static String quoted(List<String> keys) {
StringBuilder array = new StringBuilder("[");
for (String key : keys) {
array.append(array.length() == 1 ? "" : ", ").append('"').append(key).append('"');
}
ModIngestFixture.write(pack, "regions/" + key + ".json", "{\"name\": \"" + key + "\", \"rarity\": " + rarity + ", \"landBiomes\": [" + pool + "]}");
return array.append(']').toString();
}
private static void biome(Path pack, String key, String derivative) throws IOException {
@@ -96,7 +130,9 @@ public class IngestRegionAffinityTest {
biomes.add(biome("minecraft:mushroom_fields", 0.9F, 1.0F, "c:is_mushroom"));
biomes.add(biome("minecraft:crimson_forest", 2.0F, 0.0F, "c:is_nether"));
biomes.add(biome("minecraft:warped_forest", 2.0F, 0.0F, "c:is_nether"));
biomes.add(biome("minecraft:lush_caves", 0.5F, 0.5F, "c:is_cave"));
biomes.add(biome(MOD + ":mirage_isles", 0.7F, 0.8F, "c:is_mushroom"));
biomes.add(biome(MOD + ":spore_hollow", 0.9F, 1.0F, "c:is_cave"));
return new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1L, "affinity",
List.of(new CatalogNamespace(MOD, "Mod Isles", "1")), biomes, List.of(), List.of(),
List.of(new CatalogDimensionType("minecraft:overworld", -64, 384, 384, null)), List.of(), Map.of());
@@ -0,0 +1,151 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.pack.ingest;
import art.arcane.iris.pack.ingest.catalog.CatalogBiome;
import art.arcane.iris.pack.ingest.catalog.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.Map;
import java.util.Set;
import java.util.TreeMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IngestTemplateCapTest {
private static final String MOD = "modcaps";
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void caveTemplatesHostAtMostThreeModBiomesAndOverflowMovesToTheNextBestTemplate() throws Exception {
ModIngestReport report = ingest(7, 0);
Map<String, Integer> templates = templateUses(report);
assertEquals(Integer.valueOf(3), templates.get("caves/lush"));
for (Map.Entry<String, Integer> template : templates.entrySet()) {
assertTrue(template.toString(), template.getValue() <= 3);
}
for (int index = 0; index < 7; index++) {
assertEquals(ModIngestTier.ADOPTED, ModIngestFixture.item(report, MOD + ":grotto_" + index).tier());
}
}
@Test
public void landTemplatesHostAtMostFiveModBiomesAndOverflowMovesToTheNextBestTemplate() throws Exception {
ModIngestReport report = ingest(0, 7);
Map<String, Integer> templates = templateUses(report);
assertEquals(Integer.valueOf(5), templates.get("wet/marsh"));
assertEquals(Integer.valueOf(2), templates.get("dry/plains"));
}
@Test
public void modBiomeIsSkippedWithAReasonWhenEveryCompatibleTemplateIsFull() throws Exception {
ModIngestReport report = ingest(10, 0);
int adopted = 0;
List<ModIngestItem> skipped = new ArrayList<>();
for (int index = 0; index < 10; index++) {
ModIngestItem item = ModIngestFixture.item(report, MOD + ":grotto_" + index);
if (item.tier() == ModIngestTier.ADOPTED) {
adopted++;
} else {
skipped.add(item);
}
}
assertEquals(9, adopted);
assertEquals(1, skipped.size());
assertEquals(ModIngestTier.SKIPPED, skipped.getFirst().tier());
assertTrue(skipped.getFirst().reason(), skipped.getFirst().reason().contains("already hosts 3 adopted biomes"));
}
private ModIngestReport ingest(int caves, int marshes) throws IOException {
Path root = folder.newFolder().toPath();
Path source = root.resolve("source");
ModIngestFixture.write(source, "dimensions/overworld.json", """
{"name": "Overworld", "environment": "NORMAL", "regions": ["wet", "dry"], "dimensionHeight": {"min": -64, "max": 320}}
""");
ModIngestFixture.write(source, "regions/wet.json", """
{"name": "Wet", "landBiomes": ["wet/marsh"], "caveBiomes": ["caves/lush"]}
""");
ModIngestFixture.write(source, "regions/dry.json", """
{"name": "Dry", "landBiomes": ["dry/plains"], "caveBiomes": ["caves/drip", "caves/sand"]}
""");
biome(source, "wet/marsh", "minecraft:swamp");
biome(source, "dry/plains", "minecraft:plains");
biome(source, "caves/lush", "minecraft:lush_caves");
biome(source, "caves/drip", "minecraft:dripstone_caves");
biome(source, "caves/sand", "minecraft:dripstone_caves");
Path target = root.resolve("packs").resolve("capped");
return ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(caves, marshes), Set.of(MOD), Set.of(), false));
}
private static Map<String, Integer> templateUses(ModIngestReport report) {
Map<String, Integer> uses = new TreeMap<>();
for (ModIngestItem item : report.items()) {
if (item.kind().equals("biome") && item.tier() == ModIngestTier.ADOPTED) {
String reason = item.reason();
uses.merge(reason.substring(reason.indexOf("from template ") + "from template ".length()), 1, Integer::sum);
}
}
return uses;
}
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(int caves, int marshes) {
List<CatalogBiome> biomes = new ArrayList<>();
biomes.add(biome("minecraft:swamp", 0.8F, 0.9F, List.of("c:is_swamp")));
biomes.add(biome("minecraft:plains", 0.8F, 0.4F, List.of("c:is_plains")));
biomes.add(biome("minecraft:lush_caves", 0.5F, 0.5F, List.of("c:is_cave", "c:is_lush")));
biomes.add(biome("minecraft:dripstone_caves", 2.0F, 0.0F, List.of("c:is_cave")));
for (int index = 0; index < caves; index++) {
biomes.add(biome(MOD + ":grotto_" + index, 0.5F, 0.5F, List.of("c:is_cave", "c:is_lush")));
}
for (int index = 0; index < marshes; index++) {
biomes.add(biome(MOD + ":marsh_" + index, 0.8F, 0.9F, List.of("c:is_swamp")));
}
return new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1000L, "fingerprint-caps",
List.of(new CatalogNamespace(MOD, "Mod Caps", "1.0.0")), biomes, List.of(), List.of(),
List.of(new CatalogDimensionType("minecraft:overworld", -64, 384, 384, null)), List.of(), Map.of());
}
private static CatalogBiome biome(String key, float temperature, float downfall, List<String> categoryTags) {
List<List<String>> steps = new ArrayList<>(11);
for (int index = 0; index < 11; index++) {
steps.add(List.of());
}
List<String> tags = new ArrayList<>(categoryTags);
tags.add("minecraft:is_overworld");
return new CatalogBiome(key, temperature, downfall, true, tags, steps, List.of(), null);
}
}