mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Kept mod trees on plantable ground
- A mod biome that brings its own vegetation pays a template penalty scaled by the share of the template's top surface layer that cannot hold plants, so snow, ice, sand, and stone templates no longer swallow its trees - The top layer palette is resolved through biome-palette snippets and weighted by palette entry weight - Mod biomes without their own vegetation keep the closest climate template
This commit is contained in:
1 parent
d749633829
commit
ad66538d7a
3 files changed
+171
-3
No files matched your search
@@ -29,6 +29,7 @@ import java.util.ArrayDeque;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -40,6 +41,9 @@ final class IngestPackModel {
|
|||||||
private static final int DEFAULT_MIN_Y = -64;
|
private static final int DEFAULT_MIN_Y = -64;
|
||||||
private static final int DEFAULT_MAX_Y = 320;
|
private static final int DEFAULT_MAX_Y = 320;
|
||||||
private static final String DEFAULT_DERIVATIVE = "minecraft:the_void";
|
private static final String DEFAULT_DERIVATIVE = "minecraft:the_void";
|
||||||
|
private static final Set<String> PLANTABLE_BLOCKS = Set.of("minecraft:grass_block", "minecraft:dirt", "minecraft:coarse_dirt",
|
||||||
|
"minecraft:podzol", "minecraft:rooted_dirt", "minecraft:mycelium", "minecraft:moss_block", "minecraft:pale_moss_block",
|
||||||
|
"minecraft:mud", "minecraft:muddy_mangrove_roots", "minecraft:farmland");
|
||||||
|
|
||||||
private final IngestPack pack;
|
private final IngestPack pack;
|
||||||
private final WorldgenCatalogIndex index;
|
private final WorldgenCatalogIndex index;
|
||||||
@@ -50,6 +54,7 @@ final class IngestPackModel {
|
|||||||
private final int packMaxY;
|
private final int packMaxY;
|
||||||
private final List<RegionProfile> regions;
|
private final List<RegionProfile> regions;
|
||||||
private final Map<String, String> reachableBiomes;
|
private final Map<String, String> reachableBiomes;
|
||||||
|
private final Map<String, Double> plantableShares = new HashMap<>();
|
||||||
|
|
||||||
private IngestPackModel(IngestPack pack, WorldgenCatalogIndex index, String dimensionPath, JsonObject dimension,
|
private IngestPackModel(IngestPack pack, WorldgenCatalogIndex index, String dimensionPath, JsonObject dimension,
|
||||||
int[] heightRange, List<RegionProfile> regions, Map<String, String> reachableBiomes) {
|
int[] heightRange, List<RegionProfile> regions, Map<String, String> reachableBiomes) {
|
||||||
@@ -115,6 +120,42 @@ final class IngestPackModel {
|
|||||||
return climateReference(index, reachableBiomes.get(biomeKey)).map(IngestClimate::of).orElse(null);
|
return climateReference(index, reachableBiomes.get(biomeKey)).map(IngestClimate::of).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double plantableShare(String biomeKey) throws IOException {
|
||||||
|
Double cached = plantableShares.get(biomeKey);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
double share = topLayerPlantableShare(biomeKey);
|
||||||
|
plantableShares.put(biomeKey, share);
|
||||||
|
return share;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double topLayerPlantableShare(String biomeKey) throws IOException {
|
||||||
|
Optional<JsonObject> biome = pack.find(IngestPack.biomePath(biomeKey));
|
||||||
|
JsonElement layers = biome.map((JsonObject value) -> value.get("layers")).orElse(null);
|
||||||
|
if (layers == null || !layers.isJsonArray() || layers.getAsJsonArray().isEmpty()) {
|
||||||
|
return 1D;
|
||||||
|
}
|
||||||
|
JsonObject top = pack.snippet(layers.getAsJsonArray().get(0), "biome-palette");
|
||||||
|
JsonElement palette = top == null ? null : top.get("palette");
|
||||||
|
if (palette == null || !palette.isJsonArray() || palette.getAsJsonArray().isEmpty()) {
|
||||||
|
return 1D;
|
||||||
|
}
|
||||||
|
double total = 0D;
|
||||||
|
double plantable = 0D;
|
||||||
|
for (JsonElement entry : palette.getAsJsonArray()) {
|
||||||
|
if (!entry.isJsonObject() || !entry.getAsJsonObject().has("block")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double weight = Math.max(0D, number(entry.getAsJsonObject().get("weight"), 1D));
|
||||||
|
total += weight;
|
||||||
|
if (PLANTABLE_BLOCKS.contains(VanillaJson.namespaced(entry.getAsJsonObject().get("block").getAsString()))) {
|
||||||
|
plantable += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total <= 0D ? 1D : plantable / total;
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> biomeCategories(String biomeKey) {
|
Set<String> biomeCategories(String biomeKey) {
|
||||||
return climateReference(index, reachableBiomes.get(biomeKey)).map((CatalogBiome biome) -> BiomeCategories.of(biome.tags())).orElse(Set.of());
|
return climateReference(index, reachableBiomes.get(biomeKey)).map((CatalogBiome biome) -> BiomeCategories.of(biome.tags())).orElse(Set.of());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ final class ModBiomeAdopter {
|
|||||||
private static final double REGION_CLIMATE_WEIGHT = 0.25D;
|
private static final double REGION_CLIMATE_WEIGHT = 0.25D;
|
||||||
private static final double REUSE_WEIGHT = 0.05D;
|
private static final double REUSE_WEIGHT = 0.05D;
|
||||||
private static final double THEMED_REGION_PENALTY = 0.6D;
|
private static final double THEMED_REGION_PENALTY = 0.6D;
|
||||||
|
private static final double BARE_SURFACE_PENALTY = 3D;
|
||||||
private static final int THEMED_RARITY_FACTOR = 5;
|
private static final int THEMED_RARITY_FACTOR = 5;
|
||||||
|
|
||||||
private final IngestPackModel model;
|
private final IngestPackModel model;
|
||||||
@@ -71,7 +72,8 @@ final class ModBiomeAdopter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
BiomeRole role = classification.role();
|
BiomeRole role = classification.role();
|
||||||
Placement placement = place(role, IngestClimate.of(biome), BiomeCategories.of(biome.tags()));
|
boolean ownsVegetation = ownsVegetation(biome, namespace);
|
||||||
|
Placement placement = place(role, IngestClimate.of(biome), BiomeCategories.of(biome.tags()), ownsVegetation);
|
||||||
if (placement == null) {
|
if (placement == null) {
|
||||||
items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "",
|
items.add(new ModIngestItem(namespace, biome.key(), "biome", ModIngestTier.SKIPPED, "",
|
||||||
"no region in this dimension accepts a " + role.label() + " biome"));
|
"no region in this dimension accepts a " + role.label() + " biome"));
|
||||||
@@ -82,7 +84,7 @@ final class ModBiomeAdopter {
|
|||||||
templateUses.merge(template, 1, Integer::sum);
|
templateUses.merge(template, 1, Integer::sum);
|
||||||
String irisKey = irisKey(biome.key());
|
String irisKey = irisKey(biome.key());
|
||||||
JsonObject json = model.pack().require(IngestPack.biomePath(template)).deepCopy();
|
JsonObject json = model.pack().require(IngestPack.biomePath(template)).deepCopy();
|
||||||
transform(json, biome, ownsVegetation(biome, namespace));
|
transform(json, biome, ownsVegetation);
|
||||||
model.pack().create(IngestPack.biomePath(irisKey), json);
|
model.pack().create(IngestPack.biomePath(irisKey), json);
|
||||||
appendToPool(region.key(), role, irisKey);
|
appendToPool(region.key(), role, irisKey);
|
||||||
adopted.put(biome.key(), new AdoptedBiome(biome.key(), irisKey, role, region.key()));
|
adopted.put(biome.key(), new AdoptedBiome(biome.key(), irisKey, role, region.key()));
|
||||||
@@ -98,7 +100,7 @@ final class ModBiomeAdopter {
|
|||||||
return INGEST_BIOME_FOLDER + namespace + "/" + modBiomeKey.substring(colon + 1);
|
return INGEST_BIOME_FOLDER + namespace + "/" + modBiomeKey.substring(colon + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Placement place(BiomeRole role, IngestClimate climate, Set<String> categories) {
|
private Placement place(BiomeRole role, IngestClimate climate, Set<String> categories, boolean ownsVegetation) throws IOException {
|
||||||
Placement best = null;
|
Placement best = null;
|
||||||
double bestScore = Double.POSITIVE_INFINITY;
|
double bestScore = Double.POSITIVE_INFINITY;
|
||||||
for (IngestPackModel.RegionProfile region : model.regions()) {
|
for (IngestPackModel.RegionProfile region : model.regions()) {
|
||||||
@@ -112,6 +114,7 @@ final class ModBiomeAdopter {
|
|||||||
double score = climate.distance(templateClimate == null ? region.climate() : templateClimate)
|
double score = climate.distance(templateClimate == null ? region.climate() : templateClimate)
|
||||||
+ CATEGORY_WEIGHT * BiomeCategories.distance(categories, model.biomeCategories(template))
|
+ CATEGORY_WEIGHT * BiomeCategories.distance(categories, model.biomeCategories(template))
|
||||||
+ REUSE_WEIGHT * templateUses.getOrDefault(template, 0)
|
+ REUSE_WEIGHT * templateUses.getOrDefault(template, 0)
|
||||||
|
+ (ownsVegetation ? BARE_SURFACE_PENALTY * (1D - model.plantableShare(template)) : 0D)
|
||||||
+ regionPenalty;
|
+ regionPenalty;
|
||||||
if (best == null || score < bestScore) {
|
if (best == null || score < bestScore) {
|
||||||
best = new Placement(region, template);
|
best = new Placement(region, template);
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* 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.WorldgenCatalog;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IngestPlantableSurfaceTest {
|
||||||
|
private static final String MOD = "modtrees";
|
||||||
|
private static final int VEGETAL_STEP = ModIngestFixture.VEGETAL_STEP;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder folder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modBiomesWithTheirOwnTreesAvoidSnowCoveredTemplates() throws Exception {
|
||||||
|
ModIngestReport report = ingest();
|
||||||
|
|
||||||
|
assertTrue(reason(report, "snowy_maple_woods"), reason(report, "snowy_maple_woods").contains("in region tundra from template tundra/"));
|
||||||
|
assertTrue(reason(report, "scarlet_peaks"), reason(report, "scarlet_peaks").contains("in region tundra from template tundra/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void modBiomesWithoutVegetationKeepTheBestClimateTemplate() throws Exception {
|
||||||
|
ModIngestReport report = ingest();
|
||||||
|
|
||||||
|
assertTrue(reason(report, "frost_flats"), reason(report, "frost_flats").contains("from template frozen/plains"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModIngestReport ingest() throws IOException {
|
||||||
|
Path root = folder.newFolder().toPath();
|
||||||
|
Path source = root.resolve("source");
|
||||||
|
ModIngestFixture.write(source, "dimensions/overworld.json", """
|
||||||
|
{"name": "Overworld", "environment": "NORMAL", "regions": ["frozen", "tundra"], "dimensionHeight": {"min": -64, "max": 320}}
|
||||||
|
""");
|
||||||
|
ModIngestFixture.write(source, "regions/frozen.json", """
|
||||||
|
{"name": "Frozen", "landBiomes": ["frozen/plains", "frozen/slopes"]}
|
||||||
|
""");
|
||||||
|
ModIngestFixture.write(source, "regions/tundra.json", """
|
||||||
|
{"name": "Tundra", "landBiomes": ["tundra/taiga", "tundra/mountain-forest"]}
|
||||||
|
""");
|
||||||
|
ModIngestFixture.write(source, "snippet/biome-palette/snow/top.json", """
|
||||||
|
{"palette": [{"block": "minecraft:snow_block"}], "minHeight": 1, "maxHeight": 1}
|
||||||
|
""");
|
||||||
|
biome(source, "frozen/plains", "minecraft:snowy_plains", "\"snippet/biome-palette/snow/top\"");
|
||||||
|
biome(source, "frozen/slopes", "minecraft:snowy_slopes", "\"snippet/biome-palette/snow/top\"");
|
||||||
|
biome(source, "tundra/taiga", "minecraft:taiga", "{\"palette\": [{\"block\": \"grass_block\"}]}");
|
||||||
|
biome(source, "tundra/mountain-forest", "minecraft:windswept_forest", "{\"palette\": [{\"block\": \"minecraft:grass_block\", \"weight\": 3}, {\"block\": \"minecraft:snow_block\"}]}");
|
||||||
|
Path target = root.resolve("packs").resolve("trees");
|
||||||
|
return ModIngester.ingest(new ModIngestRequest(source, target, "overworld", catalog(), Set.of(MOD), Set.of(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void biome(Path pack, String key, String derivative, String topLayer) throws IOException {
|
||||||
|
ModIngestFixture.write(pack, "biomes/" + key + ".json", "{\"name\": \"" + key + "\", \"derivative\": \"" + derivative
|
||||||
|
+ "\", \"layers\": [" + topLayer + ", {\"palette\": [{\"block\": \"minecraft:dirt\"}]}]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldgenCatalog catalog() {
|
||||||
|
List<CatalogBiome> biomes = new ArrayList<>();
|
||||||
|
biomes.add(biome("minecraft:snowy_plains", 0.0F, 0.5F, List.of("c:is_snowy", "c:is_plains"), List.of()));
|
||||||
|
biomes.add(biome("minecraft:snowy_slopes", -0.3F, 0.9F, List.of("c:is_snowy", "minecraft:is_mountain"), List.of()));
|
||||||
|
biomes.add(biome("minecraft:taiga", 0.25F, 0.8F, List.of("minecraft:is_taiga"), List.of()));
|
||||||
|
biomes.add(biome("minecraft:windswept_forest", 0.2F, 0.3F, List.of("minecraft:is_mountain", "minecraft:is_forest"), List.of()));
|
||||||
|
biomes.add(biome(MOD + ":snowy_maple_woods", 0.0F, 0.5F, List.of("c:is_snowy"), List.of(MOD + ":maple_trees")));
|
||||||
|
biomes.add(biome(MOD + ":scarlet_peaks", -0.3F, 0.9F, List.of("c:is_snowy", "minecraft:is_mountain"), List.of(MOD + ":scarlet_trees")));
|
||||||
|
biomes.add(biome(MOD + ":frost_flats", 0.0F, 0.5F, List.of("c:is_snowy", "c:is_plains"), List.of()));
|
||||||
|
List<CatalogPlacedFeature> features = List.of(tree("maple_trees"), tree("scarlet_trees"));
|
||||||
|
return new WorldgenCatalog(WorldgenCatalog.FORMAT, "26.3", "fabric", 1L, "plantable",
|
||||||
|
List.of(new CatalogNamespace(MOD, "Mod Trees", "1")), biomes, features, List.of(),
|
||||||
|
List.of(new CatalogDimensionType("minecraft:overworld", -64, 384, 384, null)), List.of(), Map.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CatalogPlacedFeature tree(String path) {
|
||||||
|
String key = MOD + ":" + path;
|
||||||
|
return new CatalogPlacedFeature(key, key, "minecraft:tree", JsonParser.parseString("{}"),
|
||||||
|
JsonParser.parseString("[{\"type\": \"minecraft:count\", \"count\": 4}]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CatalogBiome biome(String key, float temperature, float downfall, List<String> tags, List<String> vegetal) {
|
||||||
|
List<List<String>> steps = new ArrayList<>(11);
|
||||||
|
for (int index = 0; index < 11; index++) {
|
||||||
|
steps.add(index == VEGETAL_STEP ? vegetal : List.of());
|
||||||
|
}
|
||||||
|
List<String> allTags = new ArrayList<>(tags);
|
||||||
|
allTags.add("minecraft:is_overworld");
|
||||||
|
return new CatalogBiome(key, temperature, downfall, true, allTags, steps, List.of(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String reason(ModIngestReport report, String biome) {
|
||||||
|
return ModIngestFixture.item(report, MOD + ":" + biome).reason();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user