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=<a,b> to keep mod biomes out of named regions; unknown region names are refused
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-14 21:26:22 -04:00
1 parent 8388194129
commit 0bc235afb6
34 files changed
+389 -66

No files matched your search

@@ -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<String> namespaces, boolean replace) {
static final List<String> OPTION_SUGGESTIONS = List.of("dimension=", "namespaces=", "replace=true");
record ModIngestArguments(String sourcePack, String targetPack, String dimension, Set<String> namespaces,
Set<String> excludedRegions, boolean replace) {
static final List<String> 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<String> regions(String raw) {
if (raw == null) {
return Set.of();
}
Set<String> 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;
@@ -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()),
@@ -169,7 +169,7 @@ final class ModdedCommandHelp {
));
SECTIONS.put("pk", SECTIONS.get("pack"));
SECTIONS.put("ingest", List.of(
Entry.command("mods", "<source> <target> [dimension=<key>] [namespaces=<a,b>] [replace=true]", ModdedCatalogMessages.HELP_INGEST_MODS)
Entry.command("mods", "<source> <target> [dimension=<key>] [namespaces=<a,b>] [excludeRegions=<a,b>] [replace=true]", ModdedCatalogMessages.HELP_INGEST_MODS)
));
SECTIONS.put("world", List.of(
Entry.command("enable", "<dimension> <pack|pack:dimensionKey> [seed|random]", ModdedHelpMessages.COMMAND_ENABLE_CREATE_AND_INJECT_A_PERSISTENT_IRIS_DIMENSION_AT_RUNTIME_DOWNLOADS_THE_PACK, "create"),
@@ -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 <source> <target> [dimension=<key>] "
+ "[namespaces=<a,b>] [replace=true]"
+ "[namespaces=<a,b>] [excludeRegions=<a,b>] [replace=true]"
);
public static final TextKey INGEST_PACK_MISSING = TextKey.of(
"iris.modded.catalog.ingest_pack_missing",
@@ -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"));
}
@@ -62,7 +62,7 @@ public class ModdedCatalogCommandsTest {
assertTrue(ModdedCommandHelp.documents("ingest", "mods"));
List<String> lines = ModdedCommandHelp.consoleLines("ingest");
assertTrue(lines.stream().anyMatch((String line) ->
line.startsWith("/iris ingest mods <source> <target> [dimension=<key>] [namespaces=<a,b>] [replace=true] - ")));
line.startsWith("/iris ingest mods <source> <target> [dimension=<key>] [namespaces=<a,b>] [excludeRegions=<a,b>] [replace=true] - ")));
}
private static CommandNode<CommandSourceStack> child(CommandNode<CommandSourceStack> parent, String name) {