Merged mod ingester

This commit is contained in:
Brian Neumann-Fopiano
2026-10-14 20:12:47 -04:00
27 changed files with 3793 additions and 2 deletions
@@ -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)));
}
}
}
@@ -0,0 +1,147 @@
/*
* 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.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class FeatureConverterFormatTest {
private static final VanillaHeights.VerticalBounds OVERWORLD = new VanillaHeights.VerticalBounds(-64, 384);
private static final String PATCH_TAIL = """
{"type": "minecraft:offset",
"x": {"type": "minecraft:trapezoid", "max": 7, "min": -7, "plateau": 0},
"y": {"type": "minecraft:trapezoid", "max": 3, "min": -3, "plateau": 0},
"z": {"type": "minecraft:trapezoid", "max": 7, "min": -7, "plateau": 0}},
{"type": "minecraft:block_predicate_filter", "predicate": {"type": "minecraft:matching_block_tag", "tag": "minecraft:air"}}
""";
@Test
public void currentVanillaIronOreSplitsIntoStoneAndDeepslateDeposits() throws Exception {
CatalogPlacedFeature iron = feature("minecraft:ore", """
{"type": "minecraft:ore", "discard_chance_on_air_exposure": 0.0, "size": 9, "targets": [
{"state": "minecraft:iron_ore", "target": {"predicate_type": "minecraft:any_of", "rules": [
{"predicate_type": "minecraft:all_of", "rules": [
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"},
{"max_inclusive": 2031, "min_inclusive": 0, "predicate_type": "minecraft:height_match"}]},
{"predicate_type": "minecraft:all_of", "rules": [
{"predicate_type": "minecraft:not", "rule": {"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"}},
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:stone_ore_replaceables"}]}]}},
{"state": "minecraft:deepslate_iron_ore", "target": {"predicate_type": "minecraft:any_of", "rules": [
{"predicate_type": "minecraft:all_of", "rules": [
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"},
{"max_inclusive": 8, "min_inclusive": -2032, "predicate_type": "minecraft:height_match"}]},
{"predicate_type": "minecraft:all_of", "rules": [
{"predicate_type": "minecraft:not", "rule": {"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"}},
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:deepslate_ore_replaceables"}]}]}}]}
""", """
[{"type": "minecraft:count", "count": 10}, {"type": "minecraft:in_square"},
{"type": "minecraft:height_range", "height": {"type": "minecraft:trapezoid", "max_inclusive": {"absolute": 56}, "min_inclusive": {"absolute": -24}}},
{"type": "minecraft:biome"}]
""");
List<JsonObject> deposits = OreDepositConverter.convert(iron, OVERWORLD, -64);
assertEquals(2, deposits.size());
assertEquals("minecraft:iron_ore", deposits.get(0).getAsJsonArray("palette").get(0).getAsJsonObject().get("block").getAsString());
assertEquals(40, deposits.get(0).get("minHeight").getAsInt());
assertEquals(120, deposits.get(0).get("maxHeight").getAsInt());
assertEquals("TRIANGLE", deposits.get(0).get("heightDistribution").getAsString());
assertEquals(10, deposits.get(0).get("maxPerChunk").getAsInt());
assertEquals(List.of("minecraft:deepslate"), IngestJson.strings(deposits.get(1).get("replaceableBlocks")));
}
@Test
public void environmentScanOrePassesThroughWithTheModifierNamed() {
CatalogPlacedFeature buried = feature("minecraft:ore",
"{\"size\": 4, \"targets\": [{\"state\": \"minecraft:stone\", \"target\": {\"predicate_type\": \"minecraft:always_true\"}}]}",
"[{\"type\": \"minecraft:environment_scan\", \"direction_of_search\": \"down\", \"max_steps\": 16, \"target_condition\": {\"type\": \"minecraft:solid\"}}]");
IngestConversionException failure = assertThrows(IngestConversionException.class, () -> OreDepositConverter.convert(buried, OVERWORLD, -64));
assertTrue(failure.getMessage(), failure.getMessage().contains("minecraft:environment_scan"));
}
@Test
public void densePatchCountsBeforeAndAfterTheHeightmapSaturateCoverage() throws Exception {
CatalogPlacedFeature lavender = feature(PatchDecoratorConverter.SIMPLE_BLOCK,
"{\"type\": \"minecraft:simple_block\", \"to_place\": {\"id\": \"examplemod:lavender\"}}",
"[{\"type\": \"minecraft:count\", \"count\": 15}, {\"type\": \"minecraft:in_square\"},"
+ "{\"type\": \"minecraft:heightmap\", \"heightmap\": \"WORLD_SURFACE_WG\"}, {\"type\": \"minecraft:biome\"},"
+ "{\"type\": \"minecraft:count\", \"count\": 96}," + PATCH_TAIL + "]");
JsonObject decorator = PatchDecoratorConverter.convert(lavender, index());
assertEquals(1.0D - Math.exp(-(15.0D * 96.0D * 0.25D) / 256.0D), decorator.get("chance").getAsDouble(), 0.000001D);
assertTrue(decorator.get("chance").getAsDouble() < 1.0D);
}
@Test
public void unnamespacedTypesAndClampedCountsConvert() throws Exception {
CatalogPlacedFeature flowers = feature("simple_block", "{\"type\": \"simple_block\", \"to_place\": {\"id\": \"minecraft:poppy\"}}", """
[{"type": "rarity_filter", "chance": 2}, {"type": "minecraft:in_square"},
{"type": "count", "count": {"type": "minecraft:clamped", "min_inclusive": 0, "max_inclusive": 64,
"source": {"type": "minecraft:uniform", "min_inclusive": -3, "max_inclusive": 1}}},
{"type": "minecraft:heightmap", "heightmap": "MOTION_BLOCKING_NO_LEAVES"},
{"type": "minecraft:offset", "x": {"type": "minecraft:trapezoid", "plateau": 0, "min": -7, "max": 7},
"y": {"type": "minecraft:trapezoid", "plateau": 0, "min": -3, "max": 3}, "z": {"type": "minecraft:trapezoid", "plateau": 0, "min": -7, "max": 7}},
{"type": "minecraft:block_predicate_filter", "predicate": {"type": "minecraft:all_of", "predicates": [
{"type": "minecraft:matching_blocks", "blocks": ["minecraft:grass_block"], "offset": [0, -1, 0]}]}},
{"type": "minecraft:biome"}]
""");
JsonObject decorator = PatchDecoratorConverter.convert(flowers, index());
assertEquals(1.0D - Math.exp(-(0.5D * 0.2D * 0.25D) / 256.0D), decorator.get("chance").getAsDouble(), 0.000001D);
assertEquals("minecraft:grass_block", decorator.getAsJsonArray("whitelist").get(0).getAsJsonObject().get("block").getAsString());
}
@Test
public void underwaterAndRegistryProviderPatchesPassThrough() {
CatalogPlacedFeature seagrass = feature(PatchDecoratorConverter.SIMPLE_BLOCK,
"{\"to_place\": {\"id\": \"minecraft:seagrass\"}}",
"[{\"type\": \"minecraft:heightmap\", \"heightmap\": \"OCEAN_FLOOR_WG\"}]");
CatalogPlacedFeature plains = feature(PatchDecoratorConverter.SIMPLE_BLOCK,
"{\"type\": \"minecraft:simple_block\", \"to_place\": \"minecraft:flower_plain\"}",
"[{\"type\": \"minecraft:heightmap\", \"heightmap\": \"MOTION_BLOCKING\"}]");
assertThrows(IngestConversionException.class, () -> PatchDecoratorConverter.convert(seagrass, index()));
IngestConversionException reference = assertThrows(IngestConversionException.class, () -> PatchDecoratorConverter.convert(plains, index()));
assertTrue(reference.getMessage(), reference.getMessage().contains("minecraft:flower_plain"));
}
private static CatalogPlacedFeature feature(String type, String config, String placement) {
return new CatalogPlacedFeature("examplemod:sample", "examplemod:sample", type,
JsonParser.parseString(config), JsonParser.parseString(placement));
}
private static WorldgenCatalogIndex index() {
return WorldgenCatalogIndex.of(new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 0L, "",
List.of(), List.of(), List.of(), List.of(), List.of(), List.of()));
}
}
@@ -0,0 +1,411 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.pack.ingest;
import art.arcane.iris.pack.ingest.catalog.CatalogBiome;
import art.arcane.iris.pack.ingest.catalog.CatalogDimensionType;
import art.arcane.iris.pack.ingest.catalog.CatalogNamespace;
import art.arcane.iris.pack.ingest.catalog.CatalogPlacedFeature;
import art.arcane.iris.pack.ingest.catalog.CatalogStructure;
import art.arcane.iris.pack.ingest.catalog.WorldgenCatalog;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
final class ModIngestFixture {
static final String MOD = "examplemod";
static final String OTHER_MOD = "othermod";
static final int ORE_STEP = 6;
static final int VEGETAL_STEP = 9;
private static final List<String> OVERWORLD = List.of("minecraft:is_overworld");
private static final String STONE_THEN_DEEPSLATE_TIN = """
{
"type": "minecraft:ore",
"discard_chance_on_air_exposure": 0.0,
"size": 7,
"targets": [
{
"state": "examplemod:tin_ore",
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"},
{"predicate_type": "minecraft:height_match", "min_inclusive": 0, "max_inclusive": 2031}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{"predicate_type": "minecraft:not", "rule": {"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"}},
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:stone_ore_replaceables"}
]
}
]
}
},
{
"state": {"id": "examplemod:deepslate_tin_ore"},
"target": {
"predicate_type": "minecraft:any_of",
"rules": [
{
"predicate_type": "minecraft:all_of",
"rules": [
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"},
{"predicate_type": "minecraft:height_match", "min_inclusive": -2032, "max_inclusive": 8}
]
},
{
"predicate_type": "minecraft:all_of",
"rules": [
{"predicate_type": "minecraft:not", "rule": {"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"}},
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:deepslate_ore_replaceables"}
]
}
]
}
}
]
}
""";
private static final String TIN_PLACEMENT = """
[
{"type": "minecraft:count", "count": 12},
{"type": "minecraft:rarity_filter", "chance": 2},
{"type": "minecraft:in_square"},
{"type": "minecraft:height_range", "height": {"type": "minecraft:uniform", "min_inclusive": {"above_bottom": 16}, "max_inclusive": {"below_top": 20}}},
{"type": "minecraft:biome"}
]
""";
private static final String RUBY_CONFIG = """
{
"discard_chance_on_air_exposure": 0.5,
"size": 3,
"targets": [
{"state": {"Name": "examplemod:ruby_ore", "Properties": {"lit": "false"}}, "target": {"predicate_type": "minecraft:block_match", "block": "minecraft:stone"}}
]
}
""";
private static final String RUBY_PLACEMENT = """
[
{"type": "minecraft:count", "count": {"type": "minecraft:uniform", "min_inclusive": 1, "max_inclusive": 3}},
{"type": "minecraft:in_square"},
{"type": "minecraft:height_range", "height": {"type": "minecraft:trapezoid", "min_inclusive": {"absolute": -16}, "max_inclusive": {"absolute": 48}, "plateau": 0}},
{"type": "minecraft:biome"}
]
""";
private static final String SAPPHIRE_CONFIG = """
{"size": 4, "targets": [{"state": "examplemod:sapphire_ore", "target": {"predicate_type": "minecraft:tag_match", "tag": "minecraft:base_stone_overworld"}}]}
""";
private static final String SAPPHIRE_PLACEMENT = """
[
{"type": "count", "count": 2},
{"type": "minecraft:height_range", "height": {"type": "minecraft:uniform", "min_inclusive": {"absolute": 0}, "max_inclusive": {"absolute": 32}}}
]
""";
private static final String GLOW_FLOWERS_CONFIG = """
{
"type": "minecraft:simple_block",
"to_place": {
"type": "minecraft:weighted",
"entries": [
{"data": "examplemod:glowcap", "weight": 3},
{"data": {"id": "examplemod:glowbell", "properties": {"lit": "true"}}, "weight": 1}
]
}
}
""";
private static final String GLOW_FLOWERS_PLACEMENT = """
[
{"type": "minecraft:rarity_filter", "chance": 4},
{"type": "minecraft:in_square"},
{"type": "minecraft:heightmap", "heightmap": "MOTION_BLOCKING"},
{"type": "minecraft:biome"},
{"type": "minecraft:count", "count": 32},
{"type": "minecraft:offset", "x": {"type": "minecraft:trapezoid", "min": -7, "max": 7, "plateau": 0}, "y": {"type": "minecraft:trapezoid", "min": -3, "max": 3, "plateau": 0}, "z": {"type": "minecraft:trapezoid", "min": -7, "max": 7, "plateau": 0}},
{"type": "minecraft:block_predicate_filter", "predicate": {"type": "minecraft:all_of", "predicates": [{"type": "minecraft:matching_block_tag", "tag": "minecraft:air"}, {"type": "minecraft:matching_blocks", "blocks": "minecraft:grass_block", "offset": [0, -1, 0]}]}}
]
""";
private static final String BLUEBELL_PATCH_CONFIG = """
{
"tries": 64,
"xz_spread": 7,
"y_spread": 3,
"feature": {
"feature": {"type": "minecraft:simple_block", "config": {"to_place": {"type": "minecraft:simple_state_provider", "state": {"Name": "examplemod:bluebell"}}}},
"placement": [{"type": "minecraft:block_predicate_filter", "predicate": {"type": "minecraft:matching_blocks", "blocks": ["minecraft:air"]}}]
}
}
""";
private static final String BLUEBELL_PATCH_PLACEMENT = """
[
{"type": "minecraft:rarity_filter", "chance": 2},
{"type": "minecraft:in_square"},
{"type": "minecraft:heightmap", "heightmap": "MOTION_BLOCKING"},
{"type": "minecraft:biome"}
]
""";
private static final String BURIED_TIN_PLACEMENT = """
[
{"type": "minecraft:count", "count": 2},
{"type": "minecraft:environment_scan", "direction_of_search": "down", "max_steps": 8, "target_condition": {"type": "minecraft:solid"}}
]
""";
private static final String WEIRD_ORE_CONFIG = """
{"size": 5, "targets": [{"state": "examplemod:weird_ore", "target": {"predicate_type": "minecraft:tag_match", "tag": "examplemod:custom_stone"}}]}
""";
private ModIngestFixture() {
}
static Path writeSourcePack(Path root) throws IOException {
Path pack = root.resolve("source");
write(pack, "dimensions/overworld.json", """
{
"name": "Overworld",
"environment": "NORMAL",
"regions": ["hot", "temperate"],
"dimensionHeight": {"min": -128, "max": 320},
"deposits": [{"minHeight": 10, "maxHeight": 40, "palette": [{"block": "minecraft:coal_ore"}]}],
"importedStructures": {"disabled": ["examplemod:old_ruin"]},
"importedFeatures": {"enabled": false, "disabled": ["minecraft:ore_diamond"], "disabledSteps": ["LAKES"]}
}
""");
write(pack, "regions/hot.json", """
{
"name": "Hot",
"landBiomes": ["hot/desert", "hot/savanna"],
"seaBiomes": ["hot/warm-ocean"],
"shoreBiomes": ["hot/beach"],
"caveBiomes": ["caves/dripstone"]
}
""");
write(pack, "regions/temperate.json", """
{
"name": "Temperate",
"landBiomes": ["temperate/forest", "temperate/plains"],
"seaBiomes": ["temperate/ocean"],
"shoreBiomes": ["temperate/beach"]
}
""");
write(pack, "biomes/hot/desert.json", """
{
"name": "Desert",
"derivative": "minecraft:desert",
"layers": [{"palette": [{"block": "minecraft:sand"}]}],
"objects": [{"chance": 0.1, "place": ["desert/cactus"]}]
}
""");
write(pack, "biomes/hot/savanna.json", """
{"name": "Savanna", "derivative": "minecraft:savanna", "children": ["hot/desert"]}
""");
write(pack, "biomes/hot/warm-ocean.json", """
{"name": "Warm Ocean", "derivative": "minecraft:warm_ocean"}
""");
write(pack, "biomes/hot/beach.json", """
{"name": "Hot Beach", "derivative": "minecraft:beach"}
""");
write(pack, "biomes/caves/dripstone.json", """
{"name": "Dripstone", "derivative": "minecraft:dripstone_caves"}
""");
write(pack, "biomes/temperate/forest.json", """
{
"name": "Forest",
"derivative": "minecraft:forest",
"vanillaDerivative": "minecraft:forest",
"customDerivitives": [{"id": "forest", "grassColor": "#91BD59"}],
"biomeScatter": ["minecraft:birch_forest"],
"children": ["temperate/plains"],
"objects": [{"chance": 0.4, "place": ["trees/oak"]}],
"proceduralObjects": {"trees": [{"chance": 0.2}], "ruins": [{"chance": 0.01}]},
"decorators": [{"chance": 0.3, "palette": [{"block": "minecraft:short_grass"}]}]
}
""");
write(pack, "biomes/temperate/plains.json", """
{"name": "Plains", "derivative": "plains"}
""");
write(pack, "biomes/temperate/ocean.json", """
{"name": "Ocean", "derivative": "minecraft:ocean"}
""");
write(pack, "biomes/temperate/beach.json", """
{"name": "Beach", "derivative": "minecraft:beach"}
""");
write(pack, "biomes/unused/orphan.json", """
{"name": "Orphan", "derivative": "minecraft:forest"}
""");
write(pack, ".git/HEAD", "ref: refs/heads/main\n");
write(pack, "source.code-workspace", "{}");
return pack;
}
static WorldgenCatalog catalog() {
List<CatalogBiome> biomes = new ArrayList<>();
List<String> sharedOres = List.of("minecraft:ore_iron", MOD + ":ore_tin", MOD + ":ore_tin_buried");
biomes.add(biome("minecraft:desert", 2.0F, 0.0F, OVERWORLD, steps(ORE_STEP, concat(sharedOres, MOD + ":ore_sapphire"))));
biomes.add(biome("minecraft:savanna", 1.2F, 0.4F, OVERWORLD, steps(ORE_STEP, sharedOres)));
biomes.add(biome("minecraft:warm_ocean", 0.5F, 0.5F, List.of("minecraft:is_overworld", "minecraft:is_ocean"), steps(ORE_STEP, sharedOres)));
biomes.add(biome("minecraft:ocean", 0.5F, 0.5F, List.of("minecraft:is_overworld", "minecraft:is_ocean"), steps(ORE_STEP, sharedOres)));
biomes.add(biome("minecraft:beach", 0.8F, 0.4F, List.of("minecraft:is_overworld", "minecraft:is_beach"), steps(ORE_STEP, sharedOres)));
biomes.add(biome("minecraft:dripstone_caves", 0.8F, 0.4F, OVERWORLD, steps(ORE_STEP, sharedOres)));
biomes.add(biome("minecraft:forest", 0.7F, 0.8F, OVERWORLD, merge(steps(ORE_STEP, sharedOres),
steps(VEGETAL_STEP, List.of("minecraft:trees_birch_and_oak", MOD + ":glow_flowers")))));
biomes.add(biome("minecraft:plains", 0.8F, 0.4F, OVERWORLD, merge(steps(ORE_STEP, sharedOres),
steps(VEGETAL_STEP, List.of(MOD + ":bluebell_patch", OTHER_MOD + ":daisies")))));
biomes.add(biome("minecraft:river", 0.5F, 0.5F, List.of("minecraft:is_overworld", "minecraft:is_river"), steps(ORE_STEP, sharedOres)));
biomes.add(biome("minecraft:nether_wastes", 2.0F, 0.0F, List.of("minecraft:is_nether"), steps(ORE_STEP, List.of())));
biomes.add(biome(MOD + ":scorched_mesa", 1.9F, 0.05F, OVERWORLD, steps(ORE_STEP, concat(sharedOres, MOD + ":ore_ruby"))));
biomes.add(biome(MOD + ":maple_woods", 0.6F, 0.8F, List.of("minecraft:is_overworld", "c:is_forest"), merge(steps(ORE_STEP, sharedOres),
steps(VEGETAL_STEP, List.of(MOD + ":maple_trees", MOD + ":glow_flowers", "minecraft:patch_grass")))));
biomes.add(biome(MOD + ":coral_depths", 0.5F, 0.5F, List.of("minecraft:is_overworld", "minecraft:is_ocean"), steps(ORE_STEP, sharedOres)));
biomes.add(biome(MOD + ":dune_beach", 1.8F, 0.1F, List.of("minecraft:is_overworld", "minecraft:is_beach"), steps(ORE_STEP, sharedOres)));
biomes.add(biome(MOD + ":crystal_caves", 0.8F, 0.4F, List.of("minecraft:is_overworld", "c:is_cave"),
steps(ORE_STEP, concat(sharedOres, MOD + ":weird_ore"))));
biomes.add(biome(MOD + ":misty_river", 0.5F, 0.5F, List.of("minecraft:is_overworld", "minecraft:is_river"), steps(ORE_STEP, sharedOres)));
biomes.add(biome(MOD + ":ashen_wastes", 2.0F, 0.0F, List.of("minecraft:is_nether"), steps(ORE_STEP, List.of(MOD + ":nether_ore"))));
biomes.add(biome(MOD + ":void_garden", 0.5F, 0.5F, List.of(), steps(ORE_STEP, List.of())));
biomes.add(biome(OTHER_MOD + ":bamboo_marsh", 0.9F, 0.9F, OVERWORLD, steps(VEGETAL_STEP, List.of(OTHER_MOD + ":daisies"))));
List<CatalogPlacedFeature> features = new ArrayList<>();
features.add(feature("minecraft:ore_iron", "minecraft:ore", "{\"size\": 9, \"targets\": []}", "[]"));
features.add(feature(MOD + ":ore_tin", "minecraft:ore", STONE_THEN_DEEPSLATE_TIN, TIN_PLACEMENT));
features.add(feature(MOD + ":ore_tin_buried", "minecraft:ore", STONE_THEN_DEEPSLATE_TIN, BURIED_TIN_PLACEMENT));
features.add(feature(MOD + ":ore_ruby", "minecraft:scattered_ore", RUBY_CONFIG, RUBY_PLACEMENT));
features.add(feature(MOD + ":ore_sapphire", "minecraft:ore", SAPPHIRE_CONFIG, SAPPHIRE_PLACEMENT));
features.add(feature(MOD + ":weird_ore", "minecraft:ore", WEIRD_ORE_CONFIG, SAPPHIRE_PLACEMENT));
features.add(feature(MOD + ":nether_ore", "minecraft:ore", WEIRD_ORE_CONFIG, "[]"));
features.add(feature(MOD + ":glow_flowers", "minecraft:simple_block", GLOW_FLOWERS_CONFIG, GLOW_FLOWERS_PLACEMENT));
features.add(feature(MOD + ":bluebell_patch", "minecraft:random_patch", BLUEBELL_PATCH_CONFIG, BLUEBELL_PATCH_PLACEMENT));
features.add(feature(MOD + ":maple_trees", "minecraft:tree", "{\"trunk_provider\": {}}", "[]"));
features.add(feature(OTHER_MOD + ":daisies", "minecraft:simple_block",
"{\"to_place\": {\"id\": \"othermod:daisy\"}}", GLOW_FLOWERS_PLACEMENT));
List<CatalogStructure> structures = List.of(
structure(MOD + ":maple_tower", List.of(MOD + ":maple_woods")),
structure(MOD + ":old_ruin", List.of("minecraft:plains")),
structure(MOD + ":nether_fort", List.of(MOD + ":ashen_wastes")),
structure("minecraft:village_plains", List.of("minecraft:plains")));
return new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1000L, "fingerprint-1",
List.of(new CatalogNamespace(MOD, "Example Mod", "1.0.0"), new CatalogNamespace(OTHER_MOD, "Other Mod", "2.0.0")),
biomes, features, structures,
List.of(new CatalogDimensionType("minecraft:overworld", -64, 384, 384, null)),
List.of());
}
static ModIngestRequest request(Path source, Path target, Set<String> namespaces, boolean replace) {
return new ModIngestRequest(source, target, "overworld", catalog(), namespaces, replace);
}
static JsonObject readJson(Path file) throws IOException {
return JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).getAsJsonObject();
}
static List<String> strings(JsonElement element) {
List<String> values = new ArrayList<>();
if (element == null || !element.isJsonArray()) {
return values;
}
for (JsonElement value : element.getAsJsonArray()) {
values.add(value.getAsString());
}
return values;
}
static JsonObject findByPaletteBlock(JsonArray entries, String block) {
for (JsonElement entry : entries) {
if (!entry.isJsonObject()) {
continue;
}
JsonArray palette = entry.getAsJsonObject().getAsJsonArray("palette");
if (palette == null) {
continue;
}
for (JsonElement paletteEntry : palette) {
if (block.equals(paletteEntry.getAsJsonObject().get("block").getAsString())) {
return entry.getAsJsonObject();
}
}
}
return null;
}
static ModIngestItem item(ModIngestReport report, String sourceKey) {
for (ModIngestItem item : report.items()) {
if (item.sourceKey().equals(sourceKey)) {
return item;
}
}
return null;
}
static void write(Path root, String relative, String content) throws IOException {
Path file = root.resolve(relative);
Files.createDirectories(file.getParent());
Files.writeString(file, content, StandardCharsets.UTF_8);
}
private static CatalogBiome biome(String key, float temperature, float downfall, List<String> tags, List<List<String>> steps) {
return new CatalogBiome(key, temperature, downfall, true, tags, steps, List.of(), null);
}
private static CatalogPlacedFeature feature(String key, String type, String config, String placement) {
return new CatalogPlacedFeature(key, key, type, JsonParser.parseString(config), JsonParser.parseString(placement));
}
private static CatalogStructure structure(String key, List<String> biomes) {
return new CatalogStructure(key, "minecraft:jigsaw", "surface_structures", "beard_thin", "", biomes, List.of());
}
private static List<List<String>> steps(int step, List<String> keys) {
List<List<String>> steps = new ArrayList<>(11);
for (int index = 0; index < 11; index++) {
steps.add(index == step ? keys : List.of());
}
return steps;
}
private static List<List<String>> merge(List<List<String>> first, List<List<String>> second) {
List<List<String>> merged = new ArrayList<>(11);
for (int index = 0; index < 11; index++) {
merged.add(concat(first.get(index), second.get(index)));
}
return merged;
}
private static List<String> concat(List<String> first, List<String> second) {
List<String> values = new ArrayList<>(first);
values.addAll(second);
return values;
}
private static List<String> concat(List<String> first, String extra) {
return concat(first, List.of(extra));
}
}
@@ -0,0 +1,127 @@
/*
* 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.JsonObject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ModIngesterBiomeTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private Path target;
private ModIngestReport report;
@Before
public void ingest() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
target = root.resolve("packs").resolve("modded");
report = ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false));
}
@Test
public void hotModBiomeJoinsHotRegionFromNearestTemplate() throws Exception {
JsonObject biome = ModIngestFixture.readJson(target.resolve("biomes/ingest/examplemod/scorched_mesa.json"));
assertEquals("examplemod:scorched_mesa", biome.get("derivative").getAsString());
assertEquals("Scorched Mesa", biome.get("name").getAsString());
assertTrue(biome.has("layers"));
assertTrue("template objects stay when the mod brings no vegetation", biome.has("objects"));
List<String> hotLand = ModIngestFixture.strings(ModIngestFixture.readJson(target.resolve("regions/hot.json")).get("landBiomes"));
assertEquals("ingest/examplemod/scorched_mesa", hotLand.get(hotLand.size() - 1));
assertFalse(ModIngestFixture.strings(ModIngestFixture.readJson(target.resolve("regions/temperate.json")).get("landBiomes"))
.contains("ingest/examplemod/scorched_mesa"));
ModIngestItem item = ModIngestFixture.item(report, "examplemod:scorched_mesa");
assertNotNull(item);
assertEquals(ModIngestTier.ADOPTED, item.tier());
assertEquals("biome", item.kind());
assertEquals("ingest/examplemod/scorched_mesa", item.target());
assertTrue(item.reason(), item.reason().contains("hot/desert"));
}
@Test
public void forestModBiomeDropsOverridesChildrenAndTreesWhenItBringsItsOwn() throws Exception {
JsonObject biome = ModIngestFixture.readJson(target.resolve("biomes/ingest/examplemod/maple_woods.json"));
assertEquals("examplemod:maple_woods", biome.get("derivative").getAsString());
assertFalse(biome.has("vanillaDerivative"));
assertFalse(biome.has("customDerivitives"));
assertFalse(biome.has("biomeScatter"));
assertFalse(biome.has("children"));
assertFalse(biome.has("objects"));
assertFalse(biome.getAsJsonObject("proceduralObjects").has("trees"));
assertTrue(biome.getAsJsonObject("proceduralObjects").has("ruins"));
assertTrue(ModIngestFixture.strings(ModIngestFixture.readJson(target.resolve("regions/temperate.json")).get("landBiomes"))
.contains("ingest/examplemod/maple_woods"));
}
@Test
public void oceanShoreAndCaveBiomesJoinTheMatchingPools() throws Exception {
JsonObject temperate = ModIngestFixture.readJson(target.resolve("regions/temperate.json"));
JsonObject hot = ModIngestFixture.readJson(target.resolve("regions/hot.json"));
assertTrue(ModIngestFixture.strings(temperate.get("seaBiomes")).contains("ingest/examplemod/coral_depths"));
assertTrue(ModIngestFixture.strings(hot.get("shoreBiomes")).contains("ingest/examplemod/dune_beach"));
assertTrue(ModIngestFixture.strings(hot.get("caveBiomes")).contains("ingest/examplemod/crystal_caves"));
assertFalse(temperate.has("caveBiomes"));
assertEquals("Ocean", ModIngestFixture.readJson(target.resolve("biomes/temperate/ocean.json")).get("name").getAsString());
assertTrue(ModIngestFixture.item(report, "examplemod:coral_depths").reason().contains("temperate/ocean"));
assertTrue(ModIngestFixture.item(report, "examplemod:dune_beach").reason().contains("hot/beach"));
assertTrue(ModIngestFixture.item(report, "examplemod:crystal_caves").reason().contains("caves/dripstone"));
}
@Test
public void riverNetherAndUntaggedBiomesAreSkippedWithReasons() {
ModIngestItem river = ModIngestFixture.item(report, "examplemod:misty_river");
ModIngestItem nether = ModIngestFixture.item(report, "examplemod:ashen_wastes");
ModIngestItem untagged = ModIngestFixture.item(report, "examplemod:void_garden");
assertEquals(ModIngestTier.SKIPPED, river.tier());
assertTrue(river.reason(), river.reason().contains("river"));
assertEquals(ModIngestTier.SKIPPED, nether.tier());
assertTrue(nether.reason(), nether.reason().contains("nether"));
assertEquals(ModIngestTier.SKIPPED, untagged.tier());
assertTrue(untagged.reason(), untagged.reason().contains("minecraft:is_overworld"));
assertFalse(Files.exists(target.resolve("biomes/ingest/examplemod/misty_river.json")));
assertFalse(Files.exists(target.resolve("biomes/ingest/examplemod/ashen_wastes.json")));
}
@Test
public void unselectedNamespacesAreLeftAlone() {
assertNull(ModIngestFixture.item(report, "othermod:bamboo_marsh"));
assertNull(ModIngestFixture.item(report, "othermod:daisies"));
assertFalse(Files.exists(target.resolve("biomes/ingest/othermod")));
}
}
@@ -0,0 +1,183 @@
/*
* 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.JsonObject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ModIngesterFeatureTest {
private static final double EPSILON = 0.0000005D;
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private Path target;
private ModIngestReport report;
private JsonObject dimension;
@Before
public void ingest() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
target = root.resolve("packs").resolve("modded");
report = ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false));
dimension = ModIngestFixture.readJson(target.resolve("dimensions/modded.json"));
}
@Test
public void oreInjectedIntoVanillaBiomesBecomesDimensionDepositsPerTarget() {
JsonArray deposits = dimension.getAsJsonArray("deposits");
JsonObject stone = ModIngestFixture.findByPaletteBlock(deposits, "examplemod:tin_ore");
JsonObject deepslate = ModIngestFixture.findByPaletteBlock(deposits, "examplemod:deepslate_tin_ore");
assertEquals(4, deposits.size());
assertNotNull(stone);
assertNotNull(deepslate);
assertEquals(80, stone.get("minHeight").getAsInt());
assertEquals(427, stone.get("maxHeight").getAsInt());
assertEquals("UNIFORM", stone.get("heightDistribution").getAsString());
assertEquals(12, stone.get("minPerChunk").getAsInt());
assertEquals(12, stone.get("maxPerChunk").getAsInt());
assertEquals(0.5D, stone.get("spawnChance").getAsDouble(), EPSILON);
assertEquals(7, stone.get("minSize").getAsInt());
assertEquals(7, stone.get("maxSize").getAsInt());
assertEquals("VANILLA_ELLIPSOID", stone.get("shape").getAsString());
assertEquals(0, stone.get("surfaceClearance").getAsInt());
assertEquals(List.of("minecraft:andesite", "minecraft:diorite", "minecraft:granite", "minecraft:stone", "minecraft:tuff"),
ModIngestFixture.strings(stone.get("replaceableBlocks")));
assertEquals(List.of("minecraft:deepslate"), ModIngestFixture.strings(deepslate.get("replaceableBlocks")));
assertFalse(stone.has("includedBiomes"));
assertEquals(ModIngestTier.NATIVE, ModIngestFixture.item(report, "examplemod:ore_tin").tier());
}
@Test
public void oreDeclaredByOneVanillaBiomeIsFilteredToThatBiome() {
JsonObject sapphire = ModIngestFixture.findByPaletteBlock(dimension.getAsJsonArray("deposits"), "examplemod:sapphire_ore");
assertNotNull(sapphire);
assertEquals(List.of("minecraft:desert"), ModIngestFixture.strings(sapphire.get("includedBiomes")));
assertEquals("SURFACE", sapphire.get("biomeScope").getAsString());
assertEquals(128, sapphire.get("minHeight").getAsInt());
assertEquals(160, sapphire.get("maxHeight").getAsInt());
assertEquals(2, sapphire.get("maxPerChunk").getAsInt());
assertEquals(6, ModIngestFixture.strings(sapphire.get("replaceableBlocks")).size());
}
@Test
public void modOnlyScatteredOreBecomesBiomeDeposit() throws Exception {
JsonObject biome = ModIngestFixture.readJson(target.resolve("biomes/ingest/examplemod/scorched_mesa.json"));
JsonObject ruby = ModIngestFixture.findByPaletteBlock(biome.getAsJsonArray("deposits"), "examplemod:ruby_ore");
assertNotNull(ruby);
assertEquals("false", ruby.getAsJsonArray("palette").get(0).getAsJsonObject().getAsJsonObject("data").get("lit").getAsString());
assertEquals(112, ruby.get("minHeight").getAsInt());
assertEquals(176, ruby.get("maxHeight").getAsInt());
assertEquals("TRIANGLE", ruby.get("heightDistribution").getAsString());
assertEquals(1, ruby.get("minPerChunk").getAsInt());
assertEquals(3, ruby.get("maxPerChunk").getAsInt());
assertEquals("VANILLA_SCATTERED", ruby.get("shape").getAsString());
assertEquals(0.5D, ruby.get("discardChanceOnAirExposure").getAsDouble(), EPSILON);
assertEquals(List.of("minecraft:stone"), ModIngestFixture.strings(ruby.get("replaceableBlocks")));
assertNull(ModIngestFixture.findByPaletteBlock(dimension.getAsJsonArray("deposits"), "examplemod:ruby_ore"));
assertEquals("ingest/examplemod/scorched_mesa", ModIngestFixture.item(report, "examplemod:ore_ruby").target());
}
@Test
public void flowerPatchLandsInModBiomeAndExistingVanillaDerivativeBiome() throws Exception {
JsonObject maple = ModIngestFixture.readJson(target.resolve("biomes/ingest/examplemod/maple_woods.json"));
JsonObject forest = ModIngestFixture.readJson(target.resolve("biomes/temperate/forest.json"));
JsonObject orphan = ModIngestFixture.readJson(target.resolve("biomes/unused/orphan.json"));
JsonObject mapleFlowers = ModIngestFixture.findByPaletteBlock(maple.getAsJsonArray("decorators"), "examplemod:glowcap");
JsonObject forestFlowers = ModIngestFixture.findByPaletteBlock(forest.getAsJsonArray("decorators"), "examplemod:glowcap");
assertNotNull(mapleFlowers);
assertNotNull(forestFlowers);
assertEquals(2, forest.getAsJsonArray("decorators").size());
assertEquals(1.0D - Math.exp(-(1.0D / 4.0D * 32.0D * 0.25D) / 256.0D), forestFlowers.get("chance").getAsDouble(), 0.000001D);
JsonArray palette = forestFlowers.getAsJsonArray("palette");
assertEquals(3, palette.get(0).getAsJsonObject().get("weight").getAsInt());
assertEquals("examplemod:glowbell", palette.get(1).getAsJsonObject().get("block").getAsString());
assertEquals("true", palette.get(1).getAsJsonObject().getAsJsonObject("data").get("lit").getAsString());
assertEquals("minecraft:grass_block", forestFlowers.getAsJsonArray("whitelist").get(0).getAsJsonObject().get("block").getAsString());
assertFalse(orphan.has("decorators"));
ModIngestItem item = ModIngestFixture.item(report, "examplemod:glow_flowers");
assertEquals(ModIngestTier.NATIVE, item.tier());
assertTrue(item.target(), item.target().contains("temperate/forest"));
assertTrue(item.target(), item.target().contains("ingest/examplemod/maple_woods"));
}
@Test
public void legacyRandomPatchConvertsIntoPlainsDecorator() throws Exception {
JsonObject plains = ModIngestFixture.readJson(target.resolve("biomes/temperate/plains.json"));
JsonObject bluebell = ModIngestFixture.findByPaletteBlock(plains.getAsJsonArray("decorators"), "examplemod:bluebell");
assertNotNull(bluebell);
assertEquals(1.0D - Math.exp(-(0.5D * 64.0D * 0.25D) / 256.0D), bluebell.get("chance").getAsDouble(), 0.000001D);
}
@Test
public void unconvertibleFeaturesPassThroughAndConvertedOnesAreDisabled() {
ModIngestItem trees = ModIngestFixture.item(report, "examplemod:maple_trees");
ModIngestItem weird = ModIngestFixture.item(report, "examplemod:weird_ore");
JsonObject imported = dimension.getAsJsonObject("importedFeatures");
assertEquals(ModIngestTier.PASSTHROUGH, trees.tier());
assertEquals(ModIngestTier.PASSTHROUGH, weird.tier());
assertTrue(weird.reason(), weird.reason().contains("examplemod:custom_stone"));
assertTrue(imported.get("enabled").getAsBoolean());
assertEquals(List.of("minecraft:ore_diamond", "minecraft:", "examplemod:bluebell_patch", "examplemod:glow_flowers",
"examplemod:ore_ruby", "examplemod:ore_sapphire", "examplemod:ore_tin"),
ModIngestFixture.strings(imported.get("disabled")));
assertEquals(List.of("LAKES"), ModIngestFixture.strings(imported.get("disabledSteps")));
}
@Test
public void featuresWithoutHostsOrShadowedByDisabledPrefixesAreSkipped() {
ModIngestItem nether = ModIngestFixture.item(report, "examplemod:nether_ore");
ModIngestItem buried = ModIngestFixture.item(report, "examplemod:ore_tin_buried");
assertEquals(ModIngestTier.SKIPPED, nether.tier());
assertEquals(ModIngestTier.SKIPPED, buried.tier());
assertTrue(buried.reason(), buried.reason().contains("examplemod:ore_tin"));
}
@Test
public void structuresAreAdoptedUnlessDisabledOrHostless() {
assertEquals(ModIngestTier.ADOPTED, ModIngestFixture.item(report, "examplemod:maple_tower").tier());
ModIngestItem ruin = ModIngestFixture.item(report, "examplemod:old_ruin");
assertEquals(ModIngestTier.SKIPPED, ruin.tier());
assertTrue(ruin.reason(), ruin.reason().contains("importedStructures"));
assertEquals(ModIngestTier.SKIPPED, ModIngestFixture.item(report, "examplemod:nether_fort").tier());
assertNull(ModIngestFixture.item(report, "minecraft:village_plains"));
}
}
@@ -0,0 +1,167 @@
/*
* 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.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import com.google.gson.JsonObject;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModIngesterPackTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void writesLoadableTargetPackWithoutTouchingSource() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
Map<String, String> before = snapshot(source);
Path target = root.resolve("packs").resolve("modded");
ModIngestReport report = ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false));
assertEquals(before, snapshot(source));
assertEquals(target, report.targetPack());
assertTrue(Files.isRegularFile(target.resolve("dimensions/modded.json")));
assertFalse(Files.exists(target.resolve("dimensions/overworld.json")));
assertFalse(Files.exists(target.resolve(".git")));
assertFalse(Files.exists(target.resolve("source.code-workspace")));
PackValidationResult validation = PackValidator.validate(target.toFile());
assertTrue(validation.getBlockingErrors().toString(), validation.isLoadable());
}
@Test
public void manifestAndSummaryDescribeTheIngest() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
Path target = root.resolve("packs").resolve("modded");
ModIngestReport report = ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false));
JsonObject manifest = ModIngestFixture.readJson(target.resolve("ingest/manifest.json"));
assertEquals("fingerprint-1", manifest.get("catalogFingerprint").getAsString());
assertEquals("modded", manifest.get("dimension").getAsString());
assertEquals(List.of("examplemod"), ModIngestFixture.strings(manifest.get("namespaces")));
List<String> generated = ModIngestFixture.strings(manifest.get("generatedFiles"));
List<String> modified = ModIngestFixture.strings(manifest.get("modifiedFiles"));
assertTrue(generated.toString(), generated.contains("biomes/ingest/examplemod/scorched_mesa.json"));
assertTrue(modified.toString(), modified.containsAll(List.of("dimensions/modded.json", "regions/hot.json",
"regions/temperate.json", "biomes/temperate/forest.json", "biomes/temperate/plains.json")));
assertFalse(modified.contains("biomes/unused/orphan.json"));
assertEquals(report.items().size(), manifest.getAsJsonArray("items").size());
assertTrue(report.summaryLines().toString(),
report.summaryLines().contains("examplemod: 5 native, 6 adopted, 2 passthrough, 7 skipped"));
}
@Test
public void existingTargetIsRejectedUnlessReplaceIsRequested() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
Path target = root.resolve("packs").resolve("modded");
ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false));
assertThrows(FileAlreadyExistsException.class,
() -> ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false)));
}
@Test
public void replacingTargetProducesIdenticalOutput() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
Path target = root.resolve("packs").resolve("modded");
ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), true));
Map<String, String> first = snapshot(target);
ModIngestFixture.write(target, "biomes/stale.json", "{\"name\": \"Stale\"}");
ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), true));
assertEquals(first, snapshot(target));
}
@Test
public void targetInsideSourceIsRejected() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
assertThrows(IOException.class, () -> ModIngester.ingest(
ModIngestFixture.request(source, source.resolve("nested"), Set.of(), true)));
assertThrows(IOException.class, () -> ModIngester.ingest(
ModIngestFixture.request(source, source, Set.of(), true)));
assertTrue(Files.isRegularFile(source.resolve("dimensions/overworld.json")));
}
@Test
public void ingestedPackCannotBeUsedAsSource() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
Path target = root.resolve("packs").resolve("modded");
ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(ModIngestFixture.MOD), false));
IOException failure = assertThrows(IOException.class, () -> ModIngester.ingest(new ModIngestRequest(
target, root.resolve("packs").resolve("again"), "modded", ModIngestFixture.catalog(), Set.of(), false)));
assertTrue(failure.getMessage(), failure.getMessage().contains("original pack"));
assertFalse(Files.exists(root.resolve("packs").resolve("again")));
}
@Test
public void emptyNamespaceSelectionIngestsEveryModNamespace() throws Exception {
Path root = folder.getRoot().toPath();
Path source = ModIngestFixture.writeSourcePack(root);
Path target = root.resolve("packs").resolve("everything");
ModIngestReport report = ModIngester.ingest(ModIngestFixture.request(source, target, Set.of(), false));
assertNotNull(ModIngestFixture.item(report, "othermod:bamboo_marsh"));
assertEquals(ModIngestTier.NATIVE, ModIngestFixture.item(report, "othermod:daisies").tier());
assertTrue(Files.isRegularFile(target.resolve("biomes/ingest/othermod/bamboo_marsh.json")));
assertEquals(List.of("examplemod", "othermod"),
ModIngestFixture.strings(ModIngestFixture.readJson(target.resolve("ingest/manifest.json")).get("namespaces")));
assertTrue(report.items().stream().noneMatch(item -> item.namespace().equals("minecraft")));
}
private static Map<String, String> snapshot(Path root) throws IOException {
Map<String, String> files = new TreeMap<>();
try (Stream<Path> walk = Files.walk(root)) {
for (Path path : walk.filter(Files::isRegularFile).toList()) {
files.put(root.relativize(path).toString().replace('\\', '/'), Files.readString(path, StandardCharsets.UTF_8));
}
}
return files;
}
}
@@ -0,0 +1,156 @@
/*
* 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 com.google.gson.JsonParser;
import org.junit.Test;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class VanillaFeatureParsersTest {
private static final double EPSILON = 0.000001D;
private static final VanillaHeights.VerticalBounds OVERWORLD = new VanillaHeights.VerticalBounds(-64, 384);
@Test
public void intProvidersReportMeanAndRange() throws Exception {
assertStats(7, 7, 7, VanillaIntProviders.stats(json("7")));
assertStats(4, 4, 4, VanillaIntProviders.stats(json("{\"type\": \"minecraft:constant\", \"value\": 4}")));
assertStats(2, 1, 3, VanillaIntProviders.stats(json("{\"type\": \"minecraft:uniform\", \"min_inclusive\": 1, \"max_inclusive\": 3}")));
assertStats(0.2D, 0, 1, VanillaIntProviders.stats(json("""
{"type": "minecraft:clamped", "min_inclusive": 0, "max_inclusive": 64,
"source": {"type": "minecraft:uniform", "min_inclusive": -3, "max_inclusive": 1}}
""")));
assertStats(0, -3, 3, VanillaIntProviders.stats(json("{\"type\": \"minecraft:trapezoid\", \"min\": -3, \"max\": 3, \"plateau\": 0}")));
assertStats(2.5D, 1, 4, VanillaIntProviders.stats(json("""
{"type": "minecraft:weighted_list", "distribution": [{"data": 1, "weight": 1}, {"data": 4, "weight": 1}]}
""")));
assertThrows(IngestConversionException.class, () -> VanillaIntProviders.stats(json("{\"type\": \"minecraft:mystery\"}")));
}
@Test
public void offsetProvidersReportTheChanceOfLandingOnTheSurface() throws Exception {
assertEquals(0.25D, VanillaIntProviders.probabilityOf(json("{\"type\": \"minecraft:trapezoid\", \"min\": -3, \"max\": 3, \"plateau\": 0}"), 0), EPSILON);
assertEquals(1.0D / 16.0D, VanillaIntProviders.probabilityOf(json("{\"type\": \"minecraft:trapezoid\", \"min\": -3, \"max\": 3, \"plateau\": 0}"), 3), EPSILON);
assertEquals(1.0D / 7.0D, VanillaIntProviders.probabilityOf(json("{\"type\": \"minecraft:trapezoid\", \"min\": -3, \"max\": 3, \"plateau\": 6}"), 0), EPSILON);
assertEquals(0.2D, VanillaIntProviders.probabilityOf(json("{\"type\": \"minecraft:uniform\", \"min_inclusive\": -2, \"max_inclusive\": 2}"), 0), EPSILON);
assertEquals(1.0D, VanillaIntProviders.probabilityOf(json("0"), 0), EPSILON);
assertEquals(0.0D, VanillaIntProviders.probabilityOf(json("2"), 0), EPSILON);
}
@Test
public void heightProvidersResolveAnchorsAgainstTheDimensionType() throws Exception {
VanillaHeights.HeightBand uniform = VanillaHeights.band(json("""
{"type": "minecraft:uniform", "min_inclusive": {"above_bottom": 16}, "max_inclusive": {"below_top": 20}}
"""), OVERWORLD);
VanillaHeights.HeightBand trapezoid = VanillaHeights.band(json("""
{"type": "minecraft:trapezoid", "min_inclusive": {"above_bottom": -80}, "max_inclusive": {"above_bottom": 80}}
"""), OVERWORLD);
VanillaHeights.HeightBand flat = VanillaHeights.band(json("""
{"type": "minecraft:trapezoid", "min_inclusive": {"absolute": 0}, "max_inclusive": {"absolute": 40}, "plateau": 40}
"""), OVERWORLD);
VanillaHeights.HeightBand constant = VanillaHeights.band(json("{\"absolute\": 12}"), OVERWORLD);
assertEquals(new VanillaHeights.HeightBand(-48, 299, "UNIFORM"), uniform);
assertEquals(new VanillaHeights.HeightBand(-144, 16, "TRIANGLE"), trapezoid);
assertEquals("UNIFORM", flat.distribution());
assertEquals(new VanillaHeights.HeightBand(12, 12, "UNIFORM"), constant);
assertThrows(IngestConversionException.class, () -> VanillaHeights.band(json("""
{"type": "minecraft:very_biased_to_bottom", "min_inclusive": {"absolute": 0}, "max_inclusive": {"absolute": 9}}
"""), OVERWORLD));
}
@Test
public void ruleTestsFlattenVanillaHeightSpecificTargets() throws Exception {
VanillaRuleTests.HostSet stone = VanillaRuleTests.hosts(json("""
{"predicate_type": "minecraft:any_of", "rules": [
{"predicate_type": "minecraft:all_of", "rules": [
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"},
{"predicate_type": "minecraft:height_match", "min_inclusive": 0, "max_inclusive": 2031}]},
{"predicate_type": "minecraft:all_of", "rules": [
{"predicate_type": "minecraft:not", "rule": {"predicate_type": "minecraft:tag_match", "tag": "minecraft:height_specific_ore_replaceables"}},
{"predicate_type": "minecraft:tag_match", "tag": "minecraft:stone_ore_replaceables"}]}]}
"""));
assertFalse(stone.any());
assertEquals(Set.of("minecraft:tuff", "minecraft:stone", "minecraft:granite", "minecraft:diorite", "minecraft:andesite"), stone.blocks());
assertEquals(Set.of("minecraft:netherrack"),
VanillaRuleTests.hosts(json("{\"predicate_type\": \"minecraft:block_match\", \"block\": \"minecraft:netherrack\"}")).blocks());
assertEquals(Set.of("minecraft:end_stone"), VanillaRuleTests.hosts(json(
"{\"predicate_type\": \"minecraft:blockstate_match\", \"block_state\": {\"Name\": \"minecraft:end_stone\"}}")).blocks());
assertTrue(VanillaRuleTests.hosts(json("{\"predicate_type\": \"minecraft:always_true\"}")).any());
assertThrows(IngestConversionException.class, () -> VanillaRuleTests.hosts(json(
"{\"predicate_type\": \"minecraft:random_block_match\", \"block\": \"minecraft:stone\", \"probability\": 0.5}")));
IngestConversionException unknownTag = assertThrows(IngestConversionException.class, () -> VanillaRuleTests.hosts(json(
"{\"predicate_type\": \"minecraft:tag_match\", \"tag\": \"examplemod:moon_rock\"}")));
assertTrue(unknownTag.getMessage().contains("examplemod:moon_rock"));
}
@Test
public void blockStatesAcceptEveryCodecShape() throws Exception {
JsonObject compact = VanillaBlockStates.irisBlock(json("\"minecraft:iron_ore\""));
JsonObject bracketed = VanillaBlockStates.irisBlock(json("\"minecraft:oak_log[axis=x]\""));
JsonObject modern = VanillaBlockStates.irisBlock(json("{\"id\": \"minecraft:sunflower\", \"properties\": {\"half\": \"lower\"}}"));
JsonObject legacy = VanillaBlockStates.irisBlock(json("{\"Name\": \"minecraft:sweet_berry_bush\", \"Properties\": {\"age\": \"3\"}}"));
assertEquals("minecraft:iron_ore", compact.get("block").getAsString());
assertFalse(compact.has("data"));
assertEquals("x", bracketed.getAsJsonObject("data").get("axis").getAsString());
assertEquals("lower", modern.getAsJsonObject("data").get("half").getAsString());
assertEquals("3", legacy.getAsJsonObject("data").get("age").getAsString());
assertThrows(IngestConversionException.class, () -> VanillaBlockStates.irisBlock(json("{\"oops\": true}")));
}
@Test
public void stateProvidersBecomeWeightedPalettes() throws Exception {
List<JsonObject> threshold = VanillaBlockStates.palette(json("""
{"type": "minecraft:noise_threshold", "default_state": "minecraft:dandelion",
"low_states": ["minecraft:red_tulip"], "high_states": ["minecraft:poppy", "minecraft:dandelion"],
"noise": {"base_amplitude": 1.0, "base_octave": 0}, "scale": 0.005, "seed": 2345, "threshold": -0.8, "high_chance": 0.33}
"""));
List<JsonObject> legacy = VanillaBlockStates.palette(json("""
{"type": "minecraft:weighted_state_provider", "entries": [{"data": {"Name": "minecraft:poppy"}, "weight": 2}]}
"""));
assertEquals(List.of("minecraft:dandelion", "minecraft:red_tulip", "minecraft:poppy"),
threshold.stream().map(entry -> entry.get("block").getAsString()).toList());
assertEquals(2, legacy.get(0).get("weight").getAsInt());
assertEquals("minecraft:short_grass", VanillaBlockStates.palette(json("{\"id\": \"minecraft:short_grass\"}")).get(0).get("block").getAsString());
IngestConversionException reference = assertThrows(IngestConversionException.class,
() -> VanillaBlockStates.palette(json("\"minecraft:flower_plain\"")));
assertTrue(reference.getMessage().contains("minecraft:flower_plain"));
}
private static void assertStats(double mean, int min, int max, VanillaIntProviders.IntStats stats) {
assertEquals(mean, stats.mean(), EPSILON);
assertEquals(min, stats.min());
assertEquals(max, stats.max());
}
private static JsonElement json(String value) {
return JsonParser.parseString(value);
}
}