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 committed 2026-10-15 10:11:03 -04:00
commit 969ec7fde3
4 files changed
+239 -30

No files matched your search

@@ -19,17 +19,19 @@
package art.arcane.iris.pack.ingest; package art.arcane.iris.pack.ingest;
enum BiomeRole { enum BiomeRole {
LAND("landBiomes", "land"), LAND("landBiomes", "land", 5),
SEA("seaBiomes", "sea"), SEA("seaBiomes", "sea", 3),
SHORE("shoreBiomes", "shore"), SHORE("shoreBiomes", "shore", 3),
CAVE("caveBiomes", "cave"); CAVE("caveBiomes", "cave", 3);
private final String poolField; private final String poolField;
private final String label; private final String label;
private final int templateCap;
BiomeRole(String poolField, String label) { BiomeRole(String poolField, String label, int templateCap) {
this.poolField = poolField; this.poolField = poolField;
this.label = label; this.label = label;
this.templateCap = templateCap;
} }
String poolField() { String poolField() {
@@ -39,4 +41,8 @@ enum BiomeRole {
String label() { String label() {
return 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_CLIMATE_WEIGHT = 0.25D;
private static final double REGION_CATEGORY_WEIGHT = 0.25D; private static final double REGION_CATEGORY_WEIGHT = 0.25D;
private static final double REUSE_WEIGHT = 0.05D; 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 double BARE_SURFACE_PENALTY = 3D;
private static final int THEMED_RARITY_FACTOR = 5; private static final int THEMED_RARITY_FACTOR = 5;
@@ -77,8 +76,7 @@ final class ModBiomeAdopter {
boolean ownsVegetation = ownsVegetation(biome, namespace); boolean ownsVegetation = ownsVegetation(biome, namespace);
Placement placement = place(role, IngestClimate.of(biome), BiomeCategories.of(biome.tags()), ownsVegetation); Placement placement = place(role, IngestClimate.of(biome), BiomeCategories.of(biome.tags()), ownsVegetation);
if (placement == null) { if (placement == null) {
items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "", items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "", unplacedReason(role)));
"no region in this dimension accepts a " + role.label() + " biome"));
continue; continue;
} }
IngestPackModel.RegionProfile region = placement.region(); 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 { 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; Placement best = null;
double bestScore = Double.POSITIVE_INFINITY; double bestScore = Double.POSITIVE_INFINITY;
for (IngestPackModel.RegionProfile region : model.regions()) { for (IngestPackModel.RegionProfile region : model.regions()) {
if (excludedRegions.contains(region.key())) { if (excludedRegions.contains(region.key()) || themedRegions.contains(region.key()) != themed) {
continue; continue;
} }
IngestClimate regionClimate = regionClimate(region, role, categories); IngestClimate regionClimate = regionClimate(region, role, categories);
double regionPenalty = (themedRegions.contains(region.key()) ? THEMED_REGION_PENALTY : 0D) double regionPenalty = (regionClimate == null ? 0D : REGION_CLIMATE_WEIGHT * climate.distance(regionClimate))
+ (regionClimate == null ? 0D : REGION_CLIMATE_WEIGHT * climate.distance(regionClimate))
+ REGION_CATEGORY_WEIGHT * (1D - regionAffinity(region, role, categories)); + REGION_CATEGORY_WEIGHT * (1D - regionAffinity(region, role, categories));
for (String template : region.pool(role)) { for (String template : region.pool(role)) {
int uses = templateUses.getOrDefault(template, 0);
if (uses >= role.templateCap()) {
continue;
}
IngestClimate templateClimate = model.biomeClimate(template); IngestClimate templateClimate = model.biomeClimate(template);
double score = climate.distance(templateClimate == null ? region.climate() : templateClimate) double score = climate.distance(templateClimate == null ? region.climate() : templateClimate)
+ CATEGORY_WEIGHT * BiomeCategories.distance(categories, model.biomeCategories(template)) + 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) + (ownsVegetation ? BARE_SURFACE_PENALTY * (1D - model.plantableShare(template)) : 0D)
+ regionPenalty; + regionPenalty;
if (best == null || score < bestScore) { if (best == null || score < bestScore) {
@@ -129,6 +136,15 @@ final class ModBiomeAdopter {
return best; 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) { private IngestClimate regionClimate(IngestPackModel.RegionProfile region, BiomeRole role, Set<String> categories) {
if (role != BiomeRole.LAND || categories.isEmpty()) { if (role != BiomeRole.LAND || categories.isEmpty()) {
return region.climate(); return region.climate();
@@ -33,7 +33,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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 { public class IngestRegionAffinityTest {
private static final String MOD = "modisles"; private static final String MOD = "modisles";
@@ -43,18 +44,41 @@ public class IngestRegionAffinityTest {
@Test @Test
public void modBiomeJoinsTheRegionWhoseTemplatesShareItsCategory() throws Exception { 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"); Path source = root.resolve("source");
ModIngestFixture.write(source, "dimensions/overworld.json", """ ModIngestFixture.write(source, "dimensions/overworld.json", """
{"name": "Overworld", "environment": "NORMAL", "regions": ["temperate", "hot", "cold", "wet", "magnetic", "mushroom"], {"name": "Overworld", "environment": "NORMAL", "regions": ["temperate", "hot", "cold", "wet", "magnetic", "mushroom"],
"dimensionHeight": {"min": -64, "max": 320}} "dimensionHeight": {"min": -64, "max": 320}}
"""); """);
region(source, "temperate", 1, List.of("temperate/plains")); region(source, "temperate", 1, List.of("temperate/plains"), List.of());
region(source, "hot", 1, List.of("hot/desert")); region(source, "hot", 1, List.of("hot/desert"), List.of());
region(source, "cold", 1, List.of("cold/snow")); region(source, "cold", 1, List.of("cold/snow"), List.of());
region(source, "wet", 1, List.of("wet/swamp")); region(source, "wet", 1, List.of("wet/swamp"), List.of());
region(source, "magnetic", 15, List.of("magnetic/mycelium", "magnetic/plains", "magnetic/frozen", "magnetic/marsh")); region(source, "magnetic", themedRarity, List.of("magnetic/mycelium", "magnetic/plains", "magnetic/frozen", "magnetic/marsh"), List.of());
region(source, "mushroom", 15, List.of("mushroom/fields", "mushroom/forest", "mushroom/crimson", "mushroom/warped")); 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, "temperate/plains", "minecraft:plains");
biome(source, "hot/desert", "minecraft:desert"); biome(source, "hot/desert", "minecraft:desert");
biome(source, "cold/snow", "minecraft:snowy_plains"); biome(source, "cold/snow", "minecraft:snowy_plains");
@@ -67,20 +91,30 @@ public class IngestRegionAffinityTest {
biome(source, "mushroom/forest", "minecraft:mushroom_fields"); biome(source, "mushroom/forest", "minecraft:mushroom_fields");
biome(source, "mushroom/crimson", "minecraft:crimson_forest"); biome(source, "mushroom/crimson", "minecraft:crimson_forest");
biome(source, "mushroom/warped", "minecraft:warped_forest"); biome(source, "mushroom/warped", "minecraft:warped_forest");
biome(source, "mushroom/caves", "minecraft:lush_caves");
Path target = root.resolve("packs").resolve("isles"); Path target = root.resolve("packs").resolve("isles");
return ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(MOD), Set.of(), false));
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 "));
} }
private static void region(Path pack, String key, int rarity, List<String> land) throws IOException { private static String region(ModIngestReport report, String biome) {
StringBuilder pool = new StringBuilder(); ModIngestItem item = ModIngestFixture.item(report, MOD + ":" + biome);
for (String biome : land) { assertEquals(item.reason(), ModIngestTier.ADOPTED, item.tier());
pool.append(pool.isEmpty() ? "" : ", ").append('"').append(biome).append('"'); 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 { 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: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: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: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 + ":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", 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 CatalogNamespace(MOD, "Mod Isles", "1")), biomes, List.of(), List.of(),
List.of(new CatalogDimensionType("minecraft:overworld", -64, 384, 384, null)), List.of(), Map.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);
}
}