Ordered converted ore hosts

- The worldgen catalog (format 2) records the members of every block tag referenced by a captured placed feature, so ore targets using mod block tags convert instead of passing through
- Ore conversion supports noise_based_count and noise_threshold_count placement counts
- Converted ores that produce another converted ore's host block are written ahead of it, so an ore whose host comes from a sibling ore finds that host during the deposit stage
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-14 22:37:28 -04:00
1 parent d5796b275e
commit 965e19801d
20 files changed
+445 -45

No files matched your search

@@ -27,6 +27,8 @@ import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -37,6 +39,8 @@ import java.util.TreeSet;
final class ModFeatureIngester {
private static final String VANILLA_NAMESPACE = "minecraft";
private static final String REPLACEABLE_BLOCKS = "replaceableBlocks";
private static final String PALETTE = "palette";
private final IngestPackModel model;
private final WorldgenCatalogIndex index;
@@ -96,7 +100,7 @@ final class ModFeatureIngester {
universe.addAll(model.reachableBiomes().values());
Set<String> caveKeys = caveKeys(adopted);
List<ModIngestItem> items = new ArrayList<>(plans.size());
for (FeaturePlan plan : plans.values()) {
for (FeaturePlan plan : hostProducersFirst(plans.values())) {
String namespace = WorldgenCatalogIndex.namespaceOf(plan.key());
Set<String> hosts = new TreeSet<>(plan.declaring());
hosts.retainAll(universe);
@@ -128,6 +132,57 @@ final class ModFeatureIngester {
return items;
}
static List<FeaturePlan> hostProducersFirst(Collection<FeaturePlan> plans) {
List<FeaturePlan> pending = new ArrayList<>(plans);
List<FeaturePlan> ordered = new ArrayList<>(pending.size());
while (!pending.isEmpty()) {
FeaturePlan next = pending.getFirst();
for (FeaturePlan candidate : pending) {
if (!waitsForHostProducer(candidate, pending)) {
next = candidate;
break;
}
}
pending.remove(next);
ordered.add(next);
}
return ordered;
}
private static boolean waitsForHostProducer(FeaturePlan consumer, List<FeaturePlan> pending) {
Set<String> hosts = depositBlocks(consumer, REPLACEABLE_BLOCKS);
if (hosts.isEmpty()) {
return false;
}
for (FeaturePlan producer : pending) {
if (producer != consumer && !Collections.disjoint(depositBlocks(producer, PALETTE), hosts)) {
return true;
}
}
return false;
}
private static Set<String> depositBlocks(FeaturePlan plan, String field) {
if (plan.deposits() == null) {
return Set.of();
}
Set<String> blocks = new TreeSet<>();
for (JsonObject deposit : plan.deposits()) {
JsonElement values = deposit.get(field);
if (values == null || !values.isJsonArray()) {
continue;
}
for (JsonElement value : values.getAsJsonArray()) {
if (value.isJsonPrimitive()) {
blocks.add(value.getAsString());
} else if (value.isJsonObject() && value.getAsJsonObject().has("block")) {
blocks.add(value.getAsJsonObject().get("block").getAsString());
}
}
}
return blocks;
}
private boolean treeInPackVanillaBiomes(FeaturePlan plan) {
Optional<CatalogPlacedFeature> feature = index.placedFeature(plan.key());
if (feature.isEmpty() || !trees.isTree(feature.get())) {
@@ -149,7 +204,8 @@ final class ModFeatureIngester {
CatalogPlacedFeature placed = feature.get();
try {
if (OreDepositConverter.handles(placed)) {
List<JsonObject> deposits = OreDepositConverter.convert(placed, model.verticalBounds(), model.packMinY());
List<JsonObject> deposits = OreDepositConverter.convert(placed, model.verticalBounds(), model.packMinY(),
index.catalog().blockTags());
return new FeaturePlan(key, declaring, steps, deposits, null, "");
}
if (PatchDecoratorConverter.handles(placed)) {
@@ -25,6 +25,7 @@ import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -42,7 +43,8 @@ final class OreDepositConverter {
return ORE_TYPES.contains(VanillaJson.namespaced(feature.featureType()));
}
static List<JsonObject> convert(CatalogPlacedFeature feature, VanillaHeights.VerticalBounds bounds, int packMinY) throws IngestConversionException {
static List<JsonObject> convert(CatalogPlacedFeature feature, VanillaHeights.VerticalBounds bounds, int packMinY,
Map<String, List<String>> blockTags) throws IngestConversionException {
JsonObject config = VanillaJson.featureConfig(feature.config());
Placement placement = placement(VanillaJson.placement(feature.placement()), bounds);
int size = VanillaJson.integer(config.get("size"), "ore size");
@@ -52,7 +54,7 @@ final class OreDepositConverter {
double discard = VanillaJson.number(config, "discard_chance_on_air_exposure", 0D, "ore");
String shape = VanillaJson.namespaced(feature.featureType()).equals("minecraft:scattered_ore") ? "VANILLA_SCATTERED" : "VANILLA_ELLIPSOID";
List<JsonObject> deposits = new ArrayList<>();
for (Target target : targets(VanillaJson.array(config.get("targets"), "ore targets"))) {
for (Target target : targets(VanillaJson.array(config.get("targets"), "ore targets"), blockTags)) {
JsonObject deposit = new JsonObject();
deposit.addProperty("minHeight", placement.band().min() - packMinY);
deposit.addProperty("maxHeight", placement.band().max() - packMinY);
@@ -84,13 +86,13 @@ final class OreDepositConverter {
return deposits;
}
private static List<Target> targets(JsonArray targets) throws IngestConversionException {
private static List<Target> targets(JsonArray targets, Map<String, List<String>> blockTags) throws IngestConversionException {
List<Target> result = new ArrayList<>(targets.size());
SortedSet<String> claimed = new TreeSet<>();
for (JsonElement entry : targets) {
JsonObject target = VanillaJson.object(entry, "ore target");
JsonObject block = VanillaBlockStates.irisBlock(target.get("state"));
VanillaRuleTests.HostSet hosts = VanillaRuleTests.hosts(target.get("target"));
VanillaRuleTests.HostSet hosts = VanillaRuleTests.hosts(target.get("target"), blockTags);
if (hosts.any()) {
if (!claimed.isEmpty()) {
throw new IngestConversionException("ore target accepting any block follows restricted targets");
@@ -125,6 +127,11 @@ final class OreDepositConverter {
}
case "minecraft:rarity_filter" -> spawnChance /= Math.max(1D, VanillaJson.number(object.get("chance"), "rarity_filter.chance"));
case "minecraft:random_chance" -> spawnChance *= Math.max(0D, Math.min(1D, VanillaJson.number(object.get("chance"), "random_chance.chance")));
case "minecraft:noise_based_count", "minecraft:noise_threshold_count" -> {
double mean = VanillaPlacementChain.noiseCountMean(object);
minCount = Math.min(MAX_PER_CHUNK, Math.round(minCount * mean));
maxCount = Math.min(MAX_PER_CHUNK, Math.round(maxCount * mean));
}
case "minecraft:in_square", "minecraft:biome" -> {
}
case "minecraft:height_range" -> band = VanillaHeights.band(object.get("height"), bounds);
@@ -166,6 +166,14 @@ final class VanillaPlacementChain {
return values.get(0).getAsInt() == 0 && values.get(1).getAsInt() == -1 && values.get(2).getAsInt() == 0;
}
static double noiseCountMean(JsonObject object) throws IngestConversionException {
if (VanillaJson.type(object, "type").equals("minecraft:noise_threshold_count")) {
return (VanillaJson.number(object.get("below_noise"), "noise_threshold_count.below_noise")
+ VanillaJson.number(object.get("above_noise"), "noise_threshold_count.above_noise")) / 2D;
}
return noiseBasedMean(object);
}
private static double noiseBasedMean(JsonObject object) throws IngestConversionException {
double ratio = VanillaJson.number(object.get("noise_to_count_ratio"), "noise_based_count.noise_to_count_ratio");
double offset = VanillaJson.number(object, "noise_offset", 0D, "noise_based_count");
@@ -42,32 +42,32 @@ final class VanillaRuleTests {
private VanillaRuleTests() {
}
static HostSet hosts(JsonElement ruleTest) throws IngestConversionException {
static HostSet hosts(JsonElement ruleTest, Map<String, List<String>> blockTags) throws IngestConversionException {
JsonObject object = VanillaJson.object(ruleTest, "ore target predicate");
String type = VanillaJson.type(object, "predicate_type");
return switch (type) {
case "minecraft:always_true" -> HostSet.ANY;
case "minecraft:tag_match" -> tag(VanillaJson.namespaced(VanillaJson.string(object.get("tag"), "tag_match.tag")));
case "minecraft:tag_match" -> tag(VanillaJson.namespaced(VanillaJson.string(object.get("tag"), "tag_match.tag")), blockTags);
case "minecraft:block_match" -> HostSet.of(Set.of(VanillaJson.namespaced(VanillaJson.string(object.get("block"), "block_match.block"))));
case "minecraft:blockstate_match" -> HostSet.of(Set.of(VanillaBlockStates.irisBlock(object.get("block_state")).get("block").getAsString()));
case "minecraft:any_of" -> anyOf(rules(object));
case "minecraft:all_of" -> allOf(rules(object));
case "minecraft:any_of" -> anyOf(rules(object), blockTags);
case "minecraft:all_of" -> allOf(rules(object), blockTags);
default -> throw new IngestConversionException("unsupported ore target predicate " + (type.isEmpty() ? object.toString() : type));
};
}
private static HostSet tag(String tag) throws IngestConversionException {
List<String> blocks = BLOCK_TAGS.get(tag);
private static HostSet tag(String tag, Map<String, List<String>> blockTags) throws IngestConversionException {
List<String> blocks = blockTags.containsKey(tag) ? blockTags.get(tag) : BLOCK_TAGS.get(tag);
if (blocks == null) {
throw new IngestConversionException("ore target uses block tag " + tag + " whose members are not known offline");
throw new IngestConversionException("ore target uses block tag " + tag + " whose members the catalog did not capture");
}
return HostSet.of(Set.copyOf(blocks));
}
private static HostSet anyOf(List<JsonElement> rules) throws IngestConversionException {
private static HostSet anyOf(List<JsonElement> rules, Map<String, List<String>> blockTags) throws IngestConversionException {
SortedSet<String> union = new TreeSet<>();
for (JsonElement rule : rules) {
HostSet hosts = hosts(rule);
HostSet hosts = hosts(rule, blockTags);
if (hosts.any()) {
return HostSet.ANY;
}
@@ -76,7 +76,7 @@ final class VanillaRuleTests {
return HostSet.of(union);
}
private static HostSet allOf(List<JsonElement> rules) throws IngestConversionException {
private static HostSet allOf(List<JsonElement> rules, Map<String, List<String>> blockTags) throws IngestConversionException {
HostSet positive = null;
SortedSet<String> negative = new TreeSet<>();
for (JsonElement rule : rules) {
@@ -86,14 +86,14 @@ final class VanillaRuleTests {
continue;
}
if (type.equals("minecraft:not")) {
HostSet excluded = hosts(ruleObject.get("rule"));
HostSet excluded = hosts(ruleObject.get("rule"), blockTags);
if (excluded.any()) {
throw new IngestConversionException("ore target predicate never matches");
}
negative.addAll(excluded.blocks());
continue;
}
positive = intersect(positive, hosts(ruleObject));
positive = intersect(positive, hosts(ruleObject, blockTags));
}
if (positive == null) {
throw new IngestConversionException("ore target all_of has no block constraint");
@@ -20,6 +20,7 @@
package art.arcane.iris.pack.ingest.catalog;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public record WorldgenCatalog(
@@ -33,8 +34,9 @@ public record WorldgenCatalog(
List<CatalogPlacedFeature> placedFeatures,
List<CatalogStructure> structures,
List<CatalogDimensionType> dimensionTypes,
List<CatalogLevelStem> levelStems) {
public static final int FORMAT = 1;
List<CatalogLevelStem> levelStems,
Map<String, List<String>> blockTags) {
public static final int FORMAT = 2;
public WorldgenCatalog {
Objects.requireNonNull(minecraftVersion, "minecraftVersion");
@@ -46,5 +48,6 @@ public record WorldgenCatalog(
structures = List.copyOf(Objects.requireNonNull(structures, "structures"));
dimensionTypes = List.copyOf(Objects.requireNonNull(dimensionTypes, "dimensionTypes"));
levelStems = List.copyOf(Objects.requireNonNull(levelStems, "levelStems"));
blockTags = Map.copyOf(Objects.requireNonNull(blockTags, "blockTags"));
}
}
@@ -74,6 +74,10 @@ public final class WorldgenCatalogIndex {
return Optional.ofNullable(dimensionTypes.get(key));
}
public Optional<List<String>> blockTag(String key) {
return Optional.ofNullable(catalog.blockTags().get(key));
}
public Optional<CatalogLevelStem> levelStem(String key) {
return Optional.ofNullable(levelStems.get(key));
}