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

@@ -37,12 +37,17 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenSpawn;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenStructure;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
public final class WorldgenCatalogAssembler {
private static final String MINECRAFT = "minecraft";
@@ -58,16 +63,57 @@ public final class WorldgenCatalogAssembler {
List<CatalogStructure> structures = structures(snapshot.structures());
List<CatalogDimensionType> dimensionTypes = dimensionTypes(snapshot.dimensionTypes());
List<CatalogLevelStem> levelStems = levelStems(snapshot.levelStems());
Map<String, List<String>> blockTags = referencedBlockTags(placedFeatures, snapshot.blockTags());
Map<String, List<String>> registryKeys = new LinkedHashMap<>();
registryKeys.put("biome", biomes.stream().map(CatalogBiome::key).toList());
registryKeys.put("placed_feature", placedFeatures.stream().map(CatalogPlacedFeature::key).toList());
registryKeys.put("structure", structures.stream().map(CatalogStructure::key).toList());
registryKeys.put("dimension_type", dimensionTypes.stream().map(CatalogDimensionType::key).toList());
registryKeys.put("level_stem", levelStems.stream().map(CatalogLevelStem::key).toList());
registryKeys.put("block_tag", List.copyOf(blockTags.keySet()));
List<CatalogNamespace> namespaces = namespaces(registryKeys, context);
return new WorldgenCatalog(WorldgenCatalog.FORMAT, context.minecraftVersion(), context.platform(),
context.capturedAtEpochMillis(), WorldgenCatalogFingerprint.of(namespaces, registryKeys), namespaces,
biomes, placedFeatures, structures, dimensionTypes, levelStems);
biomes, placedFeatures, structures, dimensionTypes, levelStems, blockTags);
}
private static Map<String, List<String>> referencedBlockTags(List<CatalogPlacedFeature> features,
Map<String, List<String>> available) {
Set<String> referenced = new TreeSet<>();
for (CatalogPlacedFeature feature : features) {
collectTagMatches(feature.config(), referenced);
}
Map<String, List<String>> tags = new TreeMap<>();
for (String tag : referenced) {
List<String> members = available.get(tag);
if (members != null) {
tags.put(tag, members);
}
}
return tags;
}
private static void collectTagMatches(JsonElement element, Set<String> tags) {
if (element == null || element.isJsonNull() || element.isJsonPrimitive()) {
return;
}
if (element.isJsonArray()) {
for (JsonElement entry : element.getAsJsonArray()) {
collectTagMatches(entry, tags);
}
return;
}
JsonObject object = element.getAsJsonObject();
JsonElement predicate = object.get("predicate_type");
JsonElement tag = object.get("tag");
if (predicate != null && predicate.isJsonPrimitive() && predicate.getAsString().endsWith("tag_match")
&& tag != null && tag.isJsonPrimitive()) {
String key = tag.getAsString();
tags.add(key.contains(":") ? key : MINECRAFT + ":" + key);
}
for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
collectTagMatches(entry.getValue(), tags);
}
}
private static List<CatalogNamespace> namespaces(Map<String, List<String>> registryKeys, CatalogCaptureContext context) {
@@ -23,6 +23,7 @@ import art.arcane.iris.pack.ingest.catalog.CatalogLevelStem;
import art.arcane.iris.pack.ingest.catalog.CatalogNamespace;
import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogFingerprint;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModMetadata;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot;
@@ -34,6 +35,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenSpawn;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeWorldgenSnapshot.WorldgenStructure;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import org.junit.Test;
@@ -104,15 +106,27 @@ public class WorldgenCatalogAssemblerTest {
"placed_feature", List.of("examplemod:ore_glimmer"),
"structure", List.of("examplemod:tower"),
"dimension_type", List.of("minecraft:overworld"),
"level_stem", List.of("minecraft:overworld")));
"level_stem", List.of("minecraft:overworld"),
"block_tag", List.of("examplemod:glimmer_hosts")));
assertEquals(expected, catalog.contentFingerprint());
}
@Test
public void onlyBlockTagsReferencedByPlacedFeaturesAreKept() {
WorldgenCatalog catalog = WorldgenCatalogAssembler.assemble(snapshot(), CONTEXT);
assertEquals(Map.of("examplemod:glimmer_hosts", List.of("minecraft:dirt", "minecraft:grass_block")), catalog.blockTags());
assertEquals(List.of("minecraft:dirt", "minecraft:grass_block"),
WorldgenCatalogIndex.of(catalog).blockTag("examplemod:glimmer_hosts").orElseThrow());
}
static NativeWorldgenSnapshot snapshot() {
JsonObject definition = new JsonObject();
definition.add("downfall", new JsonPrimitive(0.4F));
JsonObject config = new JsonObject();
config.add("type", new JsonPrimitive("minecraft:ore"));
JsonObject config = JsonParser.parseString("""
{"type": "minecraft:ore", "targets": [{"state": "examplemod:glimmer_ore",
"target": {"predicate_type": "minecraft:tag_match", "tag": "examplemod:glimmer_hosts"}}]}
""").getAsJsonObject();
return new NativeWorldgenSnapshot(
List.of(new WorldgenBiome("examplemod:glade", 0.7F, 0.4F, true, List.of("minecraft:is_overworld"),
List.of(List.of(), List.of("examplemod:ore_glimmer")),
@@ -124,6 +138,8 @@ public class WorldgenCatalogAssemblerTest {
List.of(new WorldgenDimensionType("minecraft:overworld", -64, 384, 384, null)),
List.of(new WorldgenLevelStem("minecraft:overworld", "minecraft:overworld", "minecraft:noise",
"minecraft:multi_noise", null)),
Map.of("examplemod:glimmer_hosts", List.of("minecraft:dirt", "minecraft:grass_block"),
"minecraft:unreferenced", List.of("minecraft:stone")),
new EncodeFailures(0, List.of(), null));
}
}
@@ -30,6 +30,7 @@ import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
@@ -74,7 +75,7 @@ public class WorldgenCatalogSummaryTest {
@Test
public void keyNamespacesWithoutModMetadataStillSummarize() {
WorldgenCatalog catalog = new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1L, "x",
List.of(), List.of(biome("datapack:dunes")), List.of(), List.of(), List.of(), List.of());
List.of(), List.of(biome("datapack:dunes")), List.of(), List.of(), List.of(), List.of(), Map.of());
NamespaceSummary summary = WorldgenCatalogSummary.namespace(catalog, "datapack").orElseThrow();
@@ -104,7 +105,7 @@ public class WorldgenCatalogSummaryTest {
List.of(new CatalogStructure("examplemod:tower", "minecraft:jigsaw", "surface_structures", "none", "",
List.of(), List.of())),
List.of(new CatalogDimensionType("examplemod:sky", 0, 256, 256, null)),
List.of(new CatalogLevelStem("examplemod:sky", "examplemod:sky", "minecraft:noise", "minecraft:fixed", null)));
List.of(new CatalogLevelStem("examplemod:sky", "examplemod:sky", "minecraft:noise", "minecraft:fixed", null)), Map.of());
}
private static CatalogBiome biome(String key) {
@@ -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));
}
@@ -28,6 +28,7 @@ import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
@@ -53,7 +54,7 @@ public class CanopyEstimateTest {
JsonParser.parseString("[{\"type\": \"minecraft:count\", \"count\": 10}, {\"type\": \"minecraft:in_square\"},"
+ "{\"type\": \"minecraft:surface_water_depth_filter\", \"max_water_depth\": 0}, {\"type\": \"minecraft:biome\"}]")));
return new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1L, "canopy", List.of(new CatalogNamespace("mod", "Mod", "1")),
biomes, features, List.of(), List.of(), List.of());
biomes, features, List.of(), List.of(), List.of(), Map.of());
}
private static CatalogBiome biome(String key, List<String> vegetal) {
@@ -26,6 +26,7 @@ import com.google.gson.JsonParser;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
@@ -65,7 +66,7 @@ public class FeatureConverterFormatTest {
{"type": "minecraft:biome"}]
""");
List<JsonObject> deposits = OreDepositConverter.convert(iron, OVERWORLD, -64);
List<JsonObject> deposits = OreDepositConverter.convert(iron, OVERWORLD, -64, Map.of());
assertEquals(2, deposits.size());
assertEquals("minecraft:iron_ore", deposits.get(0).getAsJsonArray("palette").get(0).getAsJsonObject().get("block").getAsString());
@@ -88,7 +89,7 @@ public class FeatureConverterFormatTest {
{"type": "minecraft:biome"}]
"""));
List<JsonObject> deposits = OreDepositConverter.convert(clay, OVERWORLD, -64);
List<JsonObject> deposits = OreDepositConverter.convert(clay, OVERWORLD, -64, Map.of());
assertEquals(1, deposits.size());
assertEquals("FULL_HEIGHT", deposits.getFirst().get("placementScope").getAsString());
@@ -101,7 +102,7 @@ public class FeatureConverterFormatTest {
"{\"size\": 4, \"targets\": [{\"state\": \"minecraft:stone\", \"target\": {\"predicate_type\": \"minecraft:always_true\"}}]}",
"[{\"type\": \"minecraft:environment_scan\", \"direction_of_search\": \"down\", \"max_steps\": 16, \"target_condition\": {\"type\": \"minecraft:solid\"}}]");
IngestConversionException failure = assertThrows(IngestConversionException.class, () -> OreDepositConverter.convert(buried, OVERWORLD, -64));
IngestConversionException failure = assertThrows(IngestConversionException.class, () -> OreDepositConverter.convert(buried, OVERWORLD, -64, Map.of()));
assertTrue(failure.getMessage(), failure.getMessage().contains("minecraft:environment_scan"));
}
@@ -178,6 +179,6 @@ public class FeatureConverterFormatTest {
private static WorldgenCatalogIndex index() {
return WorldgenCatalogIndex.of(new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 0L, "",
List.of(), List.of(), List.of(), List.of(), List.of(), List.of()));
List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), Map.of()));
}
}
@@ -30,6 +30,7 @@ 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.TreeSet;
@@ -129,7 +130,7 @@ public class IngestBiomePlacementTest {
}
return new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1000L, "fingerprint-placement",
List.of(new CatalogNamespace(MOD, "Mod Pack", "1.0.0")), biomes, List.of(), List.of(),
List.of(new CatalogDimensionType("minecraft:overworld", -64, 384, 384, null)), 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, String categoryTag) {
@@ -52,7 +52,7 @@ public class IngestHeightBoundsTest {
types.add(new CatalogDimensionType("examplemod:skyrealm", 0, 256, 256, null));
WorldgenCatalog catalog = new WorldgenCatalog(fixture.format(), fixture.minecraftVersion(), fixture.platform(),
fixture.capturedAtEpochMillis(), fixture.contentFingerprint(), fixture.namespaces(), fixture.biomes(),
fixture.placedFeatures(), fixture.structures(), types, fixture.levelStems());
fixture.placedFeatures(), fixture.structures(), types, fixture.levelStems(), fixture.blockTags());
JsonObject stone = tinDeposit("\"dimensionHeight\": {\"min\": 0, \"max\": 192}, \"borrowedDimensionType\": \"examplemod:skyrealm\"",
catalog);
@@ -0,0 +1,250 @@
/*
* 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.generation.biome.IrisBiome;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.ChunkedDataCache;
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.stage.IrisDepositModifier;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.ingest.catalog.CatalogBiome;
import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class IngestOreHostOrderTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String MOD = ModIngestFixture.MOD;
private static final int PACK_MIN_Y = -128;
private static final int HEIGHT = 448;
private static final int CHUNKS = 2;
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private final Map<String, NativeBlockState> states = new HashMap<>();
@Test
public void hostProducingOreIsConvertedAndRunsBeforeTheOreThatConsumesItsHost() throws Exception {
JsonArray deposits = ingestedDeposits();
assertEquals(2, deposits.size());
assertEquals("minecraft:sand", paletteBlock(deposits.get(0)));
assertEquals("minecraft:clay", paletteBlock(deposits.get(1)));
assertTrue(placed(List.of(deposits.get(0), deposits.get(1)), "minecraft:clay") > 0);
}
@Test
public void consumerRunningBeforeItsHostProducerFindsNoHost() throws Exception {
JsonArray deposits = ingestedDeposits();
assertEquals(0, placed(List.of(deposits.get(1), deposits.get(0)), "minecraft:clay"));
}
private JsonArray ingestedDeposits() throws Exception {
Path root = folder.newFolder().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
Path target = root.resolve("packs").resolve("isles");
ModIngestReport report = ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(MOD), Set.of(), false));
assertEquals(ModIngestTier.NATIVE, ModIngestFixture.item(report, MOD + ":isles/sand_beaches").tier());
assertEquals(ModIngestTier.NATIVE, ModIngestFixture.item(report, MOD + ":isles/clay_patch").tier());
return ModIngestFixture.readJson(target.resolve("biomes/ingest/" + MOD + "/sand_isles.json")).getAsJsonArray("deposits");
}
private static String paletteBlock(JsonElement deposit) {
return deposit.getAsJsonObject().getAsJsonArray("palette").get(0).getAsJsonObject().get("block").getAsString();
}
@SuppressWarnings("unchecked")
private int placed(List<JsonElement> depositJson, String block) {
Gson gson = new Gson();
IrisBiome surface = new IrisBiome();
for (JsonElement json : depositJson) {
ResolvedDeposit deposit = gson.fromJson(json, ResolvedDeposit.class);
deposit.bind(states);
surface.getDeposits().add(deposit);
}
Engine engine = mock(Engine.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS));
ChunkContext context = mock(ChunkContext.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS));
IrisRegion region = new IrisRegion();
when(engine.getHeight()).thenReturn(HEIGHT);
when(engine.getMinHeight()).thenReturn(PACK_MIN_Y);
when(engine.getSeedManager().getDeposit()).thenReturn(11L);
when(engine.getDimension()).thenReturn(new IrisDimension());
when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(61 - PACK_MIN_Y);
when(context.getBiome()).thenReturn(new ChunkedDataCache<>(
ProceduralStream.of((x, z) -> surface, Interpolated.of(value -> 0D, value -> null)), 0, 0));
when(context.getRegion()).thenReturn(new ChunkedDataCache<>(
ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> null)), 0, 0));
MantleChunk<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
Mantle<Matter> mantle = engine.getMantle().getMantle();
doReturn(chunk).when(mantle).useChunk(anyInt(), anyInt());
when(chunk.get(anyInt(), anyInt(), anyInt(), any())).thenReturn(null);
MultiBurst burst = new MultiBurst("Ingested deposit order", () -> 2);
when(engine.burst()).thenReturn(burst);
int count = 0;
try (MockedStatic<B> blocks = mockStatic(B.class, withSettings().stubOnly().defaultAnswer(CALLS_REAL_METHODS))) {
blocks.when(() -> B.getStateOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1));
IrisDepositModifier modifier = new IrisDepositModifier(engine);
for (int chunkX = 0; chunkX < CHUNKS; chunkX++) {
Hunk<NativeBlockState> terrain = islandChunk();
modifier.generateDeposits(terrain, chunkX, 0, false, context);
count += count(terrain, block);
}
} finally {
burst.close();
}
return count;
}
private Hunk<NativeBlockState> islandChunk() {
Hunk<NativeBlockState> terrain = Hunk.newArrayHunk(16, HEIGHT, 16);
for (int y = 0; y < HEIGHT; y++) {
int worldY = y + PACK_MIN_Y;
String key = worldY < 55 ? "minecraft:stone" : worldY < 61 ? "minecraft:dirt" : worldY == 61 ? "minecraft:grass_block" : "minecraft:air";
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
terrain.set(x, y, z, state(key));
}
}
}
return terrain;
}
private static int count(Hunk<NativeBlockState> terrain, String block) {
int count = 0;
for (int y = 0; y < HEIGHT; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
if (terrain.get(x, y, z).key().equals(block)) {
count++;
}
}
}
}
return count;
}
private NativeBlockState state(String block) {
String key = block.contains(":") ? block : "minecraft:" + block;
return states.computeIfAbsent(key, KeyedBlockState::new);
}
private static WorldgenCatalog catalog() {
WorldgenCatalog fixture = ModIngestFixture.catalog();
List<CatalogBiome> biomes = new ArrayList<>(fixture.biomes());
List<List<String>> steps = new ArrayList<>(11);
for (int index = 0; index < 11; index++) {
steps.add(index == 5 ? List.of(MOD + ":isles/clay_patch") : index == 7 ? List.of(MOD + ":isles/sand_beaches") : List.of());
}
biomes.add(new CatalogBiome(MOD + ":sand_isles", 0.7F, 0.8F, true, List.of("minecraft:is_overworld"), steps, List.of(), null));
List<CatalogPlacedFeature> features = new ArrayList<>(fixture.placedFeatures());
features.add(feature("isles/clay_patch", """
{"targets": [{"target": {"block": "minecraft:sand", "predicate_type": "minecraft:block_match"}, "state": "minecraft:clay"}],
"size": 50, "discard_chance_on_air_exposure": 0.0}
""", """
[{"count": 2, "type": "minecraft:count"}, {"type": "minecraft:in_square"},
{"height": {"min_inclusive": {"absolute": 57}, "max_inclusive": {"absolute": 62}, "type": "minecraft:uniform"}, "type": "minecraft:height_range"},
{"type": "minecraft:biome"}]
"""));
features.add(feature("isles/sand_beaches", """
{"targets": [{"target": {"tag": "examplemod:soil_and_gravel", "predicate_type": "minecraft:tag_match"}, "state": "minecraft:sand"}],
"size": 50, "discard_chance_on_air_exposure": 0.0}
""", """
[{"count": 6, "type": "minecraft:count"}, {"count": 5, "type": "minecraft:count"}, {"type": "minecraft:in_square"},
{"noise_to_count_ratio": 1, "noise_factor": 275.0, "noise_offset": 0.2, "type": "minecraft:noise_based_count"},
{"height": {"min_inclusive": {"absolute": 50}, "max_inclusive": {"absolute": 61}, "type": "minecraft:uniform"}, "type": "minecraft:height_range"},
{"type": "minecraft:biome"}]
"""));
Map<String, List<String>> tags = Map.of(MOD + ":soil_and_gravel", List.of("minecraft:coarse_dirt", "minecraft:dirt", "minecraft:grass_block", "minecraft:gravel"));
return new WorldgenCatalog(fixture.format(), fixture.minecraftVersion(), fixture.platform(), fixture.capturedAtEpochMillis(),
fixture.contentFingerprint(), fixture.namespaces(), biomes, features, fixture.structures(), fixture.dimensionTypes(),
fixture.levelStems(), tags);
}
private static CatalogPlacedFeature feature(String path, String config, String placement) {
String key = MOD + ":" + path;
return new CatalogPlacedFeature(key, key, "minecraft:ore", JsonParser.parseString(config), JsonParser.parseString(placement));
}
private static final class ResolvedDeposit extends IrisDepositGenerator {
private transient KList<NativeBlockState> resolved;
private void bind(Map<String, NativeBlockState> states) {
resolved = new KList<>();
for (IrisBlockData block : getPalette()) {
resolved.add(states.computeIfAbsent(block.getBlock(), KeyedBlockState::new));
}
}
@Override
public KList<NativeBlockState> getBlockData(IrisData data) {
return resolved;
}
}
}
@@ -75,7 +75,7 @@ public class IngestReservedNamespaceTest {
biomes.add(biome("underworld:ash_fields"));
return new WorldgenCatalog(fixture.format(), fixture.minecraftVersion(), fixture.platform(),
fixture.capturedAtEpochMillis(), fixture.contentFingerprint(), fixture.namespaces(), biomes,
fixture.placedFeatures(), fixture.structures(), fixture.dimensionTypes(), fixture.levelStems());
fixture.placedFeatures(), fixture.structures(), fixture.dimensionTypes(), fixture.levelStems(), fixture.blockTags());
}
private static CatalogBiome biome(String key) {
@@ -107,7 +107,7 @@ public class IngestTreeScopeTest {
features.add(feature("mossy_rocks", "minecraft:forest_rock", "{\"state\": \"minecraft:mossy_cobblestone\"}"));
return new WorldgenCatalog(fixture.format(), fixture.minecraftVersion(), fixture.platform(), fixture.capturedAtEpochMillis(),
fixture.contentFingerprint(), fixture.namespaces(), biomes, features, fixture.structures(),
fixture.dimensionTypes(), fixture.levelStems());
fixture.dimensionTypes(), fixture.levelStems(), fixture.blockTags());
}
private static CatalogPlacedFeature feature(String path, String type, String config) {
@@ -35,6 +35,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
final class ModIngestFixture {
@@ -316,7 +317,7 @@ final class ModIngestFixture {
List.of(new CatalogNamespace(MOD, "Example Mod", "1.0.0"), new CatalogNamespace(OTHER_MOD, "Other Mod", "2.0.0")),
biomes, features, structures,
List.of(new CatalogDimensionType("minecraft:overworld", -64, 384, 384, null)),
List.of());
List.of(), Map.of());
}
static ModIngestRequest request(Path source, Path target, Set<String> namespaces, boolean replace) {
@@ -216,7 +216,7 @@ public class ModIngesterPackTest {
namespaces.add(new CatalogNamespace("quietmod", "Quiet Mod", "1.0.0"));
WorldgenCatalog catalog = new WorldgenCatalog(fixture.format(), fixture.minecraftVersion(), fixture.platform(),
fixture.capturedAtEpochMillis(), fixture.contentFingerprint(), namespaces, fixture.biomes(),
fixture.placedFeatures(), fixture.structures(), fixture.dimensionTypes(), fixture.levelStems());
fixture.placedFeatures(), fixture.structures(), fixture.dimensionTypes(), fixture.levelStems(), fixture.blockTags());
ModIngestReport report = ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog, Set.of(), Set.of(), false));
@@ -24,6 +24,7 @@ import com.google.gson.JsonParser;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
@@ -93,20 +94,23 @@ public class VanillaFeatureParsersTest {
{"predicate_type": "minecraft:all_of", "rules": [
{"predicate_type": "minecraft:not", "rule": {"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"}},
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:stone_ore_replaceables"}]}]}
"""));
"""), Map.of());
assertFalse(stone.any());
assertEquals(Set.of("minecraft:tuff", "minecraft:stone", "minecraft:granite", "minecraft:diorite", "minecraft:andesite"), stone.blocks());
assertEquals(Set.of("minecraft:netherrack"),
VanillaRuleTests.hosts(json("{\"predicate_type\": \"minecraft:block_match\", \"block\": \"minecraft:netherrack\"}")).blocks());
VanillaRuleTests.hosts(json("{\"predicate_type\": \"minecraft:block_match\", \"block\": \"minecraft:netherrack\"}"), Map.of()).blocks());
assertEquals(Set.of("minecraft:end_stone"), VanillaRuleTests.hosts(json(
"{\"predicate_type\": \"minecraft:blockstate_match\", \"block_state\": {\"Name\": \"minecraft:end_stone\"}}")).blocks());
assertTrue(VanillaRuleTests.hosts(json("{\"predicate_type\": \"minecraft:always_true\"}")).any());
"{\"predicate_type\": \"minecraft:blockstate_match\", \"block_state\": {\"Name\": \"minecraft:end_stone\"}}"), Map.of()).blocks());
assertTrue(VanillaRuleTests.hosts(json("{\"predicate_type\": \"minecraft:always_true\"}"), Map.of()).any());
assertThrows(IngestConversionException.class, () -> VanillaRuleTests.hosts(json(
"{\"predicate_type\": \"minecraft:random_block_match\", \"block\": \"minecraft:stone\", \"probability\": 0.5}")));
"{\"predicate_type\": \"minecraft:random_block_match\", \"block\": \"minecraft:stone\", \"probability\": 0.5}"), Map.of()));
IngestConversionException unknownTag = assertThrows(IngestConversionException.class, () -> VanillaRuleTests.hosts(json(
"{\"predicate_type\": \"minecraft:tag_match\", \"tag\": \"examplemod:moon_rock\"}")));
"{\"predicate_type\": \"minecraft:tag_match\", \"tag\": \"examplemod:moon_rock\"}"), Map.of()));
assertTrue(unknownTag.getMessage().contains("examplemod:moon_rock"));
assertEquals(Set.of("examplemod:moon_stone"), VanillaRuleTests.hosts(json(
"{\"predicate_type\": \"minecraft:tag_match\", \"tag\": \"examplemod:moon_rock\"}"),
Map.of("examplemod:moon_rock", List.of("examplemod:moon_stone"))).blocks());
}
@Test
@@ -29,6 +29,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -55,7 +56,7 @@ public class WorldgenCatalogStoreTest {
List.of(new CatalogStructure("examplemod:tower", "minecraft:jigsaw", "surface_structures", "none",
"examplemod:has_tower", List.of("examplemod:glade"), List.of("examplemod:towers"))),
List.of(new CatalogDimensionType("examplemod:sky", 0, 256, 256, new JsonObject())),
List.of(new CatalogLevelStem("examplemod:sky", "examplemod:sky", "minecraft:noise", "minecraft:multi_noise", null)));
List.of(new CatalogLevelStem("examplemod:sky", "examplemod:sky", "minecraft:noise", "minecraft:multi_noise", null)), Map.of());
Path file = folder.getRoot().toPath().resolve("nested").resolve(WorldgenCatalogStore.FILE_NAME);
WorldgenCatalogStore.write(file, catalog);
@@ -72,7 +73,7 @@ public class WorldgenCatalogStoreTest {
List.of(new CatalogBiome("examplemod:glade", 0.7F, 0.8F, true, List.of(), List.of(List.of("examplemod:flowers")), List.of(), null),
new CatalogBiome("minecraft:forest", 0.7F, 0.8F, true, List.of(), List.of(List.of("examplemod:flowers")), List.of(), null),
new CatalogBiome("minecraft:desert", 2.0F, 0.0F, false, List.of(), List.of(), List.of(), null)),
List.of(), List.of(), List.of(), List.of());
List.of(), List.of(), List.of(), List.of(), Map.of());
WorldgenCatalogIndex index = WorldgenCatalogIndex.of(catalog);
assertEquals(1, index.biomesIn("examplemod").size());