Merged mod ingester

This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-14 20:14:16 -04:00
commit 7fc8d20516
27 files changed
+3793 -2

No files matched your search

@@ -0,0 +1,42 @@
/*
* 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;
enum BiomeRole {
LAND("landBiomes", "land"),
SEA("seaBiomes", "sea"),
SHORE("shoreBiomes", "shore"),
CAVE("caveBiomes", "cave");
private final String poolField;
private final String label;
BiomeRole(String poolField, String label) {
this.poolField = poolField;
this.label = label;
}
String poolField() {
return poolField;
}
String label() {
return label;
}
}
@@ -0,0 +1,106 @@
/*
* 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.decoration.IrisDecorationStep;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
import art.arcane.iris.structure.nativegen.NativeFeatureGenerationStatus;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
final class ImportedFeatureWriter {
static final String VANILLA_PREFIX = "minecraft:";
private ImportedFeatureWriter() {
}
static IrisImportedFeatureControl write(JsonObject dimension, Set<String> nativeKeys) {
JsonElement existing = dimension.get("importedFeatures");
JsonObject imported = existing != null && existing.isJsonObject() ? existing.getAsJsonObject() : new JsonObject();
Set<String> disabled = new LinkedHashSet<>(IngestJson.strings(imported.get("disabled")));
disabled.add(VANILLA_PREFIX);
disabled.addAll(nativeKeys);
imported.addProperty("enabled", true);
imported.add("disabled", IngestJson.stringArray(disabled));
dimension.add("importedFeatures", imported);
IrisImportedFeatureControl control = new IrisImportedFeatureControl();
control.setEnabled(true);
control.setDisabled(new KList<>(disabled));
control.setSteps(steps(imported.get("steps")));
control.setDisabledSteps(steps(imported.get("disabledSteps")));
return control;
}
static List<ModIngestItem> applySuppression(List<ModIngestItem> items, Map<String, ModFeatureIngester.FeaturePlan> plans,
IrisImportedFeatureControl control) {
List<ModIngestItem> result = new ArrayList<>(items.size());
for (ModIngestItem item : items) {
ModFeatureIngester.FeaturePlan plan = plans.get(item.sourceKey());
if (item.tier() != ModIngestTier.PASSTHROUGH || plan == null) {
result.add(item);
continue;
}
String suppression = suppression(item.sourceKey(), plan.steps(), control);
result.add(suppression == null
? item
: new ModIngestItem(item.namespace(), item.sourceKey(), item.kind(), ModIngestTier.SKIPPED, "", suppression));
}
return result;
}
private static String suppression(String key, Set<Integer> steps, IrisImportedFeatureControl control) {
NativeFeatureGenerationStatus status = null;
for (int step : steps) {
status = control.resolve(key, IrisDecorationStep.byOrdinal(step));
if (status == NativeFeatureGenerationStatus.GENERATE_NATIVE) {
return null;
}
}
if (status == NativeFeatureGenerationStatus.STEP_DISABLED) {
return "its decoration step is disabled by importedFeatures";
}
for (String entry : control.getDisabled()) {
IrisImportedFeatureControl single = new IrisImportedFeatureControl();
single.setEnabled(true);
single.setDisabled(new KList<>(entry));
if (!single.shouldGenerate(key)) {
return "suppressed by importedFeatures.disabled entry " + entry;
}
}
return "suppressed by importedFeatures";
}
private static KList<IrisDecorationStep> steps(JsonElement value) {
KList<IrisDecorationStep> steps = new KList<>();
for (String name : IngestJson.strings(value)) {
IrisDecorationStep step = IrisDecorationStep.byKey(name);
if (step != null) {
steps.add(step);
}
}
return steps;
}
}
@@ -0,0 +1,51 @@
/*
* 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 java.util.List;
record IngestClimate(double temperature, double downfall) {
static IngestClimate of(CatalogBiome biome) {
return new IngestClimate(biome.temperature(), biome.downfall());
}
static IngestClimate average(List<IngestClimate> climates) {
if (climates.isEmpty()) {
return null;
}
double temperature = 0D;
double downfall = 0D;
for (IngestClimate climate : climates) {
temperature += climate.temperature();
downfall += climate.downfall();
}
return new IngestClimate(temperature / climates.size(), downfall / climates.size());
}
double distance(IngestClimate other) {
if (other == null) {
return Double.POSITIVE_INFINITY;
}
double temperatureDelta = temperature - other.temperature();
double downfallDelta = downfall - other.downfall();
return temperatureDelta * temperatureDelta + downfallDelta * downfallDelta;
}
}
@@ -0,0 +1,25 @@
/*
* 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;
final class IngestConversionException extends Exception {
IngestConversionException(String reason) {
super(reason);
}
}
@@ -0,0 +1,88 @@
/*
* 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 com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
final class IngestJson {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
private static final int DECIMALS = 6;
private static final double SMALLEST = 0.000001D;
private IngestJson() {
}
static double round(double value) {
double rounded = BigDecimal.valueOf(value).setScale(DECIMALS, RoundingMode.HALF_UP).doubleValue();
return rounded == 0D && value > 0D ? SMALLEST : rounded;
}
static JsonArray stringArray(Collection<String> values) {
JsonArray array = new JsonArray(values.size());
for (String value : values) {
array.add(value);
}
return array;
}
static List<String> strings(JsonElement element) {
List<String> values = new ArrayList<>();
if (element == null || !element.isJsonArray()) {
return values;
}
for (JsonElement value : element.getAsJsonArray()) {
if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
values.add(value.getAsString());
}
}
return values;
}
static JsonObject read(Path file) throws IOException {
try {
JsonElement element = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8));
if (!element.isJsonObject()) {
throw new IOException("Pack file is not a JSON object: " + file);
}
return element.getAsJsonObject();
} catch (JsonParseException e) {
throw new IOException("Pack file is not valid JSON: " + file, e);
}
}
static String format(JsonElement element) {
return GSON.toJson(element) + "\n";
}
}
@@ -0,0 +1,88 @@
/*
* 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.WorldgenCatalog;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
final class IngestManifest {
static final String PATH = "ingest/manifest.json";
static final int FORMAT = 1;
private IngestManifest() {
}
static void write(IngestPack pack, WorldgenCatalog catalog, String sourceDimension, String dimension,
Set<String> namespaces, List<ModIngestItem> items) throws IOException {
JsonObject manifest = new JsonObject();
manifest.addProperty("format", FORMAT);
manifest.addProperty("catalogFingerprint", catalog.contentFingerprint());
manifest.addProperty("catalogCapturedAt", catalog.capturedAtEpochMillis());
manifest.addProperty("minecraftVersion", catalog.minecraftVersion());
manifest.addProperty("platform", catalog.platform());
manifest.addProperty("sourceDimension", sourceDimension);
manifest.addProperty("dimension", dimension);
manifest.add("namespaces", IngestJson.stringArray(namespaces));
manifest.add("generatedFiles", IngestJson.stringArray(pack.generatedFiles()));
manifest.add("modifiedFiles", IngestJson.stringArray(pack.modifiedFiles()));
JsonArray itemArray = new JsonArray(items.size());
for (ModIngestItem item : items) {
JsonObject entry = new JsonObject();
entry.addProperty("namespace", item.namespace());
entry.addProperty("sourceKey", item.sourceKey());
entry.addProperty("kind", item.kind());
entry.addProperty("tier", item.tier().name());
entry.addProperty("target", item.target());
entry.addProperty("reason", item.reason());
itemArray.add(entry);
}
manifest.add("items", itemArray);
pack.write(PATH, manifest);
}
static List<String> summary(String dimension, IngestPack pack, Set<String> namespaces, List<ModIngestItem> items) {
List<String> lines = new ArrayList<>(namespaces.size() + 1);
lines.add("pack " + dimension + ": " + pack.generatedFiles().size() + " generated files, "
+ pack.modifiedFiles().size() + " modified files");
for (String namespace : namespaces) {
Map<ModIngestTier, Integer> counts = new EnumMap<>(ModIngestTier.class);
for (ModIngestTier tier : ModIngestTier.values()) {
counts.put(tier, 0);
}
for (ModIngestItem item : items) {
if (item.namespace().equals(namespace)) {
counts.merge(item.tier(), 1, Integer::sum);
}
}
lines.add(namespace + ": " + counts.get(ModIngestTier.NATIVE) + " native, "
+ counts.get(ModIngestTier.ADOPTED) + " adopted, "
+ counts.get(ModIngestTier.PASSTHROUGH) + " passthrough, "
+ counts.get(ModIngestTier.SKIPPED) + " skipped");
}
return lines;
}
}
@@ -0,0 +1,155 @@
/*
* 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 com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
final class IngestPack {
private static final String SNIPPET_PREFIX = "snippet/";
private final Path root;
private final Map<String, JsonObject> loaded = new HashMap<>();
private final Map<String, JsonObject> pending = new TreeMap<>();
private final Set<String> generated = new TreeSet<>();
IngestPack(Path root) {
this.root = root;
}
Path root() {
return root;
}
static String biomePath(String biomeKey) {
return "biomes/" + biomeKey + ".json";
}
static String regionPath(String regionKey) {
return "regions/" + regionKey + ".json";
}
static String dimensionPath(String dimensionKey) {
return "dimensions/" + dimensionKey + ".json";
}
Optional<JsonObject> find(String relative) throws IOException {
JsonObject cached = loaded.get(relative);
if (cached != null) {
return Optional.of(cached);
}
Path file = resolve(relative);
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
return Optional.empty();
}
JsonObject json = IngestJson.read(file);
loaded.put(relative, json);
return Optional.of(json);
}
JsonObject require(String relative) throws IOException {
Optional<JsonObject> json = find(relative);
if (json.isEmpty()) {
throw new IOException("Pack file is missing: " + resolve(relative));
}
return json.get();
}
JsonObject edit(String relative) throws IOException {
JsonObject json = require(relative);
pending.put(relative, json);
return json;
}
void create(String relative, JsonObject json) throws IOException {
if (loaded.containsKey(relative) || Files.exists(resolve(relative), LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Ingest would overwrite an existing pack file: " + resolve(relative));
}
loaded.put(relative, json);
pending.put(relative, json);
generated.add(relative);
}
JsonObject snippet(JsonElement value, String snippetType) throws IOException {
if (value == null || value.isJsonNull()) {
return null;
}
if (value.isJsonObject()) {
return value.getAsJsonObject();
}
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
return null;
}
String reference = value.getAsString();
if (!reference.startsWith(SNIPPET_PREFIX)) {
return null;
}
String base = SNIPPET_PREFIX + snippetType + "/";
String key = reference.startsWith(base) ? reference : base + reference.substring(SNIPPET_PREFIX.length());
return find(key + ".json").map(JsonObject::deepCopy).orElse(null);
}
List<String> generatedFiles() {
return new ArrayList<>(generated);
}
List<String> modifiedFiles() {
List<String> modified = new ArrayList<>();
for (String relative : pending.keySet()) {
if (!generated.contains(relative)) {
modified.add(relative);
}
}
return modified;
}
void flush() throws IOException {
for (Map.Entry<String, JsonObject> entry : pending.entrySet()) {
write(entry.getKey(), entry.getValue());
}
}
void write(String relative, JsonElement json) throws IOException {
Path file = resolve(relative);
Files.createDirectories(file.getParent());
Files.writeString(file, IngestJson.format(json), StandardCharsets.UTF_8);
}
private Path resolve(String relative) throws IOException {
Path file = root.resolve(relative).normalize();
if (!file.startsWith(root)) {
throw new IOException("Pack path escapes the pack root: " + relative);
}
return file;
}
}
@@ -0,0 +1,202 @@
/*
* 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.WorldgenCatalogIndex;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
final class IngestPackModel {
private static final int DEFAULT_MIN_Y = -64;
private static final int DEFAULT_MAX_Y = 320;
private static final String DEFAULT_DERIVATIVE = "minecraft:the_void";
private final IngestPack pack;
private final WorldgenCatalogIndex index;
private final String dimensionPath;
private final String environment;
private final int packMinY;
private final int packMaxY;
private final List<RegionProfile> regions;
private final Map<String, String> reachableBiomes;
private IngestPackModel(IngestPack pack, WorldgenCatalogIndex index, String dimensionPath, String environment,
int[] heightRange, List<RegionProfile> regions, Map<String, String> reachableBiomes) {
this.pack = pack;
this.index = index;
this.dimensionPath = dimensionPath;
this.environment = environment;
this.packMinY = heightRange[0];
this.packMaxY = heightRange[1];
this.regions = regions;
this.reachableBiomes = reachableBiomes;
}
static IngestPackModel load(IngestPack pack, String dimensionKey, WorldgenCatalogIndex index) throws IOException {
String dimensionPath = IngestPack.dimensionPath(dimensionKey);
JsonObject dimension = pack.require(dimensionPath);
JsonElement environmentValue = dimension.get("environment");
String environment = environmentValue != null && environmentValue.isJsonPrimitive()
? environmentValue.getAsString().trim().toUpperCase(Locale.ROOT)
: "NORMAL";
Map<String, String> reachable = new LinkedHashMap<>();
List<String> regionKeys = IngestJson.strings(dimension.get("regions"));
List<JsonObject> regionJson = new ArrayList<>(regionKeys.size());
List<String> loadedRegionKeys = new ArrayList<>(regionKeys.size());
for (String regionKey : regionKeys) {
Optional<JsonObject> region = pack.find(IngestPack.regionPath(regionKey));
if (region.isPresent()) {
regionJson.add(region.get());
loadedRegionKeys.add(regionKey);
}
}
collectReachable(pack, regionJson, reachable);
List<RegionProfile> profiles = new ArrayList<>(regionJson.size());
for (int position = 0; position < regionJson.size(); position++) {
profiles.add(profile(loadedRegionKeys.get(position), regionJson.get(position), reachable, index));
}
return new IngestPackModel(pack, index, dimensionPath, environment, heightRange(pack, dimension),
Collections.unmodifiableList(profiles), Collections.unmodifiableMap(reachable));
}
IngestPack pack() {
return pack;
}
String dimensionPath() {
return dimensionPath;
}
String environment() {
return environment;
}
int packMinY() {
return packMinY;
}
List<RegionProfile> regions() {
return regions;
}
Map<String, String> reachableBiomes() {
return reachableBiomes;
}
IngestClimate biomeClimate(String biomeKey) {
String derivative = reachableBiomes.get(biomeKey);
return derivative == null ? null : index.biome(derivative).map(IngestClimate::of).orElse(null);
}
VanillaHeights.VerticalBounds verticalBounds() {
String typeKey = switch (environment) {
case "NORMAL" -> "minecraft:overworld";
case "NETHER" -> "minecraft:the_nether";
case "THE_END" -> "minecraft:the_end";
default -> "";
};
Optional<CatalogDimensionType> type = index.dimensionType(typeKey);
if (type.isPresent() && type.get().height() > 0) {
return new VanillaHeights.VerticalBounds(type.get().minY(), type.get().height());
}
return new VanillaHeights.VerticalBounds(packMinY, Math.max(1, packMaxY - packMinY));
}
private static RegionProfile profile(String key, JsonObject region, Map<String, String> reachable, WorldgenCatalogIndex index) {
Map<BiomeRole, List<String>> pools = new EnumMap<>(BiomeRole.class);
for (BiomeRole role : BiomeRole.values()) {
List<String> pool = new ArrayList<>();
for (String biome : IngestJson.strings(region.get(role.poolField()))) {
if (reachable.containsKey(biome) && !pool.contains(biome)) {
pool.add(biome);
}
}
pools.put(role, Collections.unmodifiableList(pool));
}
List<IngestClimate> landClimates = new ArrayList<>();
for (String biome : pools.get(BiomeRole.LAND)) {
Optional<CatalogBiome> catalogBiome = index.biome(reachable.get(biome));
catalogBiome.ifPresent(value -> landClimates.add(IngestClimate.of(value)));
}
return new RegionProfile(key, IngestClimate.average(landClimates), pools);
}
private static void collectReachable(IngestPack pack, List<JsonObject> regions, Map<String, String> reachable) throws IOException {
ArrayDeque<String> queue = new ArrayDeque<>();
for (JsonObject region : regions) {
for (BiomeRole role : BiomeRole.values()) {
queue.addAll(IngestJson.strings(region.get(role.poolField())));
}
}
while (!queue.isEmpty()) {
String biomeKey = queue.removeFirst();
if (reachable.containsKey(biomeKey)) {
continue;
}
Optional<JsonObject> biome = pack.find(IngestPack.biomePath(biomeKey));
if (biome.isEmpty()) {
continue;
}
JsonElement derivative = biome.get().get("derivative");
String derivativeKey = derivative != null && derivative.isJsonPrimitive()
? VanillaJson.namespaced(derivative.getAsString())
: DEFAULT_DERIVATIVE;
reachable.put(biomeKey, derivativeKey);
queue.addAll(IngestJson.strings(biome.get().get("children")));
}
}
private static int[] heightRange(IngestPack pack, JsonObject dimension) throws IOException {
JsonObject range = pack.snippet(dimension.get("dimensionHeight"), "range");
if (range == null) {
return new int[]{DEFAULT_MIN_Y, DEFAULT_MAX_Y};
}
return new int[]{(int) number(range.get("min"), DEFAULT_MIN_Y), (int) number(range.get("max"), DEFAULT_MAX_Y)};
}
private static double number(JsonElement value, double fallback) {
if (value == null || !value.isJsonPrimitive()) {
return fallback;
}
try {
return Double.parseDouble(value.getAsString().trim());
} catch (NumberFormatException e) {
return fallback;
}
}
record RegionProfile(String key, IngestClimate climate, Map<BiomeRole, List<String>> pools) {
List<String> pool(BiomeRole role) {
return pools.get(role);
}
}
}
@@ -0,0 +1,173 @@
/*
* 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.decoration.IrisDecorationStep;
import art.arcane.iris.pack.ingest.catalog.CatalogBiome;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
final class ModBiomeAdopter {
static final String INGEST_BIOME_FOLDER = "ingest/";
private static final List<String> OVERRIDE_FIELDS = List.of("customDerivitives", "vanillaDerivative", "biomeScatter", "biomeSkyScatter", "children");
private static final int VEGETAL_STEP = IrisDecorationStep.VEGETAL_DECORATION.ordinal();
private final IngestPackModel model;
private final ModBiomeClassifier classifier;
private final Set<String> decoratorFeatures;
ModBiomeAdopter(IngestPackModel model, WorldgenCatalogIndex index, Set<String> decoratorFeatures) {
this.model = model;
this.classifier = new ModBiomeClassifier(model.environment(), index.catalog());
this.decoratorFeatures = decoratorFeatures;
}
Map<String, AdoptedBiome> adopt(List<CatalogBiome> modBiomes, List<ModIngestItem> items) throws IOException {
Map<String, AdoptedBiome> adopted = new LinkedHashMap<>();
for (CatalogBiome biome : modBiomes) {
String namespace = WorldgenCatalogIndex.namespaceOf(biome.key());
ModBiomeClassifier.Classification classification = classifier.classify(biome);
if (classification.skipped()) {
items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "", classification.skipReason()));
continue;
}
BiomeRole role = classification.role();
IngestClimate climate = IngestClimate.of(biome);
IngestPackModel.RegionProfile region = nearestRegion(role, climate);
if (region == null) {
items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "",
"no region in this dimension has a " + role.label() + " biome pool"));
continue;
}
String template = nearestTemplate(region, role, climate);
String irisKey = irisKey(biome.key());
JsonObject json = model.pack().require(IngestPack.biomePath(template)).deepCopy();
transform(json, biome, ownsVegetation(biome, namespace));
model.pack().create(IngestPack.biomePath(irisKey), json);
appendToPool(region.key(), role, irisKey);
adopted.put(biome.key(), new AdoptedBiome(biome.key(), irisKey, role));
items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.ADOPTED, irisKey,
role.label() + " biome in region " + region.key() + " from template " + template));
}
return adopted;
}
static String irisKey(String modBiomeKey) {
int colon = modBiomeKey.indexOf(':');
String namespace = colon < 0 ? "minecraft" : modBiomeKey.substring(0, colon);
return INGEST_BIOME_FOLDER + namespace + "/" + modBiomeKey.substring(colon + 1);
}
private IngestPackModel.RegionProfile nearestRegion(BiomeRole role, IngestClimate climate) {
IngestPackModel.RegionProfile best = null;
double bestDistance = Double.POSITIVE_INFINITY;
for (IngestPackModel.RegionProfile region : model.regions()) {
if (region.pool(role).isEmpty()) {
continue;
}
double distance = climate.distance(region.climate());
if (best == null || distance < bestDistance) {
best = region;
bestDistance = distance;
}
}
return best;
}
private String nearestTemplate(IngestPackModel.RegionProfile region, BiomeRole role, IngestClimate climate) {
String best = null;
double bestDistance = Double.POSITIVE_INFINITY;
for (String candidate : region.pool(role)) {
IngestClimate candidateClimate = model.biomeClimate(candidate);
double distance = climate.distance(candidateClimate == null ? region.climate() : candidateClimate);
if (best == null || distance < bestDistance) {
best = candidate;
bestDistance = distance;
}
}
return best;
}
private void transform(JsonObject json, CatalogBiome biome, boolean ownsVegetation) throws IOException {
json.addProperty("name", readableName(biome.key()));
json.addProperty("derivative", biome.key());
for (String field : OVERRIDE_FIELDS) {
json.remove(field);
}
if (!ownsVegetation) {
return;
}
json.remove("objects");
JsonObject procedural = model.pack().snippet(json.get("proceduralObjects"), "procedural-objects");
if (procedural == null) {
return;
}
JsonObject withoutTrees = procedural.deepCopy();
withoutTrees.remove("trees");
json.add("proceduralObjects", withoutTrees);
}
private boolean ownsVegetation(CatalogBiome biome, String namespace) {
if (biome.featureSteps().size() <= VEGETAL_STEP) {
return false;
}
for (String feature : biome.featureSteps().get(VEGETAL_STEP)) {
if (WorldgenCatalogIndex.namespaceOf(feature).equals(namespace) && !decoratorFeatures.contains(feature)) {
return true;
}
}
return false;
}
private void appendToPool(String regionKey, BiomeRole role, String irisKey) throws IOException {
JsonObject region = model.pack().edit(IngestPack.regionPath(regionKey));
JsonElement pool = region.get(role.poolField());
JsonArray array = pool != null && pool.isJsonArray() ? pool.getAsJsonArray() : new JsonArray();
array.add(irisKey);
region.add(role.poolField(), array);
}
private static String readableName(String key) {
String path = key.substring(key.indexOf(':') + 1);
String leaf = path.substring(path.lastIndexOf('/') + 1);
StringBuilder name = new StringBuilder(leaf.length());
for (String word : leaf.split("[_\\-]+")) {
if (word.isEmpty()) {
continue;
}
if (!name.isEmpty()) {
name.append(' ');
}
name.append(word.substring(0, 1).toUpperCase(Locale.ROOT)).append(word.substring(1));
}
return name.isEmpty() ? key : name.toString();
}
record AdoptedBiome(String modKey, String irisKey, BiomeRole role) {
}
}
@@ -0,0 +1,112 @@
/*
* 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.WorldgenCatalog;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
final class ModBiomeClassifier {
private static final Set<String> CAVE_TAGS = Set.of("is_cave", "is_caves", "caves", "is_underground", "underground");
private static final Set<String> END_TAGS = Set.of("is_end", "is_the_end", "is_outer_end_island");
private final String environment;
private final String requiredTag;
private final boolean requiredTagCaptured;
ModBiomeClassifier(String environment, WorldgenCatalog catalog) {
this.environment = environment;
this.requiredTag = switch (environment) {
case "NORMAL" -> "is_overworld";
case "NETHER" -> "is_nether";
case "THE_END" -> "is_end";
default -> "";
};
this.requiredTagCaptured = !requiredTag.isEmpty() && anyBiomeTagged(catalog, requiredTag);
}
Classification classify(CatalogBiome biome) {
Set<String> tags = conventionTags(biome.tags());
if (environment.equals("NORMAL") && tags.contains("is_nether")) {
return Classification.skip("tagged for the nether, not this overworld dimension");
}
if (environment.equals("NORMAL") && !Collections.disjoint(tags, END_TAGS)) {
return Classification.skip("tagged for the end, not this overworld dimension");
}
if (requiredTagCaptured && !tags.contains(requiredTag)) {
return Classification.skip("not tagged minecraft:" + requiredTag + " for this " + environment.toLowerCase(Locale.ROOT) + " dimension");
}
if (tags.contains("is_river")) {
return Classification.skip("river biome; Iris hydrology owns rivers");
}
if (tags.contains("is_ocean") || tags.contains("is_deep_ocean")) {
return Classification.of(BiomeRole.SEA);
}
if (tags.contains("is_beach")) {
return Classification.of(BiomeRole.SHORE);
}
for (String tag : tags) {
if (CAVE_TAGS.contains(tag)) {
return Classification.of(BiomeRole.CAVE);
}
}
return Classification.of(BiomeRole.LAND);
}
private static boolean anyBiomeTagged(WorldgenCatalog catalog, String tag) {
for (CatalogBiome biome : catalog.biomes()) {
if (conventionTags(biome.tags()).contains(tag)) {
return true;
}
}
return false;
}
private static Set<String> conventionTags(List<String> tags) {
Set<String> paths = new HashSet<>();
for (String tag : tags) {
String key = VanillaJson.namespaced(tag);
int colon = key.indexOf(':');
String namespace = key.substring(0, colon);
if (namespace.equals("minecraft") || namespace.equals("c")) {
paths.add(key.substring(colon + 1));
}
}
return paths;
}
record Classification(BiomeRole role, String skipReason) {
static Classification of(BiomeRole role) {
return new Classification(role, null);
}
static Classification skip(String reason) {
return new Classification(null, reason);
}
boolean skipped() {
return role == null;
}
}
}
@@ -0,0 +1,219 @@
/*
* 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.CatalogPlacedFeature;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
final class ModFeatureIngester {
private final IngestPackModel model;
private final WorldgenCatalogIndex index;
private final Map<String, FeaturePlan> plans = new TreeMap<>();
private final Set<String> nativeKeys = new TreeSet<>();
ModFeatureIngester(IngestPackModel model, WorldgenCatalogIndex index, Set<String> namespaces) {
this.model = model;
this.index = index;
Map<String, Set<String>> declaring = new TreeMap<>();
Map<String, Set<Integer>> steps = new TreeMap<>();
for (CatalogBiome biome : index.catalog().biomes()) {
for (int step = 0; step < biome.featureSteps().size(); step++) {
for (String feature : biome.featureSteps().get(step)) {
if (!namespaces.contains(WorldgenCatalogIndex.namespaceOf(feature))) {
continue;
}
declaring.computeIfAbsent(feature, ignored -> new LinkedHashSet<>()).add(biome.key());
steps.computeIfAbsent(feature, ignored -> new TreeSet<>()).add(step);
}
}
}
for (Map.Entry<String, Set<String>> entry : declaring.entrySet()) {
plans.put(entry.getKey(), plan(entry.getKey(), entry.getValue(), steps.get(entry.getKey())));
}
}
Set<String> decoratorKeys() {
Set<String> keys = new TreeSet<>();
for (FeaturePlan plan : plans.values()) {
if (plan.decorator() != null) {
keys.add(plan.key());
}
}
return keys;
}
Set<String> nativeKeys() {
return nativeKeys;
}
Map<String, FeaturePlan> plans() {
return plans;
}
List<ModIngestItem> apply(Map<String, ModBiomeAdopter.AdoptedBiome> adopted) throws IOException {
Set<String> universe = new TreeSet<>(adopted.keySet());
universe.addAll(model.reachableBiomes().values());
Set<String> caveKeys = caveKeys(adopted);
List<ModIngestItem> items = new ArrayList<>(plans.size());
for (FeaturePlan plan : plans.values()) {
String namespace = WorldgenCatalogIndex.namespaceOf(plan.key());
Set<String> hosts = new TreeSet<>(plan.declaring());
hosts.retainAll(universe);
if (hosts.isEmpty()) {
items.add(new ModIngestItem(namespace, plan.key(), "feature", ModIngestTier.SKIPPED, "",
"no biome in the target dimension declares it"));
continue;
}
if (plan.deposits() != null) {
items.add(new ModIngestItem(namespace, plan.key(), "feature", ModIngestTier.NATIVE,
applyDeposits(plan, adopted, universe, caveKeys), "ore converted to Iris deposits"));
nativeKeys.add(plan.key());
continue;
}
if (plan.decorator() != null) {
items.add(new ModIngestItem(namespace, plan.key(), "feature", ModIngestTier.NATIVE,
applyDecorator(plan, adopted), "patch converted to an Iris decorator"));
nativeKeys.add(plan.key());
continue;
}
items.add(new ModIngestItem(namespace, plan.key(), "feature", ModIngestTier.PASSTHROUGH, "importedFeatures", plan.reason()));
}
return items;
}
private FeaturePlan plan(String key, Set<String> declaring, Set<Integer> steps) {
Optional<CatalogPlacedFeature> feature = index.placedFeature(key);
if (feature.isEmpty()) {
return FeaturePlan.passthrough(key, declaring, steps, "placed feature is missing from the catalog");
}
CatalogPlacedFeature placed = feature.get();
try {
if (OreDepositConverter.handles(placed)) {
List<JsonObject> deposits = OreDepositConverter.convert(placed, model.verticalBounds(), model.packMinY());
return new FeaturePlan(key, declaring, steps, deposits, null, "");
}
if (PatchDecoratorConverter.handles(placed)) {
return new FeaturePlan(key, declaring, steps, null, PatchDecoratorConverter.convert(placed, index), "");
}
} catch (IngestConversionException e) {
return FeaturePlan.passthrough(key, declaring, steps, "generated natively: " + e.getMessage());
} catch (RuntimeException e) {
return FeaturePlan.passthrough(key, declaring, steps, "generated natively: malformed " + placed.featureType() + " data (" + e.getMessage() + ")");
}
return FeaturePlan.passthrough(key, declaring, steps, "generated natively: feature type " + placed.featureType() + " has no Iris equivalent");
}
private String applyDeposits(FeaturePlan plan, Map<String, ModBiomeAdopter.AdoptedBiome> adopted, Set<String> universe, Set<String> caveKeys) throws IOException {
List<String> adoptedTargets = new ArrayList<>();
boolean vanillaDeclared = false;
for (String biome : plan.declaring()) {
vanillaDeclared |= WorldgenCatalogIndex.namespaceOf(biome).equals("minecraft");
ModBiomeAdopter.AdoptedBiome adoptedBiome = adopted.get(biome);
if (adoptedBiome != null) {
adoptedTargets.add(adoptedBiome.irisKey());
}
}
if (!vanillaDeclared && !adoptedTargets.isEmpty()) {
for (String irisKey : adoptedTargets) {
appendAll(model.pack().require(IngestPack.biomePath(irisKey)), "deposits", plan.deposits());
}
return String.join(",", adoptedTargets);
}
Set<String> included = new TreeSet<>(plan.declaring());
included.retainAll(universe);
List<JsonObject> deposits = new ArrayList<>(plan.deposits().size());
for (JsonObject deposit : plan.deposits()) {
JsonObject copy = deposit.deepCopy();
if (!included.containsAll(universe)) {
copy.add("includedBiomes", IngestJson.stringArray(included));
copy.addProperty("biomeScope", caveKeys.containsAll(included) ? "CAVE" : "SURFACE");
}
deposits.add(copy);
}
appendAll(model.pack().edit(model.dimensionPath()), "deposits", deposits);
return "dimension";
}
private String applyDecorator(FeaturePlan plan, Map<String, ModBiomeAdopter.AdoptedBiome> adopted) throws IOException {
List<String> targets = new ArrayList<>();
for (String biome : plan.declaring()) {
ModBiomeAdopter.AdoptedBiome adoptedBiome = adopted.get(biome);
if (adoptedBiome != null) {
appendAll(model.pack().require(IngestPack.biomePath(adoptedBiome.irisKey())), "decorators", List.of(plan.decorator()));
targets.add(adoptedBiome.irisKey());
}
}
for (Map.Entry<String, String> biome : model.reachableBiomes().entrySet()) {
if (plan.declaring().contains(biome.getValue())) {
appendAll(model.pack().edit(IngestPack.biomePath(biome.getKey())), "decorators", List.of(plan.decorator()));
targets.add(biome.getKey());
}
}
return String.join(",", targets);
}
private Set<String> caveKeys(Map<String, ModBiomeAdopter.AdoptedBiome> adopted) {
Set<String> caves = new TreeSet<>();
for (ModBiomeAdopter.AdoptedBiome biome : adopted.values()) {
if (biome.role() == BiomeRole.CAVE) {
caves.add(biome.modKey());
}
}
Set<String> surface = new TreeSet<>();
for (IngestPackModel.RegionProfile region : model.regions()) {
for (BiomeRole role : BiomeRole.values()) {
for (String biome : region.pool(role)) {
String derivative = model.reachableBiomes().get(biome);
(role == BiomeRole.CAVE ? caves : surface).add(derivative);
}
}
}
caves.removeAll(surface);
return caves;
}
private static void appendAll(JsonObject target, String field, List<JsonObject> values) {
JsonElement existing = target.get(field);
JsonArray array = existing != null && existing.isJsonArray() ? existing.getAsJsonArray() : new JsonArray();
for (JsonObject value : values) {
array.add(value.deepCopy());
}
target.add(field, array);
}
record FeaturePlan(String key, Set<String> declaring, Set<Integer> steps, List<JsonObject> deposits, JsonObject decorator, String reason) {
static FeaturePlan passthrough(String key, Set<String> declaring, Set<Integer> steps, String reason) {
return new FeaturePlan(key, declaring, steps, null, null, reason);
}
}
}
@@ -16,16 +16,142 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.pack.ingest;
import art.arcane.iris.pack.AtomicDirectoryPublisher;
import art.arcane.iris.pack.ingest.catalog.CatalogBiome;
import art.arcane.iris.pack.ingest.catalog.CatalogNamespace;
import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature;
import art.arcane.iris.pack.ingest.catalog.CatalogStructure;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex;
import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl;
import art.arcane.iris.studio.workspace.IrisProjectCopier;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
public final class ModIngester {
private static final Set<String> RESERVED_NAMESPACES = Set.of("minecraft", "c", "iris");
private ModIngester() {
}
public static ModIngestReport ingest(ModIngestRequest request) throws IOException {
throw new UnsupportedOperationException("Mod ingest is not implemented yet");
Objects.requireNonNull(request, "request");
Path source = request.sourcePack().toAbsolutePath().normalize();
Path target = request.targetPack().toAbsolutePath().normalize();
requireSeparate(source, target);
if (Files.exists(source.resolve(IngestManifest.PATH), LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Source pack " + source + " was written by mod ingest; ingest from its original pack instead.");
}
Set<String> namespaces = selectNamespaces(request);
prepareTarget(target, request.replaceTarget());
String targetKey = target.getFileName().toString();
IrisProjectCopier.copyProject(source.toFile(), target.getParent().toFile(), request.dimensionKey(), targetKey);
try {
return populate(request, target, targetKey, namespaces);
} catch (IOException | RuntimeException e) {
try {
AtomicDirectoryPublisher.deleteTree(target);
} catch (IOException cleanupFailure) {
e.addSuppressed(cleanupFailure);
}
throw e;
}
}
private static ModIngestReport populate(ModIngestRequest request, Path target, String targetKey, Set<String> namespaces) throws IOException {
WorldgenCatalogIndex index = WorldgenCatalogIndex.of(request.catalog());
IngestPack pack = new IngestPack(target);
IngestPackModel model = IngestPackModel.load(pack, targetKey, index);
ModFeatureIngester features = new ModFeatureIngester(model, index, namespaces);
List<ModIngestItem> biomeItems = new ArrayList<>();
Map<String, ModBiomeAdopter.AdoptedBiome> adopted = new ModBiomeAdopter(model, index, features.decoratorKeys())
.adopt(modBiomes(index, namespaces), biomeItems);
List<ModIngestItem> featureItems = features.apply(adopted);
JsonObject dimension = pack.edit(model.dimensionPath());
IrisImportedFeatureControl control = ImportedFeatureWriter.write(dimension, features.nativeKeys());
featureItems = ImportedFeatureWriter.applySuppression(featureItems, features.plans(), control);
Set<String> hostBiomes = new TreeSet<>(adopted.keySet());
hostBiomes.addAll(model.reachableBiomes().values());
List<ModIngestItem> items = new ArrayList<>(biomeItems);
items.addAll(featureItems);
items.addAll(ModStructureIngester.ingest(request.catalog().structures(), namespaces, hostBiomes, dimension));
pack.flush();
IngestManifest.write(pack, request.catalog(), request.dimensionKey(), targetKey, namespaces, items);
return new ModIngestReport(target, items, IngestManifest.summary(targetKey, pack, namespaces, items));
}
private static Set<String> selectNamespaces(ModIngestRequest request) {
Set<String> selected = new TreeSet<>();
if (!request.namespaces().isEmpty()) {
for (String namespace : request.namespaces()) {
String normalized = namespace.trim().toLowerCase(Locale.ROOT);
if (RESERVED_NAMESPACES.contains(normalized)) {
throw new IllegalArgumentException("Namespace " + normalized + " cannot be ingested; it belongs to Minecraft or Iris.");
}
selected.add(normalized);
}
return selected;
}
WorldgenCatalog catalog = request.catalog();
for (CatalogNamespace namespace : catalog.namespaces()) {
selected.add(namespace.namespace());
}
for (CatalogBiome biome : catalog.biomes()) {
selected.add(WorldgenCatalogIndex.namespaceOf(biome.key()));
}
for (CatalogPlacedFeature feature : catalog.placedFeatures()) {
selected.add(WorldgenCatalogIndex.namespaceOf(feature.key()));
}
for (CatalogStructure structure : catalog.structures()) {
selected.add(WorldgenCatalogIndex.namespaceOf(structure.key()));
}
selected.removeAll(RESERVED_NAMESPACES);
return selected;
}
private static List<CatalogBiome> modBiomes(WorldgenCatalogIndex index, Set<String> namespaces) {
List<CatalogBiome> biomes = new ArrayList<>();
for (String namespace : namespaces) {
biomes.addAll(index.biomesIn(namespace));
}
biomes.sort(Comparator.comparing(CatalogBiome::key));
return biomes;
}
private static void requireSeparate(Path source, Path target) throws IOException {
Path realSource = Files.exists(source) ? source.toRealPath() : source;
if (target.getParent() == null) {
throw new IOException("Target pack needs a parent directory: " + target);
}
for (Path candidate : List.of(source, realSource)) {
if (candidate.equals(target) || target.startsWith(candidate) || candidate.startsWith(target)) {
throw new IOException("Target pack " + target + " overlaps the source pack " + candidate);
}
}
}
private static void prepareTarget(Path target, boolean replace) throws IOException {
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) {
if (!replace) {
throw new FileAlreadyExistsException(target.toString());
}
AtomicDirectoryPublisher.deleteTree(target);
}
Files.createDirectories(target.getParent());
}
}
@@ -0,0 +1,75 @@
/*
* 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.CatalogStructure;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex;
import art.arcane.iris.structure.nativegen.IrisImportedStructureControl;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
final class ModStructureIngester {
private ModStructureIngester() {
}
static List<ModIngestItem> ingest(List<CatalogStructure> structures, Set<String> namespaces, Set<String> hostBiomes, JsonObject dimension) {
IrisImportedStructureControl control = control(dimension.get("importedStructures"));
List<CatalogStructure> selected = new ArrayList<>();
for (CatalogStructure structure : structures) {
if (namespaces.contains(WorldgenCatalogIndex.namespaceOf(structure.key()))) {
selected.add(structure);
}
}
selected.sort(Comparator.comparing(CatalogStructure::key));
List<ModIngestItem> items = new ArrayList<>(selected.size());
for (CatalogStructure structure : selected) {
String namespace = WorldgenCatalogIndex.namespaceOf(structure.key());
if (!control.shouldGenerate(structure.key())) {
items.add(new ModIngestItem(namespace, structure.key(), "structure", ModIngestTier.SKIPPED, "",
"disabled by the dimension's importedStructures"));
continue;
}
if (!structure.biomes().isEmpty() && structure.biomes().stream().noneMatch(hostBiomes::contains)) {
items.add(new ModIngestItem(namespace, structure.key(), "structure", ModIngestTier.SKIPPED, "",
"none of its biomes generate in the target dimension"));
continue;
}
items.add(new ModIngestItem(namespace, structure.key(), "structure", ModIngestTier.ADOPTED, "importedStructures",
"generates natively in its biomes"));
}
return items;
}
private static IrisImportedStructureControl control(JsonElement value) {
IrisImportedStructureControl control = new IrisImportedStructureControl();
if (value == null || !value.isJsonObject()) {
return control;
}
JsonObject object = value.getAsJsonObject();
control.setDisabled(new KList<>(IngestJson.strings(object.get("disabled"))));
control.setDisabledExact(new KList<>(IngestJson.strings(object.get("disabledExact"))));
return control;
}
}
@@ -0,0 +1,146 @@
/*
* 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.CatalogPlacedFeature;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
final class OreDepositConverter {
static final Set<String> ORE_TYPES = Set.of("minecraft:ore", "minecraft:scattered_ore");
private static final int MAX_PER_CHUNK = 2048;
private static final int MAX_SIZE = 8192;
private OreDepositConverter() {
}
static boolean handles(CatalogPlacedFeature feature) {
return ORE_TYPES.contains(VanillaJson.namespaced(feature.featureType()));
}
static List<JsonObject> convert(CatalogPlacedFeature feature, VanillaHeights.VerticalBounds bounds, int packMinY) 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");
if (size <= 0) {
throw new IngestConversionException("ore size " + size + " never places blocks");
}
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"))) {
JsonObject deposit = new JsonObject();
deposit.addProperty("minHeight", placement.band().min() - packMinY);
deposit.addProperty("maxHeight", placement.band().max() - packMinY);
deposit.addProperty("heightDistribution", placement.band().distribution());
deposit.addProperty("surfaceClearance", 0);
deposit.addProperty("minPerChunk", placement.minPerChunk());
deposit.addProperty("maxPerChunk", placement.maxPerChunk());
if (placement.spawnChance() < 1D) {
deposit.addProperty("spawnChance", IngestJson.round(placement.spawnChance()));
}
deposit.addProperty("minSize", Math.min(size, MAX_SIZE));
deposit.addProperty("maxSize", Math.min(size, MAX_SIZE));
deposit.addProperty("shape", shape);
if (discard > 0D) {
deposit.addProperty("discardChanceOnAirExposure", IngestJson.round(Math.min(1D, discard)));
}
if (!target.hosts().any()) {
deposit.add("replaceableBlocks", IngestJson.stringArray(target.hosts().blocks()));
}
JsonArray palette = new JsonArray();
palette.add(target.block());
deposit.add("palette", palette);
deposits.add(deposit);
}
if (deposits.isEmpty()) {
throw new IngestConversionException("ore has no reachable targets");
}
return deposits;
}
private static List<Target> targets(JsonArray targets) 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"));
if (hosts.any()) {
if (!claimed.isEmpty()) {
throw new IngestConversionException("ore target accepting any block follows restricted targets");
}
result.add(new Target(block, hosts));
return result;
}
SortedSet<String> remaining = new TreeSet<>(hosts.blocks());
remaining.removeAll(claimed);
if (remaining.isEmpty()) {
continue;
}
claimed.addAll(remaining);
result.add(new Target(block, VanillaRuleTests.HostSet.of(remaining)));
}
return result;
}
private static Placement placement(JsonArray modifiers, VanillaHeights.VerticalBounds bounds) throws IngestConversionException {
long minCount = 1L;
long maxCount = 1L;
double spawnChance = 1D;
VanillaHeights.HeightBand band = null;
for (JsonElement modifier : modifiers) {
JsonObject object = VanillaJson.object(modifier, "placement modifier");
String type = VanillaJson.type(object, "type");
switch (type) {
case "minecraft:count" -> {
VanillaIntProviders.IntStats stats = VanillaIntProviders.stats(object.get("count"));
minCount = Math.min(MAX_PER_CHUNK, minCount * Math.max(0, stats.min()));
maxCount = Math.min(MAX_PER_CHUNK, maxCount * Math.max(0, stats.max()));
}
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:in_square", "minecraft:biome" -> {
}
case "minecraft:height_range" -> band = VanillaHeights.band(object.get("height"), bounds);
default -> throw new IngestConversionException("unsupported ore placement modifier " + (type.isEmpty() ? object.toString() : type));
}
}
if (band == null) {
throw new IngestConversionException("ore placement has no height_range");
}
if (maxCount <= 0L || spawnChance <= 0D) {
throw new IngestConversionException("ore placement never places");
}
return new Placement((int) minCount, (int) maxCount, spawnChance, band);
}
private record Target(JsonObject block, VanillaRuleTests.HostSet hosts) {
}
private record Placement(int minPerChunk, int maxPerChunk, double spawnChance, VanillaHeights.HeightBand band) {
}
}
@@ -0,0 +1,133 @@
/*
* 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.CatalogPlacedFeature;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalogIndex;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
final class PatchDecoratorConverter {
static final String SIMPLE_BLOCK = "minecraft:simple_block";
private static final Set<String> LEGACY_PATCH_TYPES = Set.of("minecraft:flower", "minecraft:random_patch", "minecraft:no_bonemeal_flower");
private static final int LEGACY_DEFAULT_TRIES = 128;
private static final int LEGACY_DEFAULT_Y_SPREAD = 3;
private static final double COLUMNS_PER_CHUNK = 256D;
private PatchDecoratorConverter() {
}
static boolean handles(CatalogPlacedFeature feature) {
String type = VanillaJson.namespaced(feature.featureType());
return type.equals(SIMPLE_BLOCK) || LEGACY_PATCH_TYPES.contains(type);
}
static JsonObject convert(CatalogPlacedFeature feature, WorldgenCatalogIndex index) throws IngestConversionException {
String type = VanillaJson.namespaced(feature.featureType());
JsonObject config = VanillaJson.featureConfig(feature.config());
VanillaPlacementChain outer = VanillaPlacementChain.surfaceChain(VanillaJson.placement(feature.placement()));
Set<String> whitelist = new LinkedHashSet<>(outer.whitelist());
double expected;
JsonElement provider;
if (LEGACY_PATCH_TYPES.contains(type)) {
int tries = VanillaJson.integer(config, "tries", LEGACY_DEFAULT_TRIES, "patch");
int verticalSpread = Math.abs(VanillaJson.integer(config, "y_spread", LEGACY_DEFAULT_Y_SPREAD, "patch"));
double surfaceHitChance = outer.surfaceHitChance() / (verticalSpread + 1D);
NestedBlock nested = nestedBlock(config.get("feature"), index);
whitelist.addAll(nested.whitelist());
provider = nested.provider();
expected = outer.expectedPlacements() * tries * surfaceHitChance;
} else {
provider = config.get("to_place");
expected = outer.expectedPlacements() * outer.surfaceHitChance();
}
if (expected <= 0D) {
throw new IngestConversionException("patch placement never places");
}
List<JsonObject> palette = VanillaBlockStates.palette(provider);
JsonObject decorator = new JsonObject();
decorator.addProperty("chance", IngestJson.round(columnCoverage(expected)));
JsonArray paletteArray = new JsonArray(palette.size());
for (JsonObject entry : palette) {
paletteArray.add(entry);
}
decorator.add("palette", paletteArray);
if (!whitelist.isEmpty()) {
JsonArray whitelistArray = new JsonArray(whitelist.size());
for (String block : whitelist) {
JsonObject entry = new JsonObject();
entry.addProperty("block", block);
whitelistArray.add(entry);
}
decorator.add("whitelist", whitelistArray);
}
return decorator;
}
static double columnCoverage(double expectedPlacements) {
return 1D - Math.exp(-expectedPlacements / COLUMNS_PER_CHUNK);
}
private static NestedBlock nestedBlock(JsonElement holder, WorldgenCatalogIndex index) throws IngestConversionException {
if (holder != null && holder.isJsonPrimitive()) {
String key = VanillaJson.namespaced(holder.getAsString());
Optional<CatalogPlacedFeature> placed = index.placedFeature(key);
if (placed.isEmpty() || !VanillaJson.namespaced(placed.get().featureType()).equals(SIMPLE_BLOCK)) {
throw new IngestConversionException("patch feature " + key + " is not a captured simple_block feature");
}
return new NestedBlock(VanillaJson.featureConfig(placed.get().config()).get("to_place"),
VanillaPlacementChain.filterWhitelist(VanillaJson.placement(placed.get().placement())));
}
JsonObject placed = VanillaJson.object(holder, "patch feature");
JsonElement placement = placed.get("placement");
Set<String> whitelist = placement == null || placement.isJsonNull()
? Set.of()
: VanillaPlacementChain.filterWhitelist(VanillaJson.array(placement, "patch feature placement"));
return new NestedBlock(configuredToPlace(placed.get("feature"), index.catalog()), whitelist);
}
private static JsonElement configuredToPlace(JsonElement configured, WorldgenCatalog catalog) throws IngestConversionException {
if (configured != null && configured.isJsonPrimitive()) {
String key = VanillaJson.namespaced(configured.getAsString());
for (CatalogPlacedFeature candidate : catalog.placedFeatures()) {
if (VanillaJson.namespaced(candidate.configuredFeature()).equals(key)
&& VanillaJson.namespaced(candidate.featureType()).equals(SIMPLE_BLOCK)) {
return VanillaJson.featureConfig(candidate.config()).get("to_place");
}
}
throw new IngestConversionException("patch configured feature " + key + " is not a captured simple_block feature");
}
JsonObject object = VanillaJson.object(configured, "patch configured feature");
String type = VanillaJson.type(object, "type");
if (!type.equals(SIMPLE_BLOCK)) {
throw new IngestConversionException("patch places " + (type.isEmpty() ? "an unknown feature" : type) + " instead of simple_block");
}
return VanillaJson.featureConfig(object).get("to_place");
}
private record NestedBlock(JsonElement provider, Set<String> whitelist) {
}
}
@@ -0,0 +1,154 @@
/*
* 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 com.google.gson.JsonArray;
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;
final class VanillaBlockStates {
private VanillaBlockStates() {
}
static JsonObject irisBlock(JsonElement state) throws IngestConversionException {
if (state != null && state.isJsonPrimitive() && state.getAsJsonPrimitive().isString()) {
return parseStateString(state.getAsString());
}
JsonObject object = VanillaJson.object(state, "block state");
if (object.has("id")) {
return block(VanillaJson.string(object.get("id"), "block state id"), object.get("properties"));
}
if (object.has("Name")) {
return block(VanillaJson.string(object.get("Name"), "block state Name"), object.get("Properties"));
}
throw new IngestConversionException("unsupported block state " + object);
}
static List<JsonObject> palette(JsonElement provider) throws IngestConversionException {
if (provider != null && provider.isJsonPrimitive()) {
throw new IngestConversionException("block state provider reference " + provider.getAsString() + " is not captured in the catalog");
}
JsonObject object = VanillaJson.object(provider, "block state provider");
String type = VanillaJson.type(object, "type");
Map<String, JsonObject> entries = new LinkedHashMap<>();
switch (type) {
case "" -> add(entries, irisBlock(object), 1);
case "minecraft:simple_state_provider", "minecraft:simple" -> add(entries, irisBlock(object.get("state")), 1);
case "minecraft:weighted_state_provider", "minecraft:weighted" -> addWeighted(entries, VanillaJson.array(object.get("entries"), "weighted entries"));
case "minecraft:noise_provider", "minecraft:noise", "minecraft:dual_noise_provider", "minecraft:dual_noise" ->
addAll(entries, VanillaJson.array(object.get("states"), "noise states"));
case "minecraft:noise_threshold_provider", "minecraft:noise_threshold" -> {
add(entries, irisBlock(object.get("default_state")), 1);
addAll(entries, VanillaJson.array(object.get("low_states"), "noise_threshold low_states"));
addAll(entries, VanillaJson.array(object.get("high_states"), "noise_threshold high_states"));
}
default -> throw new IngestConversionException("unsupported block state provider " + type);
}
if (entries.isEmpty()) {
throw new IngestConversionException("block state provider has no states");
}
List<JsonObject> palette = new ArrayList<>(entries.size());
for (JsonObject entry : entries.values()) {
JsonObject copy = entry.deepCopy();
if (copy.has("weight") && copy.get("weight").getAsInt() == 1) {
copy.remove("weight");
}
palette.add(copy);
}
return palette;
}
private static void addWeighted(Map<String, JsonObject> entries, JsonArray weighted) throws IngestConversionException {
for (JsonElement entry : weighted) {
JsonObject entryObject = VanillaJson.object(entry, "weighted entry");
int weight = VanillaJson.integer(entryObject, "weight", 1, "weighted entry");
if (weight > 0) {
add(entries, irisBlock(entryObject.get("data")), weight);
}
}
}
private static void addAll(Map<String, JsonObject> entries, JsonArray states) throws IngestConversionException {
for (JsonElement state : states) {
add(entries, irisBlock(state), 1);
}
}
private static void add(Map<String, JsonObject> entries, JsonObject block, int weight) {
String key = block.toString();
JsonObject existing = entries.get(key);
if (existing == null) {
JsonObject entry = block.deepCopy();
entry.addProperty("weight", weight);
entries.put(key, entry);
return;
}
existing.addProperty("weight", existing.get("weight").getAsInt() + weight);
}
private static JsonObject parseStateString(String value) throws IngestConversionException {
String trimmed = value.trim();
int bracket = trimmed.indexOf('[');
if (bracket < 0) {
return block(trimmed, null);
}
if (!trimmed.endsWith("]")) {
throw new IngestConversionException("malformed block state " + value);
}
JsonObject properties = new JsonObject();
String body = trimmed.substring(bracket + 1, trimmed.length() - 1);
for (String pair : body.split(",")) {
if (pair.isBlank()) {
continue;
}
int equals = pair.indexOf('=');
if (equals <= 0) {
throw new IngestConversionException("malformed block state " + value);
}
properties.addProperty(pair.substring(0, equals).trim(), pair.substring(equals + 1).trim());
}
return block(trimmed.substring(0, bracket), properties);
}
private static JsonObject block(String id, JsonElement properties) throws IngestConversionException {
if (id.isBlank()) {
throw new IngestConversionException("block state has an empty id");
}
JsonObject block = new JsonObject();
block.addProperty("block", VanillaJson.namespaced(id));
if (properties == null || properties.isJsonNull()) {
return block;
}
JsonObject propertyObject = VanillaJson.object(properties, "block state properties");
if (propertyObject.size() == 0) {
return block;
}
JsonObject data = new JsonObject();
for (Map.Entry<String, JsonElement> property : propertyObject.entrySet()) {
data.addProperty(property.getKey(), property.getValue().getAsString());
}
block.add("data", data);
return block;
}
}
@@ -0,0 +1,90 @@
/*
* 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 com.google.gson.JsonElement;
import com.google.gson.JsonObject;
final class VanillaHeights {
static final String UNIFORM = "UNIFORM";
static final String TRIANGLE = "TRIANGLE";
private VanillaHeights() {
}
static HeightBand band(JsonElement provider, VerticalBounds bounds) throws IngestConversionException {
JsonObject object = VanillaJson.object(provider, "height provider");
String type = VanillaJson.type(object, "type");
if (type.isEmpty() && isAnchor(object)) {
int y = bounds.resolve(object);
return new HeightBand(y, y, UNIFORM);
}
return switch (type) {
case "minecraft:constant" -> constant(object, bounds);
case "minecraft:uniform" -> range(object, bounds, UNIFORM);
case "minecraft:trapezoid" -> range(object, bounds,
VanillaJson.integer(object, "plateau", 0, "trapezoid") == 0 ? TRIANGLE : UNIFORM);
default -> throw new IngestConversionException("unsupported height provider " + (type.isEmpty() ? object.toString() : type));
};
}
private static HeightBand constant(JsonObject object, VerticalBounds bounds) throws IngestConversionException {
int y = bounds.resolve(object.get("value"));
return new HeightBand(y, y, UNIFORM);
}
private static HeightBand range(JsonObject object, VerticalBounds bounds, String distribution) throws IngestConversionException {
int min = bounds.resolve(object.get("min_inclusive"));
int max = bounds.resolve(object.get("max_inclusive"));
if (max < min) {
throw new IngestConversionException("empty height range " + min + ".." + max);
}
return new HeightBand(min, max, distribution);
}
private static boolean isAnchor(JsonObject object) {
return object.has("absolute") || object.has("above_bottom") || object.has("below_top");
}
record VerticalBounds(int minY, int height) {
int top() {
return minY + height - 1;
}
int resolve(JsonElement anchor) throws IngestConversionException {
if (anchor != null && anchor.isJsonPrimitive()) {
return VanillaJson.integer(anchor, "vertical anchor");
}
JsonObject object = VanillaJson.object(anchor, "vertical anchor");
if (object.has("absolute")) {
return VanillaJson.integer(object.get("absolute"), "vertical anchor absolute");
}
if (object.has("above_bottom")) {
return minY + VanillaJson.integer(object.get("above_bottom"), "vertical anchor above_bottom");
}
if (object.has("below_top")) {
return top() - VanillaJson.integer(object.get("below_top"), "vertical anchor below_top");
}
throw new IngestConversionException("unsupported vertical anchor " + object);
}
}
record HeightBand(int min, int max, String distribution) {
}
}
@@ -0,0 +1,184 @@
/*
* 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 com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
final class VanillaIntProviders {
private static final int MAX_ENUMERATED_VALUES = 4096;
private VanillaIntProviders() {
}
static IntStats stats(JsonElement provider) throws IngestConversionException {
if (provider != null && provider.isJsonPrimitive()) {
int value = VanillaJson.integer(provider, "int provider");
return new IntStats(value, value, value);
}
JsonObject object = VanillaJson.object(provider, "int provider");
String type = VanillaJson.type(object, "type");
return switch (type) {
case "minecraft:constant" -> stats(object.get("value"));
case "minecraft:uniform" -> uniform(bounds(object));
case "minecraft:biased_to_bottom" -> biasedToBottom(bounds(object));
case "minecraft:trapezoid" -> trapezoid(object);
case "minecraft:clamped" -> clamped(object);
case "minecraft:clamped_normal" -> clampedNormal(object);
case "minecraft:weighted_list" -> weightedList(object);
default -> throw new IngestConversionException("unsupported int provider " + (type.isEmpty() ? object.toString() : type));
};
}
static double probabilityOf(JsonElement provider, int value) throws IngestConversionException {
if (provider != null && provider.isJsonPrimitive()) {
return VanillaJson.integer(provider, "int provider") == value ? 1D : 0D;
}
JsonObject object = VanillaJson.object(provider, "int provider");
String type = VanillaJson.type(object, "type");
if (type.equals("minecraft:constant")) {
return probabilityOf(object.get("value"), value);
}
if (type.equals("minecraft:trapezoid")) {
return trapezoidProbability(object, value);
}
IntStats stats = stats(provider);
if (value < stats.min() || value > stats.max()) {
return 0D;
}
return 1D / ((long) stats.max() - stats.min() + 1L);
}
private static double trapezoidProbability(JsonObject object, int value) throws IngestConversionException {
int min = VanillaJson.integer(object.get("min"), "trapezoid.min");
int max = VanillaJson.integer(object.get("max"), "trapezoid.max");
int plateau = VanillaJson.integer(object, "plateau", 0, "trapezoid");
int range = max - min;
if (range < 0 || value < min || value > max) {
return 0D;
}
int plateauStart = Math.max(0, (range - plateau) / 2);
int first = range - plateauStart;
int offset = value - min;
long pairs = 0L;
for (int i = Math.max(0, offset - plateauStart); i <= Math.min(first, offset); i++) {
pairs++;
}
return pairs / ((first + 1D) * (plateauStart + 1D));
}
private static IntStats uniform(int[] bounds) {
return new IntStats((bounds[0] + bounds[1]) / 2.0D, bounds[0], bounds[1]);
}
private static IntStats biasedToBottom(int[] bounds) {
return new IntStats(bounds[0] + (bounds[1] - bounds[0]) / 4.0D, bounds[0], bounds[1]);
}
private static IntStats trapezoid(JsonObject object) throws IngestConversionException {
int min = VanillaJson.integer(object.get("min"), "trapezoid.min");
int max = VanillaJson.integer(object.get("max"), "trapezoid.max");
return new IntStats((min + max) / 2.0D, Math.min(min, max), Math.max(min, max));
}
private static IntStats clamped(JsonObject object) throws IngestConversionException {
int low = VanillaJson.integer(object.get("min_inclusive"), "clamped.min_inclusive");
int high = VanillaJson.integer(object.get("max_inclusive"), "clamped.max_inclusive");
JsonElement source = object.get("source");
int[] uniformSource = enumerableBounds(source);
if (uniformSource != null && (long) uniformSource[1] - uniformSource[0] < MAX_ENUMERATED_VALUES) {
double total = 0D;
int count = uniformSource[1] - uniformSource[0] + 1;
for (int value = uniformSource[0]; value <= uniformSource[1]; value++) {
total += clamp(value, low, high);
}
return new IntStats(total / count, clamp(uniformSource[0], low, high), clamp(uniformSource[1], low, high));
}
IntStats inner = stats(source);
return new IntStats(Math.max(low, Math.min(high, inner.mean())), clamp(inner.min(), low, high), clamp(inner.max(), low, high));
}
private static IntStats clampedNormal(JsonObject object) throws IngestConversionException {
double mean = VanillaJson.number(object.get("mean"), "clamped_normal.mean");
int low = (int) VanillaJson.number(object.has("min_inclusive") ? object.get("min_inclusive") : object.get("min"), "clamped_normal.min");
int high = (int) VanillaJson.number(object.has("max_inclusive") ? object.get("max_inclusive") : object.get("max"), "clamped_normal.max");
return new IntStats(Math.max(low, Math.min(high, mean)), low, high);
}
private static IntStats weightedList(JsonObject object) throws IngestConversionException {
JsonArray distribution = VanillaJson.array(object.get("distribution"), "weighted_list.distribution");
double weighted = 0D;
double totalWeight = 0D;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (JsonElement entry : distribution) {
JsonObject entryObject = VanillaJson.object(entry, "weighted_list entry");
double weight = VanillaJson.number(entryObject, "weight", 1D, "weighted_list entry");
IntStats inner = stats(entryObject.get("data"));
weighted += inner.mean() * weight;
totalWeight += weight;
min = Math.min(min, inner.min());
max = Math.max(max, inner.max());
}
if (totalWeight <= 0D) {
throw new IngestConversionException("weighted_list has no weight");
}
return new IntStats(weighted / totalWeight, min, max);
}
private static int[] enumerableBounds(JsonElement source) throws IngestConversionException {
if (source != null && source.isJsonPrimitive()) {
int value = VanillaJson.integer(source, "clamped.source");
return new int[]{value, value};
}
if (source == null || !source.isJsonObject()) {
return null;
}
JsonObject object = source.getAsJsonObject();
String type = VanillaJson.type(object, "type");
if (type.equals("minecraft:uniform")) {
return bounds(object);
}
if (type.equals("minecraft:constant")) {
int value = VanillaJson.integer(object.get("value"), "clamped.source.value");
return new int[]{value, value};
}
return null;
}
private static int[] bounds(JsonObject object) throws IngestConversionException {
JsonObject holder = object.has("min_inclusive") || !object.has("value")
? object
: VanillaJson.object(object.get("value"), "int provider value");
int min = VanillaJson.integer(holder.get("min_inclusive"), "int provider min_inclusive");
int max = VanillaJson.integer(holder.get("max_inclusive"), "int provider max_inclusive");
if (max < min) {
throw new IngestConversionException("int provider max_inclusive " + max + " is below min_inclusive " + min);
}
return new int[]{min, max};
}
private static int clamp(int value, int low, int high) {
return Math.max(low, Math.min(high, value));
}
record IntStats(double mean, int min, int max) {
}
}
@@ -0,0 +1,132 @@
/*
* 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 com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.Locale;
final class VanillaJson {
private VanillaJson() {
}
static String namespaced(String key) {
String normalized = key.trim().toLowerCase(Locale.ROOT);
if (normalized.startsWith("#")) {
normalized = normalized.substring(1);
}
return normalized.indexOf(':') < 0 ? "minecraft:" + normalized : normalized;
}
static JsonObject featureConfig(JsonElement config) throws IngestConversionException {
if (config == null || config.isJsonNull()) {
throw new IngestConversionException("feature config was not captured in the catalog");
}
JsonObject object = object(config, "feature config");
JsonElement nested = object.get("config");
if (nested != null && nested.isJsonObject() && !object.has("targets") && !object.has("to_place")) {
return nested.getAsJsonObject();
}
return object;
}
static JsonArray placement(JsonElement placement) throws IngestConversionException {
if (placement == null || placement.isJsonNull()) {
throw new IngestConversionException("placement modifiers were not captured in the catalog");
}
return array(placement, "placement modifiers");
}
static String type(JsonObject object, String field) {
JsonElement value = object.get(field);
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
return "";
}
return namespaced(value.getAsString());
}
static JsonObject object(JsonElement element, String context) throws IngestConversionException {
if (element == null || !element.isJsonObject()) {
throw new IngestConversionException(context + " is not a JSON object");
}
return element.getAsJsonObject();
}
static JsonArray array(JsonElement element, String context) throws IngestConversionException {
if (element == null || !element.isJsonArray()) {
throw new IngestConversionException(context + " is not a JSON array");
}
return element.getAsJsonArray();
}
static String string(JsonElement element, String context) throws IngestConversionException {
if (element == null || !element.isJsonPrimitive() || !element.getAsJsonPrimitive().isString()) {
throw new IngestConversionException(context + " is not a string");
}
return element.getAsString();
}
static double number(JsonElement element, String context) throws IngestConversionException {
JsonPrimitive primitive = numeric(element, context);
double value = primitive.getAsDouble();
if (!Double.isFinite(value)) {
throw new IngestConversionException(context + " is not a finite number");
}
return value;
}
static int integer(JsonElement element, String context) throws IngestConversionException {
double value = number(element, context);
if (value != Math.rint(value) || value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) {
throw new IngestConversionException(context + " is not an integer");
}
return (int) value;
}
static double number(JsonObject object, String field, double fallback, String context) throws IngestConversionException {
JsonElement value = object.get(field);
return value == null || value.isJsonNull() ? fallback : number(value, context + "." + field);
}
static int integer(JsonObject object, String field, int fallback, String context) throws IngestConversionException {
JsonElement value = object.get(field);
return value == null || value.isJsonNull() ? fallback : integer(value, context + "." + field);
}
private static JsonPrimitive numeric(JsonElement element, String context) throws IngestConversionException {
if (element == null || !element.isJsonPrimitive()) {
throw new IngestConversionException(context + " is not a number");
}
JsonPrimitive primitive = element.getAsJsonPrimitive();
if (primitive.isNumber()) {
return primitive;
}
if (primitive.isString()) {
try {
return new JsonPrimitive(Double.parseDouble(primitive.getAsString().trim()));
} catch (NumberFormatException e) {
throw new IngestConversionException(context + " is not a number: " + primitive.getAsString());
}
}
throw new IngestConversionException(context + " is not a number");
}
}
@@ -0,0 +1,158 @@
/*
* 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 com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.LinkedHashSet;
import java.util.Set;
final class VanillaPlacementChain {
private static final Set<String> SURFACE_HEIGHTMAPS = Set.of("MOTION_BLOCKING", "MOTION_BLOCKING_NO_LEAVES", "WORLD_SURFACE", "WORLD_SURFACE_WG");
private static final int NOISE_SAMPLES = 201;
private final Set<String> whitelist = new LinkedHashSet<>();
private double expectedPlacements = 1D;
private double surfaceHitChance = 1D;
private boolean surface;
private VanillaPlacementChain() {
}
static VanillaPlacementChain surfaceChain(JsonArray placement) throws IngestConversionException {
VanillaPlacementChain chain = new VanillaPlacementChain();
chain.read(placement);
if (!chain.surface) {
throw new IngestConversionException("placement does not snap to a surface heightmap");
}
return chain;
}
static Set<String> filterWhitelist(JsonArray placement) throws IngestConversionException {
VanillaPlacementChain chain = new VanillaPlacementChain();
for (JsonElement modifier : placement) {
JsonObject object = VanillaJson.object(modifier, "placement modifier");
String type = VanillaJson.type(object, "type");
if (!type.equals("minecraft:block_predicate_filter")) {
throw new IngestConversionException("unsupported nested placement modifier " + type);
}
chain.collectWhitelist(object.get("predicate"));
}
return chain.whitelist;
}
double expectedPlacements() {
return expectedPlacements;
}
double surfaceHitChance() {
return surfaceHitChance;
}
Set<String> whitelist() {
return whitelist;
}
private void read(JsonArray placement) throws IngestConversionException {
for (JsonElement modifier : placement) {
JsonObject object = VanillaJson.object(modifier, "placement modifier");
String type = VanillaJson.type(object, "type");
switch (type) {
case "minecraft:count" -> expectedPlacements *= Math.max(0D, VanillaIntProviders.stats(object.get("count")).mean());
case "minecraft:rarity_filter" -> expectedPlacements /= Math.max(1D, VanillaJson.number(object.get("chance"), "rarity_filter.chance"));
case "minecraft:random_chance" -> expectedPlacements *= clampProbability(VanillaJson.number(object.get("chance"), "random_chance.chance"));
case "minecraft:noise_threshold_count" -> expectedPlacements *= (VanillaJson.number(object.get("below_noise"), "noise_threshold_count.below_noise")
+ VanillaJson.number(object.get("above_noise"), "noise_threshold_count.above_noise")) / 2D;
case "minecraft:noise_based_count" -> expectedPlacements *= noiseBasedMean(object);
case "minecraft:in_square", "minecraft:biome" -> {
}
case "minecraft:heightmap" -> readHeightmap(object);
case "minecraft:offset" -> readVerticalOffset(object.get("y"));
case "minecraft:random_offset" -> readVerticalOffset(object.get("y_spread"));
case "minecraft:block_predicate_filter" -> collectWhitelist(object.get("predicate"));
default -> throw new IngestConversionException("unsupported placement modifier " + (type.isEmpty() ? object.toString() : type));
}
}
}
private void readHeightmap(JsonObject object) throws IngestConversionException {
String heightmap = VanillaJson.string(object.get("heightmap"), "heightmap.heightmap");
if (!SURFACE_HEIGHTMAPS.contains(heightmap)) {
throw new IngestConversionException("heightmap " + heightmap + " is not a dry surface placement");
}
surface = true;
}
private void readVerticalOffset(JsonElement provider) throws IngestConversionException {
if (!surface || provider == null || provider.isJsonNull()) {
return;
}
surfaceHitChance *= VanillaIntProviders.probabilityOf(provider, 0);
}
private void collectWhitelist(JsonElement predicate) throws IngestConversionException {
if (predicate == null || !predicate.isJsonObject()) {
return;
}
JsonObject object = predicate.getAsJsonObject();
String type = VanillaJson.type(object, "type");
if (type.equals("minecraft:all_of") && object.get("predicates") != null && object.get("predicates").isJsonArray()) {
for (JsonElement inner : object.getAsJsonArray("predicates")) {
collectWhitelist(inner);
}
return;
}
if (!type.equals("minecraft:matching_blocks") || !isBelowOffset(object.get("offset"))) {
return;
}
JsonElement blocks = object.get("blocks");
if (blocks != null && blocks.isJsonArray()) {
for (JsonElement block : blocks.getAsJsonArray()) {
whitelist.add(VanillaJson.namespaced(VanillaJson.string(block, "matching_blocks.blocks")));
}
return;
}
whitelist.add(VanillaJson.namespaced(VanillaJson.string(blocks, "matching_blocks.blocks")));
}
private static boolean isBelowOffset(JsonElement offset) {
if (offset == null || !offset.isJsonArray() || offset.getAsJsonArray().size() != 3) {
return false;
}
JsonArray values = offset.getAsJsonArray();
return values.get(0).getAsInt() == 0 && values.get(1).getAsInt() == -1 && values.get(2).getAsInt() == 0;
}
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");
double total = 0D;
for (int sample = 0; sample < NOISE_SAMPLES; sample++) {
double noise = -1D + 2D * sample / (NOISE_SAMPLES - 1D);
total += Math.max(0D, Math.ceil((noise + offset) * ratio));
}
return total / NOISE_SAMPLES;
}
private static double clampProbability(double value) {
return Math.max(0D, Math.min(1D, value));
}
}
@@ -0,0 +1,141 @@
/*
* 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 com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
final class VanillaRuleTests {
private static final Map<String, List<String>> BLOCK_TAGS = Map.of(
"minecraft:stone_ore_replaceables", List.of("minecraft:stone", "minecraft:granite", "minecraft:diorite", "minecraft:andesite"),
"minecraft:deepslate_ore_replaceables", List.of("minecraft:deepslate"),
"minecraft:height_specific_ore_replaceables", List.of("minecraft:tuff"),
"minecraft:base_stone_overworld", List.of("minecraft:stone", "minecraft:granite", "minecraft:diorite",
"minecraft:andesite", "minecraft:tuff", "minecraft:deepslate"),
"minecraft:base_stone_nether", List.of("minecraft:netherrack", "minecraft:basalt", "minecraft:blackstone"));
private VanillaRuleTests() {
}
static HostSet hosts(JsonElement ruleTest) 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: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));
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);
if (blocks == null) {
throw new IngestConversionException("ore target uses block tag " + tag + " whose members are not known offline");
}
return HostSet.of(Set.copyOf(blocks));
}
private static HostSet anyOf(List<JsonElement> rules) throws IngestConversionException {
SortedSet<String> union = new TreeSet<>();
for (JsonElement rule : rules) {
HostSet hosts = hosts(rule);
if (hosts.any()) {
return HostSet.ANY;
}
union.addAll(hosts.blocks());
}
return HostSet.of(union);
}
private static HostSet allOf(List<JsonElement> rules) throws IngestConversionException {
HostSet positive = null;
SortedSet<String> negative = new TreeSet<>();
for (JsonElement rule : rules) {
JsonObject ruleObject = VanillaJson.object(rule, "all_of rule");
String type = VanillaJson.type(ruleObject, "predicate_type");
if (type.equals("minecraft:height_match")) {
continue;
}
if (type.equals("minecraft:not")) {
HostSet excluded = hosts(ruleObject.get("rule"));
if (excluded.any()) {
throw new IngestConversionException("ore target predicate never matches");
}
negative.addAll(excluded.blocks());
continue;
}
positive = intersect(positive, hosts(ruleObject));
}
if (positive == null) {
throw new IngestConversionException("ore target all_of has no block constraint");
}
if (positive.any()) {
if (!negative.isEmpty()) {
throw new IngestConversionException("ore target excludes blocks from an unrestricted host set");
}
return HostSet.ANY;
}
SortedSet<String> result = new TreeSet<>(positive.blocks());
result.removeAll(negative);
return HostSet.of(result);
}
private static HostSet intersect(HostSet current, HostSet next) {
if (current == null || current.any()) {
return next;
}
if (next.any()) {
return current;
}
SortedSet<String> result = new TreeSet<>(current.blocks());
result.retainAll(next.blocks());
return HostSet.of(result);
}
private static List<JsonElement> rules(JsonObject object) throws IngestConversionException {
JsonElement value = object.has("rules") ? object.get("rules") : object.get("predicates");
JsonArray array = VanillaJson.array(value, "ore target predicate rules");
List<JsonElement> rules = new ArrayList<>(array.size());
for (JsonElement rule : array) {
rules.add(rule);
}
return rules;
}
record HostSet(boolean any, SortedSet<String> blocks) {
static final HostSet ANY = new HostSet(true, Collections.emptySortedSet());
static HostSet of(Set<String> blocks) {
return new HostSet(false, Collections.unmodifiableSortedSet(new TreeSet<>(blocks)));
}
}
}