Capped adoptions per template

- Each pack biome now serves as the template for at most five adopted land biomes and three adopted sea, shore, or cave biomes
- A mod biome whose best template is full falls back to the next best template under the cap
- A mod biome that finds every compatible template full is skipped with a reason naming the cap instead of being dropped
This commit is contained in:
Brian Neumann-Fopiano
2026-10-15 09:13:55 -04:00
parent 0ca33068f3
commit 523cc7e7be
3 changed files with 177 additions and 8 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;
}
}
@@ -77,8 +77,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();
@@ -114,10 +113,14 @@ final class ModBiomeAdopter {
+ (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 +132,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();
@@ -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);
}
}