mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Added dimension stacking
- Dimension files define upright top-to-bottom terrain layers - Spacer values set exact air gaps between layers - Optional noise styles shape blended layer boundaries - Runtime queries resolve stacked terrain and biome ownership - Pack validation rejects invalid stack definitions - Project copy and hotload paths include stack dependencies - Superflat fixtures cover gaps, blending, clipping, and fluids - Platform tests cover stacked world generation contracts
This commit is contained in:
1 parent
366033a535
commit
6549b40797
119 files changed
+6874
-743
No files matched your search
@@ -222,9 +222,45 @@ public final class IrisDatapackCompiler {
|
||||
new LinkedHashSet<>(),
|
||||
requiredDimension,
|
||||
requiredFixer);
|
||||
collectDimensionStackRegistryRequirements(
|
||||
requirements,
|
||||
requiredDimension,
|
||||
requiredFixer);
|
||||
return Map.copyOf(requirements);
|
||||
}
|
||||
|
||||
private static void collectDimensionStackRegistryRequirements(
|
||||
Map<String, String> requirements,
|
||||
IrisDimension hostDimension,
|
||||
IDataFixer fixer
|
||||
) throws IOException {
|
||||
if (!hostDimension.hasDimensionStack()) {
|
||||
return;
|
||||
}
|
||||
IrisData data = hostDimension.getLoader();
|
||||
if (data == null) {
|
||||
throw new IOException("Dimension stack host '" + hostDimension.getLoadKey()
|
||||
+ "' has no pack loader");
|
||||
}
|
||||
LinkedHashSet<String> visited = new LinkedHashSet<>();
|
||||
visited.add(hostDimension.getLoadKey());
|
||||
for (String dimensionKey : hostDimension.getDimensionStack().getDimensions()) {
|
||||
if (dimensionKey == null || dimensionKey.isBlank() || !visited.add(dimensionKey)) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension sourceDimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (sourceDimension == null) {
|
||||
throw new IOException("Dimension stack host '" + hostDimension.getLoadKey()
|
||||
+ "' references missing dimension '" + dimensionKey + "'");
|
||||
}
|
||||
collectRegistryRequirements(
|
||||
requirements,
|
||||
new LinkedHashSet<>(),
|
||||
sourceDimension,
|
||||
fixer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectRegistryRequirements(
|
||||
Map<String, String> requirements,
|
||||
Set<String> installedBiomeKeys,
|
||||
@@ -235,7 +271,6 @@ public final class IrisDatapackCompiler {
|
||||
"dimension_type/iris:" + dimension.getDimensionTypeKey(),
|
||||
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
|
||||
|
||||
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
|
||||
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
|
||||
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
|
||||
if (biome == null || !biome.isCustom()) {
|
||||
@@ -246,7 +281,7 @@ public final class IrisDatapackCompiler {
|
||||
if (customBiome == null) {
|
||||
continue;
|
||||
}
|
||||
String biomeKey = namespace + ":" + customBiome.getId();
|
||||
String biomeKey = dimension.getCustomBiomeKey(customBiome.getId());
|
||||
if (!installedBiomeKeys.add(biomeKey)) {
|
||||
continue;
|
||||
}
|
||||
@@ -533,7 +568,7 @@ public final class IrisDatapackCompiler {
|
||||
|| !Files.isDirectory(dimensions, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return false;
|
||||
}
|
||||
try (Stream<Path> stream = Files.list(dimensions)) {
|
||||
try (Stream<Path> stream = Files.walk(dimensions)) {
|
||||
return stream.anyMatch(path -> !Files.isSymbolicLink(path)
|
||||
&& Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)
|
||||
&& path.getFileName().toString().endsWith(".json"));
|
||||
|
||||
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
|
||||
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
keys.add(dimension.getLoadKey() + ":" + j.getId());
|
||||
keys.add(dimension.getCustomBiomeKey(j.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,8 @@ public class MythicMobsDataProvider extends ExternalDataProvider implements List
|
||||
if (access == null) return false;
|
||||
Engine engine = access.getEngine();
|
||||
if (engine == null) return false;
|
||||
IrisRegion region = engine.getRegion(target.getBlockX(), target.getBlockZ());
|
||||
IrisRegion region = engine.getRegion(
|
||||
target.getBlockX(), target.getBlockY() - engine.getMinHeight(), target.getBlockZ());
|
||||
return regions.contains(region.getLoadKey());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +156,9 @@ public interface INMSBinding {
|
||||
return NativeStructureVolume.NONE;
|
||||
}
|
||||
|
||||
default void invalidateNativeStructureVolumeIndex(Engine engine) {
|
||||
}
|
||||
|
||||
int getBiomeId(Biome biome);
|
||||
|
||||
MCABiomeContainer newBiomeContainer(int min, int max, int[] data);
|
||||
|
||||
@@ -26,7 +26,9 @@ import art.arcane.volmlib.util.json.JSONObject;
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
final class PackDimensionValidator {
|
||||
private PackDimensionValidator() {
|
||||
@@ -35,9 +37,14 @@ final class PackDimensionValidator {
|
||||
static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
|
||||
File regionsFolder = new File(packFolder, "regions");
|
||||
File biomesFolder = new File(packFolder, "biomes");
|
||||
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
|
||||
Set<String> dimensionKeys = new LinkedHashSet<>();
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
dimensionKeys.add(PackValidationIo.deriveKey(dimensionsFolder, dimensionFile));
|
||||
}
|
||||
|
||||
for (File dimFile : dimensionFiles) {
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimFile.getName());
|
||||
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimFile);
|
||||
JSONObject dimJson;
|
||||
try {
|
||||
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
|
||||
@@ -50,6 +57,7 @@ final class PackDimensionValidator {
|
||||
validateDimensionHeights(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
validateWorldBoundary(dimensionKey, dimJson, blockingErrors);
|
||||
validateStaticObjects(packFolder, dimensionKey, dimJson, blockingErrors);
|
||||
validateDimensionStack(packFolder, dimensionKey, dimJson, dimensionKeys, blockingErrors);
|
||||
|
||||
JSONArray regionsArray = dimJson.optJSONArray("regions");
|
||||
if (regionsArray == null || regionsArray.length() == 0) {
|
||||
@@ -94,6 +102,108 @@ final class PackDimensionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
static void validateDimensionStack(File packFolder, String dimensionKey, JSONObject dimension, Set<String> dimensionKeys,
|
||||
List<String> blockingErrors) {
|
||||
if (!dimension.has("dimensionStack")) {
|
||||
return;
|
||||
}
|
||||
String context = "Dimension '" + dimensionKey + "' dimensionStack";
|
||||
JSONObject stack = dimension.optJSONObject("dimensionStack");
|
||||
if (stack == null) {
|
||||
blockingErrors.add(context + " must be an object.");
|
||||
return;
|
||||
}
|
||||
|
||||
String upperDimension = dimension.optString("upperDimension", "none");
|
||||
if (upperDimension != null && !upperDimension.isEmpty()
|
||||
&& !upperDimension.equalsIgnoreCase("none")) {
|
||||
blockingErrors.add(context + " cannot be used with upperDimension.");
|
||||
}
|
||||
|
||||
JSONArray dimensions = stack.optJSONArray("dimensions");
|
||||
if (dimensions == null) {
|
||||
blockingErrors.add(context + ".dimensions must be an array.");
|
||||
} else {
|
||||
int hostOccurrences = 0;
|
||||
if (dimensions.length() < 2) {
|
||||
blockingErrors.add(context + ".dimensions must contain at least two dimension keys.");
|
||||
}
|
||||
for (int index = 0; index < dimensions.length(); index++) {
|
||||
Object rawKey = dimensions.opt(index);
|
||||
if (!(rawKey instanceof String key) || key.isBlank()) {
|
||||
blockingErrors.add(context + ".dimensions[" + index + "] must be a nonblank dimension key.");
|
||||
continue;
|
||||
}
|
||||
if (!dimensionKeys.contains(key)) {
|
||||
blockingErrors.add(context + ".dimensions[" + index + "] references missing dimension '"
|
||||
+ key + "'.");
|
||||
}
|
||||
if (dimensionKey.equals(key)) {
|
||||
hostOccurrences++;
|
||||
}
|
||||
}
|
||||
if (hostOccurrences != 1) {
|
||||
blockingErrors.add(context + ".dimensions must contain its host dimension key '"
|
||||
+ dimensionKey + "' exactly once.");
|
||||
}
|
||||
if (dimensions.length() > 0) {
|
||||
Object rawLast = dimensions.opt(dimensions.length() - 1);
|
||||
if (rawLast instanceof String lastKey && !dimensionKey.equals(lastKey)) {
|
||||
blockingErrors.add(context + ".dimensions must end with its host dimension key '"
|
||||
+ dimensionKey + "'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateInteger(stack, "spacer", 0, 256, context, blockingErrors);
|
||||
validateDimensionStackBlend(packFolder, context, stack, blockingErrors);
|
||||
}
|
||||
|
||||
private static void validateDimensionStackBlend(File packFolder, String context, JSONObject stack,
|
||||
List<String> blockingErrors) {
|
||||
if (!stack.has("blend")) {
|
||||
return;
|
||||
}
|
||||
JSONObject blend = stack.optJSONObject("blend");
|
||||
if (blend == null) {
|
||||
blockingErrors.add(context + ".blend must be an object.");
|
||||
return;
|
||||
}
|
||||
if (blend.has("amplitude")) {
|
||||
validateInteger(blend, "amplitude", 0, 256, context + ".blend", blockingErrors);
|
||||
}
|
||||
if (blend.has("style")) {
|
||||
Object rawStyle = blend.opt("style");
|
||||
boolean styleObject = rawStyle instanceof JSONObject;
|
||||
boolean styleSnippet = rawStyle instanceof String reference
|
||||
&& reference.startsWith("snippet/style/");
|
||||
if (!styleObject && !styleSnippet) {
|
||||
blockingErrors.add(context + ".blend.style must be an object or style snippet reference.");
|
||||
return;
|
||||
}
|
||||
if (styleSnippet) {
|
||||
validateDimensionStackStyleSnippet(
|
||||
packFolder, context + ".blend.style", (String) rawStyle, blockingErrors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateDimensionStackStyleSnippet(File packFolder, String context, String reference,
|
||||
List<String> blockingErrors) {
|
||||
try {
|
||||
File snippetRoot = new File(packFolder, "snippet/style").getCanonicalFile();
|
||||
File snippetFile = new File(packFolder, reference + ".json").getCanonicalFile();
|
||||
if (!snippetFile.toPath().startsWith(snippetRoot.toPath()) || !snippetFile.isFile()) {
|
||||
blockingErrors.add(context + " references missing style snippet '" + reference + "'.");
|
||||
return;
|
||||
}
|
||||
new JSONObject(Files.readString(snippetFile.toPath(), StandardCharsets.UTF_8));
|
||||
} catch (Throwable error) {
|
||||
blockingErrors.add(context + " references invalid style snippet '" + reference + "': "
|
||||
+ error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static void validateStaticObjects(File packFolder, String dimensionKey, JSONObject dimension,
|
||||
List<String> blockingErrors) {
|
||||
if (!dimension.has("staticObjects")) {
|
||||
|
||||
@@ -18,11 +18,16 @@
|
||||
|
||||
package art.arcane.iris.core.pack;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisObjectMarker;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStaticObject;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Shared key collection for the pack packagers. Both the Bukkit re-serializing compiler and the
|
||||
* modded verbatim-copy compiler must export the complete ambient-spawning graph: object placements
|
||||
@@ -33,6 +38,48 @@ public final class PackExportClosure {
|
||||
private PackExportClosure() {
|
||||
}
|
||||
|
||||
public static KSet<String> collectDimensionKeys(IrisDimension dimension) {
|
||||
return collectDimensionKeys(dimension, key -> dimension == null || dimension.getLoader() == null
|
||||
? null
|
||||
: dimension.getLoader().getDimensionLoader().load(key));
|
||||
}
|
||||
|
||||
static KSet<String> collectDimensionKeys(
|
||||
IrisDimension dimension,
|
||||
Function<String, IrisDimension> resolver
|
||||
) {
|
||||
KSet<String> dimensionKeys = new KSet<>();
|
||||
if (dimension == null || dimension.getLoadKey() == null || dimension.getLoadKey().isBlank()) {
|
||||
return dimensionKeys;
|
||||
}
|
||||
|
||||
ArrayDeque<IrisDimension> pending = new ArrayDeque<>();
|
||||
pending.add(dimension);
|
||||
while (!pending.isEmpty()) {
|
||||
IrisDimension current = pending.removeFirst();
|
||||
String currentKey = current.getLoadKey();
|
||||
if (currentKey == null || currentKey.isBlank() || !dimensionKeys.add(currentKey)) {
|
||||
continue;
|
||||
}
|
||||
IrisDimensionStack stack = current.getDimensionStack();
|
||||
if (stack == null || stack.getDimensions() == null) {
|
||||
continue;
|
||||
}
|
||||
for (String key : stack.getDimensions()) {
|
||||
if (key == null || key.isBlank() || dimensionKeys.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension referenced = resolver.apply(key);
|
||||
if (referenced == null) {
|
||||
dimensionKeys.add(key);
|
||||
} else {
|
||||
pending.addLast(referenced);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dimensionKeys;
|
||||
}
|
||||
|
||||
public static KSet<String> collectMarkerKeys(Iterable<IrisObjectPlacement> placements) {
|
||||
KSet<String> markerKeys = new KSet<>();
|
||||
if (placements == null) {
|
||||
|
||||
@@ -80,12 +80,13 @@ final class PackHydrologyValidator {
|
||||
|
||||
List<File> sortedDimensions = new ArrayList<>(List.of(dimensionFiles));
|
||||
sortedDimensions.sort(Comparator.comparing(File::getPath));
|
||||
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
|
||||
for (File dimensionFile : sortedDimensions) {
|
||||
JSONObject dimension = PackValidationIo.readJson(dimensionFile);
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
||||
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
|
||||
String dimensionPath = "Dimension '" + dimensionKey + "'";
|
||||
JSONObject dimensionPolicy = validatePolicy(
|
||||
dimensionPath,
|
||||
|
||||
@@ -7,6 +7,7 @@ import art.arcane.iris.engine.image.IrisImageMapValidationException;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisImage;
|
||||
@@ -217,11 +218,12 @@ final class PackImageMapValidator {
|
||||
Set<String> warnings
|
||||
) {
|
||||
File[] sortedFiles = Arrays.copyOf(dimensionFiles, dimensionFiles.length);
|
||||
Arrays.sort(sortedFiles, (File first, File second) -> first.getName().compareTo(second.getName()));
|
||||
Arrays.sort(sortedFiles, (File first, File second) -> first.getPath().compareTo(second.getPath()));
|
||||
Map<String, IrisDimension> dimensions = new LinkedHashMap<>();
|
||||
Set<String> bindingReadyDimensions = new LinkedHashSet<>();
|
||||
File dimensionsFolder = new File(packFolder, PackValidator.DIMENSIONS_FOLDER);
|
||||
for (File dimensionFile : sortedFiles) {
|
||||
String dimensionKey = PackValidationIo.stripExtension(dimensionFile.getName());
|
||||
String dimensionKey = PackValidationIo.deriveKey(dimensionsFolder, dimensionFile);
|
||||
JSONObject raw = PackValidationIo.readJson(dimensionFile);
|
||||
if (raw == null) {
|
||||
continue;
|
||||
@@ -247,19 +249,35 @@ final class PackImageMapValidator {
|
||||
validateGeneratorStyleReferences(dimensionKey, dimension, data, resources, blockingErrors);
|
||||
}
|
||||
Map<String, List<Map.Entry<String, IrisDimension>>> upperParents = new TreeMap<>();
|
||||
Map<String, List<Map.Entry<String, IrisDimension>>> stackParents = new TreeMap<>();
|
||||
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
|
||||
String upperKey = normalized(entry.getValue().getUpperDimension());
|
||||
if (upperKey == null || upperKey.equalsIgnoreCase("none")) {
|
||||
if (upperKey != null && !upperKey.equalsIgnoreCase("none")) {
|
||||
upperParents.computeIfAbsent(upperKey, ignored -> new ArrayList<>()).add(entry);
|
||||
}
|
||||
IrisDimensionStack stack = entry.getValue().getDimensionStack();
|
||||
if (stack == null || stack.getDimensions() == null) {
|
||||
continue;
|
||||
}
|
||||
upperParents.computeIfAbsent(upperKey, ignored -> new ArrayList<>()).add(entry);
|
||||
Set<String> stackKeys = new LinkedHashSet<>();
|
||||
for (String stackKey : stack.getDimensions()) {
|
||||
String normalizedKey = normalized(stackKey);
|
||||
if (normalizedKey == null || normalizedKey.equals(entry.getKey())
|
||||
|| !stackKeys.add(normalizedKey)) {
|
||||
continue;
|
||||
}
|
||||
stackParents.computeIfAbsent(normalizedKey, ignored -> new ArrayList<>()).add(entry);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, IrisDimension> entry : dimensions.entrySet()) {
|
||||
if (!bindingReadyDimensions.contains(entry.getKey())) {
|
||||
continue;
|
||||
}
|
||||
List<Map.Entry<String, IrisDimension>> parents = upperParents.get(entry.getKey());
|
||||
if (parents == null || parents.isEmpty()) {
|
||||
List<Map.Entry<String, IrisDimension>> stackHosts = stackParents.get(entry.getKey());
|
||||
boolean hasUpperParents = parents != null && !parents.isEmpty();
|
||||
boolean hasStackHosts = stackHosts != null && !stackHosts.isEmpty();
|
||||
if (!hasUpperParents && !hasStackHosts) {
|
||||
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
||||
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
||||
continue;
|
||||
@@ -268,12 +286,23 @@ final class PackImageMapValidator {
|
||||
validateBindings(packFolder, "Dimension '" + entry.getKey() + "'", entry.getValue(),
|
||||
entry.getValue().getWorldBoundary(), data, resources, registries, blockingErrors, warnings);
|
||||
}
|
||||
parents.sort(Map.Entry.comparingByKey());
|
||||
for (Map.Entry<String, IrisDimension> parent : parents) {
|
||||
validateBindings(packFolder,
|
||||
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
|
||||
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries,
|
||||
blockingErrors, warnings);
|
||||
if (hasUpperParents) {
|
||||
parents.sort(Map.Entry.comparingByKey());
|
||||
for (Map.Entry<String, IrisDimension> parent : parents) {
|
||||
validateBindings(packFolder,
|
||||
"Dimension '" + parent.getKey() + "' upper dimension '" + entry.getKey() + "'",
|
||||
entry.getValue(), parent.getValue().getWorldBoundary(), data, resources, registries,
|
||||
blockingErrors, warnings);
|
||||
}
|
||||
}
|
||||
if (hasStackHosts) {
|
||||
stackHosts.sort(Map.Entry.comparingByKey());
|
||||
for (Map.Entry<String, IrisDimension> host : stackHosts) {
|
||||
validateBindings(packFolder,
|
||||
"Dimension '" + host.getKey() + "' dimension stack layer '" + entry.getKey() + "'",
|
||||
entry.getValue(), host.getValue().getWorldBoundary(), data, resources, registries,
|
||||
blockingErrors, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,11 +111,13 @@ public final class PackValidator {
|
||||
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||
}
|
||||
|
||||
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
|
||||
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
||||
List<File> discoveredDimensions = PackValidationIo.listJsonRecursive(dimensionsFolder);
|
||||
if (discoveredDimensions.isEmpty()) {
|
||||
blockingErrors.add("No dimension JSON files under dimensions/.");
|
||||
return new PackValidationResult(packName, blockingErrors, warnings, validatedAt);
|
||||
}
|
||||
discoveredDimensions.sort((first, second) -> first.getPath().compareTo(second.getPath()));
|
||||
File[] dimensionFiles = discoveredDimensions.toArray(File[]::new);
|
||||
|
||||
PackDimensionValidator.validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
|
||||
PackImageMapValidator.Validation imageMaps = PackImageMapValidator.validate(
|
||||
@@ -195,7 +197,7 @@ public final class PackValidator {
|
||||
if (!data.getContentGate().ready()) {
|
||||
return List.of();
|
||||
}
|
||||
boolean dimensionExcluded = loadEverything(data, dimensionFiles);
|
||||
boolean dimensionExcluded = loadEverything(packFolder, data, dimensionFiles);
|
||||
List<CompatFinding> findings = data.getCompatReport().findings();
|
||||
applyCompatFindings(minecraftVersion, dimensionExcluded, findings, blockingErrors, warnings);
|
||||
return findings;
|
||||
@@ -219,11 +221,12 @@ public final class PackValidator {
|
||||
*
|
||||
* @return true when any dimension in the pack cannot generate on this server
|
||||
*/
|
||||
private static boolean loadEverything(IrisData data, File[] dimensionFiles) {
|
||||
private static boolean loadEverything(File packFolder, IrisData data, File[] dimensionFiles) {
|
||||
boolean dimensionExcluded = false;
|
||||
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
IrisDimension dimension = data.getDimensionLoader().load(
|
||||
PackValidationIo.stripExtension(dimensionFile.getName()), false);
|
||||
PackValidationIo.deriveKey(dimensionsFolder, dimensionFile), false);
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.core.pack.PackExportClosure;
|
||||
@@ -106,11 +107,19 @@ public class IrisPackageCompiler {
|
||||
throw new IllegalStateException("Failed to create structure package staging folder " + folder.getAbsolutePath());
|
||||
}
|
||||
IrisLogging.info("Packaging Dimension " + dimension.getName() + " " + (obfuscate ? "(Obfuscated)" : ""));
|
||||
KList<IrisDimension> dimensions = new KList<>();
|
||||
for (String dimensionKey : PackExportClosure.collectDimensionKeys(dimension).stream().sorted().toList()) {
|
||||
IrisDimension exportedDimension = dm.getDimensionLoader().load(dimensionKey);
|
||||
if (exportedDimension != null) {
|
||||
dimensions.add(exportedDimension);
|
||||
}
|
||||
}
|
||||
KSet<IrisRegion> regions = new KSet<>();
|
||||
KSet<IrisBiome> biomes = new KSet<>();
|
||||
KSet<IrisEntity> entities = new KSet<>();
|
||||
KSet<IrisSpawner> spawners = new KSet<>();
|
||||
KSet<IrisGenerator> generators = new KSet<>();
|
||||
KSet<IrisExpression> expressions = new KSet<>();
|
||||
KSet<IrisLootTable> loot = new KSet<>();
|
||||
KSet<IrisBlockData> blocks = new KSet<>();
|
||||
|
||||
@@ -119,22 +128,30 @@ public class IrisPackageCompiler {
|
||||
for (String i : dm.getBlockLoader().getPossibleKeys()) {
|
||||
addIfPresent(blocks, dm.getBlockLoader().load(i));
|
||||
}
|
||||
for (String key : dm.getExpressionLoader().getPossibleKeys()) {
|
||||
addIfPresent(expressions, dm.getExpressionLoader().load(key));
|
||||
}
|
||||
|
||||
dimension.getAllRegions(() -> dm).forEach((region) -> addIfPresent(regions, region));
|
||||
dimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
exportedDimension.getAllRegions(() -> dm).forEach((region) -> addIfPresent(regions, region));
|
||||
exportedDimension.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i)));
|
||||
exportedDimension.getReachableBiomes(() -> dm).forEach((biome) -> addIfPresent(biomes, biome));
|
||||
exportedDimension.getEntitySpawners().forEach((sp) ->
|
||||
addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
|
||||
}
|
||||
regions.forEach((i) -> biomes.addAll(i.getAllBiomes(() -> dm)));
|
||||
dimension.getReachableBiomes(() -> dm).forEach((biome) -> addIfPresent(biomes, biome));
|
||||
regions.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||
regions.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
||||
dimension.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp)));
|
||||
biomes.forEach((i) -> i.getGenerators().forEach((j) -> addIfPresent(generators, j.getCachedGenerator(() -> dm))));
|
||||
biomes.forEach((r) -> r.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||
biomes.forEach((r) -> r.getEntitySpawners().forEach((sp) -> addIfPresent(spawners, dm.getSpawnerLoader().load(sp))));
|
||||
KList<IrisObjectPlacement> allPlacements = new KList<>();
|
||||
regions.forEach((r) -> allPlacements.addAll(r.getObjects()));
|
||||
biomes.forEach((i) -> allPlacements.addAll(i.getObjects()));
|
||||
for (IrisStaticObject placement : dimension.getStaticObjects()) {
|
||||
allPlacements.add(placement.toPlacement());
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
|
||||
allPlacements.add(placement.toPlacement());
|
||||
}
|
||||
}
|
||||
KSet<IrisMarker> markers = new KSet<>();
|
||||
for (String markerKey : PackExportClosure.collectMarkerKeys(allPlacements)) {
|
||||
@@ -148,7 +165,9 @@ public class IrisPackageCompiler {
|
||||
collectSpawnerEntityKeys(spawners).forEach((i) -> addIfPresent(entities, dm.getEntityLoader().load(i)));
|
||||
entities.forEach((e) -> e.getLoot().getTables().forEach((i) -> addIfPresent(loot, dm.getLootLoader().load(i))));
|
||||
Set<String> structureKeys = new LinkedHashSet<>();
|
||||
collectStructureKeys(structureKeys, dimension.getStructures());
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
collectStructureKeys(structureKeys, exportedDimension.getStructures());
|
||||
}
|
||||
regions.forEach((region) -> collectStructureKeys(structureKeys, region.getStructures()));
|
||||
biomes.forEach((biome) -> collectStructureKeys(structureKeys, biome.getStructures()));
|
||||
KMap<String, String> renameObjects = new KMap<>();
|
||||
@@ -176,8 +195,10 @@ public class IrisPackageCompiler {
|
||||
j.setPlace(newNames);
|
||||
}
|
||||
|
||||
for (IrisStaticObject placement : dimension.getStaticObjects()) {
|
||||
placement.setObject(renameObjects.get(placement.getObject()));
|
||||
for (IrisDimension exportedDimension : dimensions) {
|
||||
for (IrisStaticObject placement : exportedDimension.getStaticObjects()) {
|
||||
placement.setObject(renameObjects.get(placement.getObject()));
|
||||
}
|
||||
}
|
||||
|
||||
KMap<String, KList<String>> lookupObjects = renameObjects.flip();
|
||||
@@ -230,9 +251,12 @@ public class IrisPackageCompiler {
|
||||
}
|
||||
b.append(structureClosure.writeTo(folder, minify));
|
||||
b.append(ImageMapPackageClosure.writeAll(dm, folder, minify));
|
||||
a = new JSONObject(new Gson().toJson(dimension)).toString(minify ? 0 : 4);
|
||||
IO.writeAll(new File(folder, "dimensions/" + dimension.getLoadKey() + ".json"), a);
|
||||
b.append(IO.hash(a));
|
||||
for (IrisDimension exportedDimension : dimensions.stream()
|
||||
.sorted(Comparator.comparing(IrisDimension::getLoadKey)).toList()) {
|
||||
a = new JSONObject(new Gson().toJson(exportedDimension)).toString(minify ? 0 : 4);
|
||||
IO.writeAll(new File(folder, "dimensions/" + exportedDimension.getLoadKey() + ".json"), a);
|
||||
b.append(IO.hash(a));
|
||||
}
|
||||
|
||||
for (IrisGenerator i : generators) {
|
||||
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
|
||||
@@ -240,6 +264,13 @@ public class IrisPackageCompiler {
|
||||
b.append(IO.hash(a));
|
||||
}
|
||||
|
||||
for (IrisExpression i : expressions.stream()
|
||||
.sorted(Comparator.comparing(IrisExpression::getLoadKey)).toList()) {
|
||||
a = new JSONObject(new Gson().toJson(i)).toString(minify ? 0 : 4);
|
||||
IO.writeAll(new File(folder, "expressions/" + i.getLoadKey() + ".json"), a);
|
||||
b.append(IO.hash(a));
|
||||
}
|
||||
|
||||
c.append(IO.hash(b.toString()));
|
||||
b = new StringBuilder();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.core.project;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
@@ -179,19 +180,58 @@ public final class IrisProjectCopier {
|
||||
}
|
||||
|
||||
private static void transformDimension(Path stage, String sourceKey, String targetKey) throws IOException {
|
||||
Path oldDimension = stage.resolve("dimensions").resolve(sourceKey + ".json");
|
||||
Path newDimension = stage.resolve("dimensions").resolve(targetKey + ".json");
|
||||
Path dimensionsRoot = stage.resolve("dimensions");
|
||||
Path oldDimension = dimensionsRoot.resolve(sourceKey + ".json");
|
||||
Path newDimension = dimensionsRoot.resolve(targetKey + ".json");
|
||||
if (!oldDimension.equals(newDimension)) {
|
||||
Files.move(oldDimension, newDimension);
|
||||
}
|
||||
|
||||
JSONObject json = new JSONObject(IO.readAll(newDimension.toFile()));
|
||||
if (json.has("name")) {
|
||||
json.put("name", Form.capitalizeWords(targetKey.replace('-', ' ')));
|
||||
IO.writeAll(newDimension.toFile(), json.toString(4));
|
||||
try (Stream<Path> files = Files.walk(dimensionsRoot)) {
|
||||
for (Path dimensionFile : files.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
|
||||
.filter(path -> path.getFileName().toString().endsWith(".json"))
|
||||
.sorted()
|
||||
.toList()) {
|
||||
JSONObject json = new JSONObject(IO.readAll(dimensionFile.toFile()));
|
||||
boolean changed = false;
|
||||
if (dimensionFile.equals(newDimension) && json.has("name")) {
|
||||
json.put("name", Form.capitalizeWords(targetKey.replace('-', ' ')));
|
||||
changed = true;
|
||||
}
|
||||
if (rewriteDimensionStackReferences(json, sourceKey, targetKey)) {
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
IO.writeAll(dimensionFile.toFile(), json.toString(4));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean rewriteDimensionStackReferences(JSONObject json, String sourceKey, String targetKey) {
|
||||
JSONObject stack = json.optJSONObject("dimensionStack");
|
||||
JSONArray dimensions = stack == null ? null : stack.optJSONArray("dimensions");
|
||||
if (dimensions == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
JSONArray rewritten = new JSONArray();
|
||||
for (int index = 0; index < dimensions.length(); index++) {
|
||||
Object value = dimensions.opt(index);
|
||||
if (sourceKey.equals(value)) {
|
||||
rewritten.put(targetKey);
|
||||
changed = true;
|
||||
} else {
|
||||
rewritten.put(value == null ? JSONObject.NULL : value);
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
stack.put("dimensions", rewritten);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static void publish(Path stage, Path target) throws IOException {
|
||||
try {
|
||||
Files.move(stage, target, StandardCopyOption.ATOMIC_MOVE);
|
||||
|
||||
@@ -515,7 +515,7 @@ public class BoardSVC implements IrisService, BoardProvider {
|
||||
}
|
||||
|
||||
lines.add("&7&m ");
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, z).getName())));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_REGION, MessageArgument.untrusted("region", engine.getRegion(x, y, z).getName())));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_BIOME, MessageArgument.untrusted("biome", engine.getBiomeOrMantle(x, y, z).getName())));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_HEIGHT, MessageArgument.trusted("height", Math.round(engine.getHeight(x, z)))));
|
||||
lines.add(IrisLanguage.text(BukkitUiMessages.SCOREBOARD_SLOPE, MessageArgument.trusted("slope", Form.f(engine.getSlope(x, z), 2))));
|
||||
|
||||
@@ -326,7 +326,10 @@ public class TreeSVC implements IrisService {
|
||||
|
||||
// Add more or find any in the region
|
||||
if (isUseAll || placements.isEmpty()) {
|
||||
IrisRegion region = worldAccess.getEngine().getRegion(location.getBlockX(), location.getBlockZ());
|
||||
IrisRegion region = worldAccess.getEngine().getRegion(
|
||||
location.getBlockX(),
|
||||
location.getBlockY() - worldAccess.getTarget().getWorld().minHeight(),
|
||||
location.getBlockZ());
|
||||
placements.addAll(matchObjectPlacements(region.getObjects(), size, type, worldAccess.getData()));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStack;
|
||||
import art.arcane.iris.engine.object.IrisDimensionStackBlend;
|
||||
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class DimensionStackContext {
|
||||
private static final int LAYOUT_CACHE_SIZE = 65536;
|
||||
private static final long SEAM_SALT = 0x44494D535441434BL;
|
||||
private static final long HASH_OFFSET = 0xCBF29CE484222325L;
|
||||
private static final long HASH_PRIME = 0x100000001B3L;
|
||||
|
||||
private final Engine engine;
|
||||
private final int outputHeight;
|
||||
private final int spacer;
|
||||
private final List<DimensionTerrainContext> layersTopToBottom;
|
||||
private final List<DimensionTerrainContext> layersBottomToTop;
|
||||
private final List<SeamOffsetSampler> seamOffsetSamplersBottomToTop;
|
||||
private final Cache<Long, DimensionStackLayout> layoutCache;
|
||||
|
||||
private DimensionStackContext(ContextState state) {
|
||||
engine = state.engine();
|
||||
outputHeight = state.outputHeight();
|
||||
spacer = state.spacer();
|
||||
layersBottomToTop = List.copyOf(state.layersBottomToTop());
|
||||
ArrayList<DimensionTerrainContext> reversed = new ArrayList<>(layersBottomToTop);
|
||||
Collections.reverse(reversed);
|
||||
layersTopToBottom = List.copyOf(reversed);
|
||||
seamOffsetSamplersBottomToTop = List.copyOf(state.seamOffsetSamplersBottomToTop());
|
||||
layoutCache = Caffeine.newBuilder().maximumSize(LAYOUT_CACHE_SIZE).build();
|
||||
}
|
||||
|
||||
public static DimensionStackContext create(Engine engine, IrisDimensionStack stack) {
|
||||
if (stack == null) {
|
||||
throw new IllegalArgumentException("Dimension stack configuration is required");
|
||||
}
|
||||
KList<String> dimensionKeys = stack.getDimensions();
|
||||
validateRuntimeContract(engine.getDimension(), dimensionKeys);
|
||||
if (stack.getSpacer() < 0 || stack.getSpacer() > 256) {
|
||||
throw new IllegalArgumentException("Dimension stack spacer must be between 0 and 256");
|
||||
}
|
||||
|
||||
ArrayList<DimensionTerrainContext> layersBottomToTop = new ArrayList<>(dimensionKeys.size());
|
||||
for (int keyIndex = dimensionKeys.size() - 1; keyIndex >= 0; keyIndex--) {
|
||||
String dimensionKey = dimensionKeys.get(keyIndex);
|
||||
IrisDimension dimension = dimensionKey.equals(engine.getDimension().getLoadKey())
|
||||
? engine.getDimension()
|
||||
: engine.getData().getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Dimension stack reference does not resolve in this pack: " + dimensionKey);
|
||||
}
|
||||
layersBottomToTop.add(DimensionTerrainContext.forStack(engine, dimension));
|
||||
}
|
||||
|
||||
List<SeamOffsetSampler> seamSamplers = createSeamSamplers(
|
||||
engine,
|
||||
stack.getBlend(),
|
||||
layersBottomToTop
|
||||
);
|
||||
return new DimensionStackContext(new ContextState(
|
||||
engine,
|
||||
engine.getHeight(),
|
||||
stack.getSpacer(),
|
||||
layersBottomToTop,
|
||||
seamSamplers
|
||||
));
|
||||
}
|
||||
|
||||
private static void validateRuntimeContract(IrisDimension host, KList<String> dimensionKeys) {
|
||||
if (dimensionKeys == null || dimensionKeys.size() < 2) {
|
||||
throw new IllegalArgumentException("Dimension stack requires at least two dimensions");
|
||||
}
|
||||
String hostKey = host.getLoadKey();
|
||||
if (!hostKey.equals(dimensionKeys.get(dimensionKeys.size() - 1))) {
|
||||
throw new IllegalArgumentException("Dimension stack host must be its final bottom dimension");
|
||||
}
|
||||
int hostReferences = 0;
|
||||
for (String dimensionKey : dimensionKeys) {
|
||||
if (dimensionKey == null || dimensionKey.isBlank()) {
|
||||
throw new IllegalArgumentException("Dimension stack keys must be nonblank");
|
||||
}
|
||||
if (hostKey.equals(dimensionKey)) {
|
||||
hostReferences++;
|
||||
}
|
||||
}
|
||||
if (hostReferences != 1) {
|
||||
throw new IllegalArgumentException("Dimension stack must contain its host exactly once");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SeamOffsetSampler> createSeamSamplers(
|
||||
Engine engine,
|
||||
IrisDimensionStackBlend blend,
|
||||
List<DimensionTerrainContext> layersBottomToTop
|
||||
) {
|
||||
int seamCount = layersBottomToTop.size() - 1;
|
||||
ArrayList<SeamOffsetSampler> samplers = new ArrayList<>(seamCount);
|
||||
if (blend == null || blend.getAmplitude() == 0) {
|
||||
for (int seamIndex = 0; seamIndex < seamCount; seamIndex++) {
|
||||
samplers.add((x, z) -> 0);
|
||||
}
|
||||
return samplers;
|
||||
}
|
||||
if (blend.getAmplitude() < 0 || blend.getAmplitude() > 256) {
|
||||
throw new IllegalArgumentException("Dimension stack blend amplitude must be between 0 and 256");
|
||||
}
|
||||
IrisGeneratorStyle style = blend.getStyle();
|
||||
if (style == null) {
|
||||
throw new IllegalArgumentException("Dimension stack blend style is required when amplitude is positive");
|
||||
}
|
||||
|
||||
for (int seamIndex = 0; seamIndex < seamCount; seamIndex++) {
|
||||
DimensionTerrainContext lower = layersBottomToTop.get(seamIndex);
|
||||
DimensionTerrainContext upper = layersBottomToTop.get(seamIndex + 1);
|
||||
long salt = seamSalt(
|
||||
seamIndex,
|
||||
lower.getDimension().getLoadKey(),
|
||||
upper.getDimension().getLoadKey()
|
||||
);
|
||||
RNG seamRng = new RNG(engine.getSeedManager().getTerrain() ^ salt);
|
||||
CNG seamNoise = style.create(seamRng, engine.getData(), engine);
|
||||
int amplitude = blend.getAmplitude();
|
||||
samplers.add((x, z) -> clampSeamOffset(
|
||||
seamNoise.fitDouble(-amplitude, amplitude, x, z), amplitude));
|
||||
}
|
||||
return samplers;
|
||||
}
|
||||
|
||||
static int clampSeamOffset(double sampledOffset, int amplitude) {
|
||||
if (!Double.isFinite(sampledOffset)) {
|
||||
return 0;
|
||||
}
|
||||
double clamped = Math.max(-amplitude, Math.min(amplitude, sampledOffset));
|
||||
return (int) Math.round(clamped);
|
||||
}
|
||||
|
||||
static long seamSalt(int seamIndex, String lowerDimensionKey, String upperDimensionKey) {
|
||||
long hash = HASH_OFFSET ^ SEAM_SALT;
|
||||
hash = hashString(hash, lowerDimensionKey);
|
||||
hash ^= seamIndex;
|
||||
hash *= HASH_PRIME;
|
||||
return hashString(hash, upperDimensionKey);
|
||||
}
|
||||
|
||||
private static long hashString(long hash, String value) {
|
||||
long result = hash;
|
||||
for (int characterIndex = 0; characterIndex < value.length(); characterIndex++) {
|
||||
result ^= value.charAt(characterIndex);
|
||||
result *= HASH_PRIME;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public DimensionStackLayout sample(int x, int z) {
|
||||
return sample(x, z, usesTemporaryNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
private DimensionStackLayout sample(int x, int z, boolean naturalFallback) {
|
||||
ArrayList<DimensionStackLayout.LayerInput> layerInputs =
|
||||
new ArrayList<>(layersBottomToTop.size());
|
||||
for (DimensionTerrainContext terrainContext : layersBottomToTop) {
|
||||
DimensionTerrainContext.ColumnSample column = terrainContext.sampleColumn(
|
||||
x, z, naturalFallback);
|
||||
int normalTerrainHeight = localCoordinate(
|
||||
column.terrainHeight(),
|
||||
terrainContext.getLocalHeight()
|
||||
);
|
||||
int fluidHeight = localCoordinate(
|
||||
column.fluidHeight(),
|
||||
terrainContext.getLocalHeight()
|
||||
);
|
||||
layerInputs.add(new DimensionStackLayout.LayerInput(
|
||||
terrainContext,
|
||||
column.biome(),
|
||||
column.region(),
|
||||
column.rockBlock(),
|
||||
column.fluidBlock(),
|
||||
column.surfaceBlock(),
|
||||
normalTerrainHeight,
|
||||
fluidHeight
|
||||
));
|
||||
}
|
||||
|
||||
int[] seamOffsets = new int[seamOffsetSamplersBottomToTop.size()];
|
||||
for (int seamIndex = 0; seamIndex < seamOffsets.length; seamIndex++) {
|
||||
seamOffsets[seamIndex] = seamOffsetSamplersBottomToTop.get(seamIndex).sample(x, z);
|
||||
}
|
||||
return DimensionStackLayout.create(outputHeight, spacer, layerInputs, seamOffsets);
|
||||
}
|
||||
|
||||
private static int localCoordinate(double sample, int localHeight) {
|
||||
int maximum = Math.max(0, localHeight - 1);
|
||||
long rounded = Math.round(sample);
|
||||
return (int) Math.max(0L, Math.min(maximum, rounded));
|
||||
}
|
||||
|
||||
public int getStackTerrainHeight(int x, int z) {
|
||||
return (int) (sampleTopHeights(x, z) >>> 32);
|
||||
}
|
||||
|
||||
public int getStackTopHeight(int x, int z) {
|
||||
return (int) sampleTopHeights(x, z);
|
||||
}
|
||||
|
||||
public DimensionStackLayout getLayout(int x, int z) {
|
||||
boolean naturalFallback = usesTemporaryNaturalFallback(x, z);
|
||||
long columnKey = ((long) x << 32) ^ (z & 0xFFFFFFFFL);
|
||||
return resolveLayout(
|
||||
layoutCache,
|
||||
columnKey,
|
||||
naturalFallback,
|
||||
() -> sample(x, z, naturalFallback)
|
||||
);
|
||||
}
|
||||
|
||||
public DimensionStackLayout.Layer getLayerAt(int x, int y, int z) {
|
||||
return getLayout(x, z).layerAt(y);
|
||||
}
|
||||
|
||||
static DimensionStackLayout resolveLayout(
|
||||
Cache<Long, DimensionStackLayout> cache,
|
||||
long columnKey,
|
||||
boolean naturalFallback,
|
||||
Supplier<DimensionStackLayout> sampler
|
||||
) {
|
||||
if (naturalFallback) {
|
||||
return sampler.get();
|
||||
}
|
||||
return cache.get(columnKey, ignored -> sampler.get());
|
||||
}
|
||||
|
||||
private long sampleTopHeights(int x, int z) {
|
||||
boolean naturalFallback = usesTemporaryNaturalFallback(x, z);
|
||||
int baseY = 0;
|
||||
int renderedTerrainTopY = Integer.MIN_VALUE;
|
||||
int renderedContentTopY = Integer.MIN_VALUE;
|
||||
int outputMaxY = outputHeight - 1;
|
||||
int previousContentTopY = Integer.MIN_VALUE;
|
||||
int previousRenderMinY = Integer.MIN_VALUE;
|
||||
int previousRenderMaxY = Integer.MIN_VALUE;
|
||||
int previousSurfaceY = Integer.MIN_VALUE;
|
||||
boolean previousVisible = false;
|
||||
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
DimensionTerrainContext terrainContext = layersBottomToTop.get(layerIndex);
|
||||
if (layerIndex > 0) {
|
||||
int gapMinY = (int) Math.max(0L, (long) previousContentTopY + 1L);
|
||||
int gapMaxY = (int) Math.min((long) outputMaxY, (long) baseY - 1L);
|
||||
int previousTerrainY = previousVisible && previousSurfaceY >= previousRenderMinY
|
||||
? Math.min(outputMaxY, previousSurfaceY)
|
||||
: Integer.MIN_VALUE;
|
||||
renderedTerrainTopY = DimensionStackLayout.eraseHeightThroughGap(
|
||||
renderedTerrainTopY,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
previousTerrainY
|
||||
);
|
||||
renderedContentTopY = DimensionStackLayout.eraseHeightThroughGap(
|
||||
renderedContentTopY,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
previousVisible ? previousRenderMaxY : Integer.MIN_VALUE
|
||||
);
|
||||
}
|
||||
int localTerrainHeight = localCoordinate(
|
||||
terrainContext.getNormalTerrainHeight(x, z, naturalFallback),
|
||||
terrainContext.getLocalHeight()
|
||||
);
|
||||
int localFluidHeight = localCoordinate(
|
||||
terrainContext.getFluidHeight(x, z, naturalFallback),
|
||||
terrainContext.getLocalHeight()
|
||||
);
|
||||
int surfaceY = saturatedAdd(baseY, localTerrainHeight);
|
||||
int fluidY = saturatedAdd(baseY, localFluidHeight);
|
||||
int layerContentTopY = Math.max(surfaceY, fluidY);
|
||||
int renderMinY = Math.max(0, baseY);
|
||||
int renderMaxY = Math.min(outputMaxY, layerContentTopY);
|
||||
if (renderMinY <= renderMaxY) {
|
||||
renderedContentTopY = Math.max(renderedContentTopY, renderMaxY);
|
||||
renderedTerrainTopY = DimensionStackLayout.mergeVisibleTerrainHeight(
|
||||
renderedTerrainTopY,
|
||||
renderMinY,
|
||||
renderMaxY,
|
||||
surfaceY,
|
||||
outputMaxY
|
||||
);
|
||||
}
|
||||
previousContentTopY = layerContentTopY;
|
||||
previousRenderMinY = renderMinY;
|
||||
previousRenderMaxY = renderMaxY;
|
||||
previousSurfaceY = surfaceY;
|
||||
previousVisible = renderMinY <= renderMaxY;
|
||||
if (layerIndex + 1 < layersBottomToTop.size()) {
|
||||
int seamOffset = seamOffsetSamplersBottomToTop.get(layerIndex).sample(x, z);
|
||||
baseY = saturatedAdd(
|
||||
layerContentTopY,
|
||||
saturatedAdd(spacer, saturatedAdd(1, seamOffset))
|
||||
);
|
||||
}
|
||||
}
|
||||
int clippedTerrainTopY = Math.max(0, renderedTerrainTopY);
|
||||
int clippedContentTopY = Math.max(0, renderedContentTopY);
|
||||
return ((long) clippedTerrainTopY << 32) | (clippedContentTopY & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private static int saturatedAdd(int left, int right) {
|
||||
long sum = (long) left + right;
|
||||
if (sum > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
if (sum < Integer.MIN_VALUE) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
return (int) sum;
|
||||
}
|
||||
|
||||
public int getOutputHeight() {
|
||||
return outputHeight;
|
||||
}
|
||||
|
||||
public int getSpacer() {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
public boolean usesTemporaryNaturalFallback(int x, int z) {
|
||||
return engine.answersFromNaturalTerrain(x, z);
|
||||
}
|
||||
|
||||
public List<DimensionTerrainContext> getLayersTopToBottom() {
|
||||
return layersTopToBottom;
|
||||
}
|
||||
|
||||
public List<DimensionTerrainContext> getLayersBottomToTop() {
|
||||
return layersBottomToTop;
|
||||
}
|
||||
|
||||
private interface SeamOffsetSampler {
|
||||
int sample(double x, double z);
|
||||
}
|
||||
|
||||
private record ContextState(
|
||||
Engine engine,
|
||||
int outputHeight,
|
||||
int spacer,
|
||||
List<DimensionTerrainContext> layersBottomToTop,
|
||||
List<SeamOffsetSampler> seamOffsetSamplersBottomToTop
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class DimensionStackLayout {
|
||||
private final List<Layer> layersBottomToTop;
|
||||
private final List<Layer> layersTopToBottom;
|
||||
private final int stackTerrainTopY;
|
||||
private final int stackTopY;
|
||||
private final int clippedStackTerrainTopY;
|
||||
private final int clippedStackTopY;
|
||||
|
||||
private DimensionStackLayout(LayoutState state) {
|
||||
layersBottomToTop = List.copyOf(state.layersBottomToTop());
|
||||
ArrayList<Layer> reversed = new ArrayList<>(layersBottomToTop);
|
||||
Collections.reverse(reversed);
|
||||
layersTopToBottom = List.copyOf(reversed);
|
||||
stackTerrainTopY = state.stackTerrainTopY();
|
||||
stackTopY = state.stackTopY();
|
||||
clippedStackTerrainTopY = state.clippedStackTerrainTopY();
|
||||
clippedStackTopY = state.clippedStackTopY();
|
||||
}
|
||||
|
||||
static DimensionStackLayout create(
|
||||
int outputHeight,
|
||||
int spacer,
|
||||
List<LayerInput> layerInputsBottomToTop,
|
||||
int[] seamOffsetsBottomToTop
|
||||
) {
|
||||
if (outputHeight < 1) {
|
||||
throw new IllegalArgumentException("Output height must be positive");
|
||||
}
|
||||
if (spacer < 0) {
|
||||
throw new IllegalArgumentException("Dimension stack spacer cannot be negative");
|
||||
}
|
||||
if (layerInputsBottomToTop.isEmpty()) {
|
||||
throw new IllegalArgumentException("Dimension stack requires at least one layer");
|
||||
}
|
||||
if (seamOffsetsBottomToTop.length != layerInputsBottomToTop.size() - 1) {
|
||||
throw new IllegalArgumentException("Dimension stack seam offset count does not match its layers");
|
||||
}
|
||||
|
||||
int outputMaxY = outputHeight - 1;
|
||||
int baseY = 0;
|
||||
int stackTerrainTopY = Integer.MIN_VALUE;
|
||||
int stackTopY = Integer.MIN_VALUE;
|
||||
int renderedTerrainTopY = Integer.MIN_VALUE;
|
||||
int renderedContentTopY = Integer.MIN_VALUE;
|
||||
ArrayList<Layer> layers = new ArrayList<>(layerInputsBottomToTop.size());
|
||||
for (int layerIndex = 0; layerIndex < layerInputsBottomToTop.size(); layerIndex++) {
|
||||
LayerInput input = layerInputsBottomToTop.get(layerIndex);
|
||||
int seamOffsetBelow = layerIndex == 0 ? 0 : seamOffsetsBottomToTop[layerIndex - 1];
|
||||
int spacerBelow = layerIndex == 0 ? 0 : spacer;
|
||||
if (layerIndex > 0) {
|
||||
Layer lower = layers.get(layerIndex - 1);
|
||||
int gapMinY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
|
||||
int gapMaxY = (int) Math.min((long) outputMaxY, (long) baseY - 1L);
|
||||
renderedTerrainTopY = eraseHeightThroughGap(
|
||||
renderedTerrainTopY,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
visibleTerrainY(lower)
|
||||
);
|
||||
renderedContentTopY = eraseHeightThroughGap(
|
||||
renderedContentTopY,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
lower.visible() ? lower.renderMaxY() : Integer.MIN_VALUE
|
||||
);
|
||||
}
|
||||
int surfaceY = saturatedAdd(baseY, input.normalTerrainHeight());
|
||||
int fluidY = saturatedAdd(baseY, input.fluidHeight());
|
||||
int contentTopY = Math.max(surfaceY, fluidY);
|
||||
int renderMinY = Math.max(0, baseY);
|
||||
int renderMaxY = Math.min(outputMaxY, contentTopY);
|
||||
boolean visible = renderMinY <= renderMaxY;
|
||||
Layer layer = new Layer(
|
||||
input.terrainContext(),
|
||||
input.biome(),
|
||||
input.region(),
|
||||
input.rockBlock(),
|
||||
input.fluidBlock(),
|
||||
input.surfaceBlock(),
|
||||
baseY,
|
||||
input.normalTerrainHeight(),
|
||||
input.fluidHeight(),
|
||||
surfaceY,
|
||||
fluidY,
|
||||
contentTopY,
|
||||
spacerBelow,
|
||||
seamOffsetBelow,
|
||||
renderMinY,
|
||||
renderMaxY,
|
||||
clip(surfaceY, outputMaxY),
|
||||
clip(fluidY, outputMaxY),
|
||||
visible
|
||||
);
|
||||
layers.add(layer);
|
||||
stackTerrainTopY = Math.max(stackTerrainTopY, surfaceY);
|
||||
stackTopY = Math.max(stackTopY, contentTopY);
|
||||
if (visible) {
|
||||
renderedContentTopY = Math.max(renderedContentTopY, renderMaxY);
|
||||
renderedTerrainTopY = mergeVisibleTerrainHeight(
|
||||
renderedTerrainTopY,
|
||||
renderMinY,
|
||||
renderMaxY,
|
||||
surfaceY,
|
||||
outputMaxY
|
||||
);
|
||||
}
|
||||
if (layerIndex + 1 < layerInputsBottomToTop.size()) {
|
||||
baseY = saturatedAdd(
|
||||
contentTopY,
|
||||
saturatedAdd(spacer, saturatedAdd(1, seamOffsetsBottomToTop[layerIndex]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new DimensionStackLayout(new LayoutState(
|
||||
layers,
|
||||
stackTerrainTopY,
|
||||
stackTopY,
|
||||
Math.max(0, renderedTerrainTopY),
|
||||
Math.max(0, renderedContentTopY)
|
||||
));
|
||||
}
|
||||
|
||||
private static int saturatedAdd(int left, int right) {
|
||||
long sum = (long) left + right;
|
||||
if (sum > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
if (sum < Integer.MIN_VALUE) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
return (int) sum;
|
||||
}
|
||||
|
||||
private static int clip(int value, int outputMaxY) {
|
||||
return Math.max(0, Math.min(outputMaxY, value));
|
||||
}
|
||||
|
||||
static int mergeVisibleTerrainHeight(
|
||||
int currentTerrainTopY,
|
||||
int renderMinY,
|
||||
int renderMaxY,
|
||||
int surfaceY,
|
||||
int outputMaxY
|
||||
) {
|
||||
int terrainTopY = currentTerrainTopY;
|
||||
if (terrainTopY >= renderMinY && terrainTopY <= renderMaxY) {
|
||||
terrainTopY = Integer.MIN_VALUE;
|
||||
}
|
||||
if (surfaceY >= renderMinY) {
|
||||
terrainTopY = Math.max(terrainTopY, Math.min(outputMaxY, surfaceY));
|
||||
}
|
||||
return terrainTopY;
|
||||
}
|
||||
|
||||
static int eraseHeightThroughGap(
|
||||
int currentTopY,
|
||||
int gapMinY,
|
||||
int gapMaxY,
|
||||
int fallbackTopY
|
||||
) {
|
||||
if (currentTopY >= gapMinY && currentTopY <= gapMaxY) {
|
||||
return fallbackTopY;
|
||||
}
|
||||
return currentTopY;
|
||||
}
|
||||
|
||||
private static int visibleTerrainY(Layer layer) {
|
||||
if (!layer.visible() || layer.surfaceY() < layer.renderMinY()) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
return layer.clippedSurfaceY();
|
||||
}
|
||||
|
||||
public List<Layer> layersBottomToTop() {
|
||||
return layersBottomToTop;
|
||||
}
|
||||
|
||||
public List<Layer> layersTopToBottom() {
|
||||
return layersTopToBottom;
|
||||
}
|
||||
|
||||
public int stackTerrainTopY() {
|
||||
return stackTerrainTopY;
|
||||
}
|
||||
|
||||
public int stackTopY() {
|
||||
return stackTopY;
|
||||
}
|
||||
|
||||
public int clippedStackTerrainTopY() {
|
||||
return clippedStackTerrainTopY;
|
||||
}
|
||||
|
||||
public int clippedStackTopY() {
|
||||
return clippedStackTopY;
|
||||
}
|
||||
|
||||
public Layer topTerrainLayer() {
|
||||
Layer highest = null;
|
||||
int highestSurfaceY = Integer.MIN_VALUE;
|
||||
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
Layer layer = layersBottomToTop.get(layerIndex);
|
||||
if (layerIndex > 0) {
|
||||
Layer lower = layersBottomToTop.get(layerIndex - 1);
|
||||
if ((long) highestSurfaceY > lower.contentTopY()
|
||||
&& (long) highestSurfaceY < layer.localBaseY()) {
|
||||
highest = visibleTerrainY(lower) == Integer.MIN_VALUE ? null : lower;
|
||||
highestSurfaceY = visibleTerrainY(lower);
|
||||
}
|
||||
}
|
||||
if (layer.containsRenderedY(highestSurfaceY)) {
|
||||
highest = null;
|
||||
highestSurfaceY = Integer.MIN_VALUE;
|
||||
}
|
||||
if (!layer.visible() || layer.surfaceY() < layer.renderMinY()) {
|
||||
continue;
|
||||
}
|
||||
if (layer.clippedSurfaceY() >= highestSurfaceY) {
|
||||
highest = layer;
|
||||
highestSurfaceY = layer.clippedSurfaceY();
|
||||
}
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
public Layer surfaceLayer() {
|
||||
Layer terrainLayer = topTerrainLayer();
|
||||
return terrainLayer == null ? topVisibleLayer() : terrainLayer;
|
||||
}
|
||||
|
||||
private Layer topVisibleLayer() {
|
||||
Layer highest = null;
|
||||
int highestContentY = Integer.MIN_VALUE;
|
||||
for (int layerIndex = 0; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
Layer layer = layersBottomToTop.get(layerIndex);
|
||||
if (layerIndex > 0) {
|
||||
Layer lower = layersBottomToTop.get(layerIndex - 1);
|
||||
if ((long) highestContentY > lower.contentTopY()
|
||||
&& (long) highestContentY < layer.localBaseY()) {
|
||||
highest = lower.visible() ? lower : null;
|
||||
highestContentY = lower.visible()
|
||||
? lower.renderMaxY()
|
||||
: Integer.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
if (layer.visible() && layer.renderMaxY() >= highestContentY) {
|
||||
highest = layer;
|
||||
highestContentY = layer.renderMaxY();
|
||||
}
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
public Layer layerAt(int y) {
|
||||
Layer bottom = layersBottomToTop.get(0);
|
||||
Layer owner = bottom;
|
||||
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
Layer lower = layersBottomToTop.get(layerIndex - 1);
|
||||
Layer layer = layersBottomToTop.get(layerIndex);
|
||||
if ((long) y > lower.contentTopY() && (long) y < layer.localBaseY()) {
|
||||
owner = bottom;
|
||||
}
|
||||
if (layer.containsRenderedY(y)) {
|
||||
owner = layer;
|
||||
}
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
public boolean containsUpperLayerY(int y) {
|
||||
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
if (layersBottomToTop.get(layerIndex).containsRenderedY(y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isHostFeatureProtectedY(int y) {
|
||||
for (int layerIndex = 1; layerIndex < layersBottomToTop.size(); layerIndex++) {
|
||||
Layer lower = layersBottomToTop.get(layerIndex - 1);
|
||||
Layer upper = layersBottomToTop.get(layerIndex);
|
||||
if (upper.containsRenderedY(y)
|
||||
|| ((long) y > lower.contentTopY() && (long) y < upper.localBaseY())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static record LayerInput(
|
||||
DimensionTerrainContext terrainContext,
|
||||
IrisBiome biome,
|
||||
IrisRegion region,
|
||||
PlatformBlockState rockBlock,
|
||||
PlatformBlockState fluidBlock,
|
||||
PlatformBlockState surfaceBlock,
|
||||
int normalTerrainHeight,
|
||||
int fluidHeight
|
||||
) {
|
||||
}
|
||||
|
||||
public record Layer(
|
||||
DimensionTerrainContext terrainContext,
|
||||
IrisBiome biome,
|
||||
IrisRegion region,
|
||||
PlatformBlockState rockBlock,
|
||||
PlatformBlockState fluidBlock,
|
||||
PlatformBlockState surfaceBlock,
|
||||
int localBaseY,
|
||||
int normalTerrainHeight,
|
||||
int fluidHeight,
|
||||
int surfaceY,
|
||||
int fluidY,
|
||||
int contentTopY,
|
||||
int spacerBelow,
|
||||
int seamOffsetBelow,
|
||||
int renderMinY,
|
||||
int renderMaxY,
|
||||
int clippedSurfaceY,
|
||||
int clippedFluidY,
|
||||
boolean visible
|
||||
) {
|
||||
public boolean containsRenderedY(int y) {
|
||||
return visible && y >= renderMinY && y <= renderMaxY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record LayoutState(
|
||||
List<Layer> layersBottomToTop,
|
||||
int stackTerrainTopY,
|
||||
int stackTopY,
|
||||
int clippedStackTerrainTopY,
|
||||
int clippedStackTopY
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
package art.arcane.iris.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class DimensionTerrainContext implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
|
||||
private final Engine engine;
|
||||
private final IrisDimension dimension;
|
||||
private final IrisData data;
|
||||
private final int localHeight;
|
||||
private final ProceduralStream<Double> heightStream;
|
||||
private final ProceduralStream<Double> slopeStream;
|
||||
private final ProceduralStream<Double> fluidHeightStream;
|
||||
private final ProceduralStream<IrisBiome> biomeStream;
|
||||
private final ProceduralStream<IrisRegion> regionStream;
|
||||
private final ProceduralStream<PlatformBlockState> rockStream;
|
||||
private final FluidBlockSampler fluidBlockSampler;
|
||||
private final IrisImageMapRuntime imageMapRuntime;
|
||||
private final boolean selfReferencing;
|
||||
private final SelfFallback selfFallback;
|
||||
|
||||
private DimensionTerrainContext(ContextState state) {
|
||||
engine = state.engine();
|
||||
dimension = state.dimension();
|
||||
data = state.data();
|
||||
localHeight = state.localHeight();
|
||||
heightStream = state.heightStream();
|
||||
slopeStream = state.slopeStream();
|
||||
fluidHeightStream = state.fluidHeightStream();
|
||||
biomeStream = state.biomeStream();
|
||||
regionStream = state.regionStream();
|
||||
rockStream = state.rockStream();
|
||||
fluidBlockSampler = state.fluidBlockSampler();
|
||||
imageMapRuntime = state.imageMapRuntime();
|
||||
selfReferencing = state.selfReferencing();
|
||||
selfFallback = state.selfFallback();
|
||||
}
|
||||
|
||||
public static DimensionTerrainContext forStack(Engine engine, IrisDimension dimension) {
|
||||
boolean selfReferencing = dimension.getLoadKey().equals(engine.getDimension().getLoadKey());
|
||||
if (selfReferencing) {
|
||||
return createSelfReferencing(engine, false);
|
||||
}
|
||||
|
||||
int localHeight = dimension.getMaxHeight() - dimension.getMinHeight();
|
||||
return createCrossReferencing(
|
||||
engine,
|
||||
dimension,
|
||||
localHeight,
|
||||
dimension.getMinHeight(),
|
||||
"stack-" + dimension.getLoadKey(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static DimensionTerrainContext forUpper(Engine engine, IrisDimension dimension) {
|
||||
boolean selfReferencing = dimension.getLoadKey().equals(engine.getDimension().getLoadKey());
|
||||
if (selfReferencing) {
|
||||
return createSelfReferencing(engine, true);
|
||||
}
|
||||
|
||||
return createCrossReferencing(
|
||||
engine,
|
||||
dimension,
|
||||
engine.getHeight(),
|
||||
engine.getMinHeight(),
|
||||
"upper",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static DimensionTerrainContext createSelfReferencing(Engine engine, boolean natural) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
double fluidHeight = engine.getDimension().getFluidHeight();
|
||||
ProceduralStream<Double> fluidHeightStream = natural
|
||||
? ProceduralStream.ofDouble((x, z) -> fluidHeight)
|
||||
: complex.getRiverWaterSurfaceStream();
|
||||
FluidBlockSampler fluidBlockSampler = natural
|
||||
? (x, z) -> complex.getFluidStream().get(x, z)
|
||||
: complex::resolveSurfaceFluid;
|
||||
SelfFallback selfFallback = natural
|
||||
? null
|
||||
: new SelfFallback(
|
||||
complex.getNaturalHeightStream(),
|
||||
complex.getNaturalTrueBiomeStream(),
|
||||
fluidHeight,
|
||||
(x, z) -> complex.getFluidStream().get(x, z)
|
||||
);
|
||||
return new DimensionTerrainContext(new ContextState(
|
||||
engine,
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
engine.getHeight(),
|
||||
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
|
||||
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
|
||||
fluidHeightStream,
|
||||
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
fluidBlockSampler,
|
||||
complex.getImageMapRuntime(),
|
||||
true,
|
||||
selfFallback
|
||||
));
|
||||
}
|
||||
|
||||
private static DimensionTerrainContext createCrossReferencing(
|
||||
Engine engine,
|
||||
IrisDimension dimension,
|
||||
int localHeight,
|
||||
int minimumWorldHeight,
|
||||
String cachePrefix,
|
||||
boolean includeFluid
|
||||
) {
|
||||
IrisData resolvedData = dimension.getLoader();
|
||||
if (resolvedData == null) {
|
||||
resolvedData = engine.getData();
|
||||
}
|
||||
IrisData dimensionData = resolvedData;
|
||||
IrisImageMapRuntime imageMapRuntime = IrisImageMapRuntime.compile(
|
||||
dimensionData,
|
||||
dimension,
|
||||
minimumWorldHeight
|
||||
);
|
||||
long seedOffset = dimension.getLoadKey().hashCode();
|
||||
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
|
||||
double fluidHeight = dimension.getFluidHeight();
|
||||
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
||||
DataProvider dataProvider = () -> dimensionData;
|
||||
IrisBiome configuredFocusBiome = loadFocusBiome(dimension, dimensionData);
|
||||
IrisBiome focusBiome = configuredFocusBiome == null
|
||||
? null
|
||||
: configuredFocusBiome.withInferredType(InferredType.LAND);
|
||||
IrisRegion focusRegion = focusBiome == null
|
||||
? loadFocusRegion(dimension, dimensionData)
|
||||
: findFocusRegion(focusBiome, dimension, dataProvider, dimensionData);
|
||||
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
|
||||
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
KList<IrisRegion> preparedRegions = new KList<>();
|
||||
if (focusRegion != null) {
|
||||
preparedRegions.add(focusRegion);
|
||||
focusRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
} else {
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
preparedRegions.add(region);
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
|
||||
if (!mappedBiome.isCompatExcluded()) {
|
||||
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (focusBiome != null) {
|
||||
allBiomes.add(focusBiome);
|
||||
}
|
||||
|
||||
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
|
||||
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds =
|
||||
new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
|
||||
for (IrisBiome biome : allBiomes) {
|
||||
double minimum = 0D;
|
||||
double maximum = 0D;
|
||||
for (IrisGenerator generator : group.generators()) {
|
||||
String key = generator.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
maximum += biome.getGenLinkMax(key, engine);
|
||||
minimum += biome.getGenLinkMin(key, engine);
|
||||
}
|
||||
interpolatorBounds.put(biome, new NoiseBounds(minimum, maximum));
|
||||
}
|
||||
generatorBounds.put(group.interpolator(), interpolatorBounds);
|
||||
}
|
||||
|
||||
ProceduralStream<Double> regionStyleStream = dimension.getRegionStyle()
|
||||
.create(rng.nextParallelRNG(883), dimensionData).stream()
|
||||
.zoom(dimension.getRegionZoom());
|
||||
KList<IrisRegion> regionPool = new KList<>();
|
||||
for (IrisRegion region : dimensionData.getRegionLoader().loadAll(dimension.getRegions())) {
|
||||
if (!region.isCompatExcluded()) {
|
||||
regionPool.add(region);
|
||||
}
|
||||
}
|
||||
ProceduralStream<IrisRegion> proceduralRegionStream = focusRegion == null
|
||||
? regionStyleStream.selectRarity(regionPool)
|
||||
: ProceduralStream.of(
|
||||
(x, z) -> focusRegion,
|
||||
Interpolated.of(value -> 0D, value -> focusRegion)
|
||||
);
|
||||
ProceduralStream<IrisRegion> regionStream = focusRegion == null
|
||||
? proceduralRegionStream
|
||||
.convertAware2D((region, x, z) -> mappedRegion(imageMapRuntime, region, x, z))
|
||||
.cache2D(cachePrefix + "ImageMappedRegionStream", engine, cacheSize)
|
||||
: proceduralRegionStream;
|
||||
Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> preparedBiomeStreams =
|
||||
compileInferredBiomeStreams(dimension, dimensionData, rng, preparedRegions);
|
||||
|
||||
ProceduralStream<IrisBiome> landBiomeStream = regionStream
|
||||
.convert(region -> IrisComplex.preparedInferredBiomeStream(
|
||||
preparedBiomeStreams, region, InferredType.LAND))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
|
||||
.convert(region -> IrisComplex.preparedInferredBiomeStream(
|
||||
preparedBiomeStreams, region, InferredType.SEA))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
|
||||
.convert(region -> IrisComplex.preparedInferredBiomeStream(
|
||||
preparedBiomeStreams, region, InferredType.SHORE))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
|
||||
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
|
||||
ProceduralStream<InferredType> bridgeStream = focusBiome == null
|
||||
? dimension.getContinentalStyle()
|
||||
.create(rng.nextParallelRNG(234234565), dimensionData)
|
||||
.bake().scale(1D / dimension.getContinentZoom()).bake().stream()
|
||||
.convert(value -> value >= dimension.getLandChance()
|
||||
? InferredType.SEA
|
||||
: InferredType.LAND)
|
||||
: ProceduralStream.of(
|
||||
(x, z) -> focusBiome.getInferredType(),
|
||||
Interpolated.of(value -> 0D, value -> focusBiome.getInferredType())
|
||||
);
|
||||
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusBiome == null
|
||||
? bridgeStream
|
||||
.convertAware2D((type, x, z) -> {
|
||||
ProceduralStream<IrisBiome> stream = inferredStreams.get(type);
|
||||
return stream != null
|
||||
? stream.get(x, z)
|
||||
: inferredStreams.get(InferredType.LAND).get(x, z);
|
||||
})
|
||||
.convertAware2D((biome, x, z) -> implode(
|
||||
biome, x, z, rng, dataProvider, childSelectionPlans, 3))
|
||||
: ProceduralStream.of(
|
||||
(x, z) -> focusBiome,
|
||||
Interpolated.of(value -> 0D, value -> focusBiome)
|
||||
);
|
||||
ProceduralStream<IrisBiome> baseBiomeStream = focusBiome == null
|
||||
? proceduralBaseBiomeStream
|
||||
.convertAware2D((biome, x, z) -> mappedBiome(imageMapRuntime, biome, x, z))
|
||||
.cache2D(cachePrefix + "ImageMappedBaseBiomeStream", engine, cacheSize)
|
||||
: proceduralBaseBiomeStream;
|
||||
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise = dimension.getOverlayNoise();
|
||||
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
|
||||
? ProceduralStream.ofDouble((x, z) -> 0D)
|
||||
: ProceduralStream.ofDouble((x, z) -> {
|
||||
double value = 0D;
|
||||
for (IrisShapedGeneratorStyle style : overlayNoise) {
|
||||
value += style.get(rng, dimensionData, x, z);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset;
|
||||
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome biome = baseBiomeStream.get(x, z);
|
||||
if (biome == null) {
|
||||
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
|
||||
}
|
||||
double interpolatedHeight = 0D;
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IrisInterpolator interpolator = group.interpolator();
|
||||
IrisGenerator[] groupGenerators = group.generators();
|
||||
if (groupGenerators.length == 0) {
|
||||
continue;
|
||||
}
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
|
||||
NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (sampleX, sampleZ) -> {
|
||||
try {
|
||||
IrisBiome sampledBiome = baseBiomeStream.get(sampleX, sampleZ);
|
||||
if (sampledBiome == null) {
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
NoiseBounds bounds = cachedBounds == null ? null : cachedBounds.get(sampledBiome);
|
||||
if (bounds != null) {
|
||||
return bounds;
|
||||
}
|
||||
double minimum = 0D;
|
||||
double maximum = 0D;
|
||||
for (IrisGenerator generator : groupGenerators) {
|
||||
String key = generator.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
maximum += sampledBiome.getGenLinkMax(key, engine);
|
||||
minimum += sampledBiome.getGenLinkMin(key, engine);
|
||||
}
|
||||
return new NoiseBounds(minimum, maximum);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
});
|
||||
interpolatedHeight += IrisComplex.averageGeneratorHeights(
|
||||
groupGenerators,
|
||||
sampledBounds.min(),
|
||||
sampledBounds.max(),
|
||||
x,
|
||||
z,
|
||||
heightSeed + 239945
|
||||
);
|
||||
}
|
||||
double proceduralHeight = Math.max(
|
||||
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), localHeight),
|
||||
0D
|
||||
);
|
||||
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
|
||||
}, Interpolated.DOUBLE).cache2DDouble(
|
||||
cachePrefix + "ImageMappedHeightStream", engine, cacheSize);
|
||||
ProceduralStream<Double> slopeStream = heightStream.slope(3)
|
||||
.cache2DDouble(cachePrefix + "SlopeStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> finalBiomeStream = focusBiome == null
|
||||
? heightStream.convertAware2D((height, x, z) -> {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
|
||||
if (mappedBiome != null) {
|
||||
return mappedBiome;
|
||||
}
|
||||
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||
IrisBiome resolvedBiome = IrisComplex.resolveSurfaceBiome(
|
||||
height,
|
||||
baseBiome,
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight,
|
||||
landBiomeStream,
|
||||
seaBiomeStream,
|
||||
shoreBiomeStream
|
||||
);
|
||||
return resolvedBiome == baseBiome
|
||||
? baseBiome
|
||||
: implode(resolvedBiome, x, z, rng, dataProvider, childSelectionPlans, 3);
|
||||
}).cache2D(cachePrefix + "ImageMappedFinalBiomeStream", engine, cacheSize)
|
||||
: ProceduralStream.of(
|
||||
(x, z) -> focusBiome,
|
||||
Interpolated.of(value -> 0D, value -> focusBiome)
|
||||
).cache2D(cachePrefix + "FinalBiomeStreamFocus", engine, cacheSize);
|
||||
|
||||
ProceduralStream<PlatformBlockState> rockStream = dimension.getRockPalette()
|
||||
.getLayerGenerator(rng.nextParallelRNG(45), dimensionData).stream()
|
||||
.select(dimension.getRockPalette().getBlockData(dimensionData));
|
||||
FluidBlockSampler fluidBlockSampler;
|
||||
if (includeFluid) {
|
||||
ProceduralStream<PlatformBlockState> fluidStream = dimension.getFluidPalette()
|
||||
.getLayerGenerator(rng.nextParallelRNG(78), dimensionData).stream()
|
||||
.select(dimension.getFluidPalette().getBlockData(dimensionData));
|
||||
fluidBlockSampler = fluidStream::get;
|
||||
} else {
|
||||
fluidBlockSampler = (x, z) -> null;
|
||||
}
|
||||
|
||||
return new DimensionTerrainContext(new ContextState(
|
||||
engine,
|
||||
dimension,
|
||||
dimensionData,
|
||||
localHeight,
|
||||
heightStream,
|
||||
slopeStream,
|
||||
ProceduralStream.ofDouble((x, z) -> fluidHeight),
|
||||
finalBiomeStream,
|
||||
regionStream,
|
||||
rockStream,
|
||||
fluidBlockSampler,
|
||||
imageMapRuntime,
|
||||
false,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
private static void registerBiomeGenerators(
|
||||
IrisBiome biome,
|
||||
DataProvider dataProvider,
|
||||
Set<IrisBiome> allBiomes,
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators
|
||||
) {
|
||||
allBiomes.add(biome);
|
||||
biome.getGenerators().forEach(link -> {
|
||||
IrisGenerator generator = link.getCachedGenerator(dataProvider);
|
||||
if (generator != null) {
|
||||
generators.computeIfAbsent(generator.getInterpolator(), key -> new HashSet<>()).add(generator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static IrisRegion mappedRegion(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
IrisRegion proceduralRegion,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ);
|
||||
return mappedRegion == null ? proceduralRegion : mappedRegion;
|
||||
}
|
||||
|
||||
static double mappedTerrainHeight(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
double proceduralHeight,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight);
|
||||
}
|
||||
|
||||
static IrisBiome mappedBiome(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
IrisBiome proceduralBiome,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ);
|
||||
return mappedBiome == null ? proceduralBiome : mappedBiome;
|
||||
}
|
||||
|
||||
static PlatformBlockState mappedSurfaceBlock(
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
PlatformBlockState proceduralBlock,
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ);
|
||||
return mappedBlock == null ? proceduralBlock : mappedBlock;
|
||||
}
|
||||
|
||||
private static Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compileInferredBiomeStreams(
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
RNG rng,
|
||||
Iterable<IrisRegion> regions
|
||||
) {
|
||||
IdentityHashMap<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> compiled =
|
||||
new IdentityHashMap<>();
|
||||
for (IrisRegion region : regions) {
|
||||
if (compiled.containsKey(region)) {
|
||||
continue;
|
||||
}
|
||||
EnumMap<InferredType, ProceduralStream<IrisBiome>> streams = new EnumMap<>(InferredType.class);
|
||||
streams.put(InferredType.LAND, createInferredBiomeStream(
|
||||
dimension, data, rng, region, InferredType.LAND));
|
||||
streams.put(InferredType.SEA, createInferredBiomeStream(
|
||||
dimension, data, rng, region, InferredType.SEA));
|
||||
streams.put(InferredType.SHORE, createInferredBiomeStream(
|
||||
dimension, data, rng, region, InferredType.SHORE));
|
||||
compiled.put(region, Collections.unmodifiableMap(streams));
|
||||
}
|
||||
return Collections.unmodifiableMap(compiled);
|
||||
}
|
||||
|
||||
private static ProceduralStream<IrisBiome> createInferredBiomeStream(
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
RNG rng,
|
||||
IrisRegion region,
|
||||
InferredType type
|
||||
) {
|
||||
return switch (type) {
|
||||
case LAND -> dimension.getLandBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), data).stream()
|
||||
.zoom(dimension.getBiomeZoom())
|
||||
.zoom(dimension.getLandZoom())
|
||||
.zoom(region.getLandBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(data, region.getLandBiomes(), InferredType.LAND));
|
||||
case SEA -> dimension.getSeaBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), data).stream()
|
||||
.zoom(dimension.getBiomeZoom())
|
||||
.zoom(dimension.getSeaZoom())
|
||||
.zoom(region.getSeaBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(data, region.getSeaBiomes(), InferredType.SEA));
|
||||
case SHORE -> dimension.getShoreBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), data).stream()
|
||||
.zoom(dimension.getBiomeZoom())
|
||||
.zoom(region.getShoreBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(data, region.getShoreBiomes(), InferredType.SHORE));
|
||||
default -> throw new IllegalArgumentException("Unsupported stacked terrain biome type: " + type);
|
||||
};
|
||||
}
|
||||
|
||||
private static KList<IrisBiome> loadInferredBiomes(
|
||||
IrisData data,
|
||||
KList<String> keys,
|
||||
InferredType type
|
||||
) {
|
||||
KList<IrisBiome> inferredBiomes = new KList<>();
|
||||
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
|
||||
if (biome.isCompatExcluded()) {
|
||||
continue;
|
||||
}
|
||||
inferredBiomes.add(biome.withInferredType(type));
|
||||
}
|
||||
return inferredBiomes;
|
||||
}
|
||||
|
||||
private static IrisBiome loadFocusBiome(IrisDimension dimension, IrisData data) {
|
||||
String key = dimension.getFocus();
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
IrisBiome biome = data.getBiomeLoader().load(key);
|
||||
return biome == null || biome.isCompatExcluded() ? null : biome;
|
||||
}
|
||||
|
||||
private static IrisRegion loadFocusRegion(IrisDimension dimension, IrisData data) {
|
||||
String key = dimension.getFocusRegion();
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
IrisRegion region = data.getRegionLoader().load(key);
|
||||
return region == null || region.isCompatExcluded() ? null : region;
|
||||
}
|
||||
|
||||
private static IrisRegion findFocusRegion(
|
||||
IrisBiome focusBiome,
|
||||
IrisDimension dimension,
|
||||
DataProvider dataProvider,
|
||||
IrisData data
|
||||
) {
|
||||
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
|
||||
if (region.getAllBiomeIds().contains(focusBiome.getLoadKey())) {
|
||||
return region;
|
||||
}
|
||||
}
|
||||
String key = UUID.nameUUIDFromBytes((dimension.getLoadKey() + "\u0000" + focusBiome.getLoadKey())
|
||||
.getBytes(StandardCharsets.UTF_8)).toString();
|
||||
IrisRegion region = new IrisRegion();
|
||||
region.getLandBiomes().add(focusBiome.getLoadKey());
|
||||
region.getSeaBiomes().add(focusBiome.getLoadKey());
|
||||
region.getShoreBiomes().add(focusBiome.getLoadKey());
|
||||
region.setLoadKey(key);
|
||||
region.setLoader(data);
|
||||
region.setLoadFile(new File(
|
||||
data.getDataFolder(),
|
||||
data.getRegionLoader().getFolderName() + "/" + key + ".json"
|
||||
));
|
||||
return region;
|
||||
}
|
||||
|
||||
private static IrisBiome implode(
|
||||
IrisBiome biome,
|
||||
double x,
|
||||
double z,
|
||||
RNG rng,
|
||||
DataProvider dataProvider,
|
||||
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
|
||||
int remainingDepth
|
||||
) {
|
||||
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
|
||||
return biome;
|
||||
}
|
||||
|
||||
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
|
||||
if (selectionPlan == null) {
|
||||
synchronized (childSelectionPlans) {
|
||||
selectionPlan = childSelectionPlans.get(biome);
|
||||
if (selectionPlan == null) {
|
||||
KList<IrisBiome> options = new KList<>();
|
||||
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
|
||||
if (child != null) {
|
||||
options.add(child);
|
||||
}
|
||||
}
|
||||
options.add(biome);
|
||||
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
|
||||
childSelectionPlans.put(biome, selectionPlan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome selected = selectionPlan.select(
|
||||
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
|
||||
if (selected == null) {
|
||||
return biome;
|
||||
}
|
||||
return implode(
|
||||
selected.withInferredType(biome.getInferredType()),
|
||||
x,
|
||||
z,
|
||||
rng,
|
||||
dataProvider,
|
||||
childSelectionPlans,
|
||||
remainingDepth - 1
|
||||
);
|
||||
}
|
||||
|
||||
public double getNormalTerrainHeight(double x, double z) {
|
||||
return getNormalTerrainHeight(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
double getNormalTerrainHeight(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.heightStream().getDouble(x, z);
|
||||
}
|
||||
return heightStream.getDouble(x, z);
|
||||
}
|
||||
|
||||
public double getFluidHeight(double x, double z) {
|
||||
return getFluidHeight(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
double getFluidHeight(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.fluidHeight();
|
||||
}
|
||||
return fluidHeightStream.getDouble(x, z);
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getSlopeStream() {
|
||||
return slopeStream;
|
||||
}
|
||||
|
||||
public IrisBiome getBiome(double x, double z) {
|
||||
return getBiome(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
private IrisBiome getBiome(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.biomeStream().get(x, z);
|
||||
}
|
||||
return biomeStream.get(x, z);
|
||||
}
|
||||
|
||||
public IrisRegion getRegion(double x, double z) {
|
||||
return regionStream == null ? null : regionStream.get(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getRockBlock(double x, double z) {
|
||||
return rockStream.get(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getFluidBlock(double x, double z) {
|
||||
return getFluidBlock(x, z, usesNaturalFallback(x, z));
|
||||
}
|
||||
|
||||
private PlatformBlockState getFluidBlock(double x, double z, boolean naturalFallback) {
|
||||
if (naturalFallback && selfFallback != null) {
|
||||
return selfFallback.fluidBlockSampler().sample(x, z);
|
||||
}
|
||||
return fluidBlockSampler.sample(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getSurfaceBlock(double x, double z) {
|
||||
return imageMapRuntime.sampleSurfaceBlock(x, z);
|
||||
}
|
||||
|
||||
public IrisDimension getDimension() {
|
||||
return dimension;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IrisData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public int getLocalHeight() {
|
||||
return localHeight;
|
||||
}
|
||||
|
||||
public boolean isSelfReferencing() {
|
||||
return selfReferencing;
|
||||
}
|
||||
|
||||
ColumnSample sampleColumn(double x, double z, boolean naturalFallback) {
|
||||
return new ColumnSample(
|
||||
getNormalTerrainHeight(x, z, naturalFallback),
|
||||
getFluidHeight(x, z, naturalFallback),
|
||||
getBiome(x, z, naturalFallback),
|
||||
getRegion(x, z),
|
||||
getRockBlock(x, z),
|
||||
getFluidBlock(x, z, naturalFallback),
|
||||
getSurfaceBlock(x, z)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean usesNaturalFallback(double x, double z) {
|
||||
return selfFallback != null && engine.answersFromNaturalTerrain(
|
||||
(int) Math.floor(x),
|
||||
(int) Math.floor(z)
|
||||
);
|
||||
}
|
||||
|
||||
private interface FluidBlockSampler {
|
||||
PlatformBlockState sample(double x, double z);
|
||||
}
|
||||
|
||||
private record ContextState(
|
||||
Engine engine,
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
int localHeight,
|
||||
ProceduralStream<Double> heightStream,
|
||||
ProceduralStream<Double> slopeStream,
|
||||
ProceduralStream<Double> fluidHeightStream,
|
||||
ProceduralStream<IrisBiome> biomeStream,
|
||||
ProceduralStream<IrisRegion> regionStream,
|
||||
ProceduralStream<PlatformBlockState> rockStream,
|
||||
FluidBlockSampler fluidBlockSampler,
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
boolean selfReferencing,
|
||||
SelfFallback selfFallback
|
||||
) {
|
||||
}
|
||||
|
||||
private record SelfFallback(
|
||||
ProceduralStream<Double> heightStream,
|
||||
ProceduralStream<IrisBiome> biomeStream,
|
||||
double fluidHeight,
|
||||
FluidBlockSampler fluidBlockSampler
|
||||
) {
|
||||
}
|
||||
|
||||
record ColumnSample(
|
||||
double terrainHeight,
|
||||
double fluidHeight,
|
||||
IrisBiome biome,
|
||||
IrisRegion region,
|
||||
PlatformBlockState rockBlock,
|
||||
PlatformBlockState fluidBlock,
|
||||
PlatformBlockState surfaceBlock
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -59,15 +59,22 @@ final class EngineHotloader {
|
||||
IrisComplex nextComplex = null;
|
||||
try {
|
||||
engine.sealForTransition("complex hotload", false);
|
||||
engine.prepareRuntimeHotload();
|
||||
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
|
||||
engine.runtimeAssembly.set(assembly);
|
||||
EngineRuntime next;
|
||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||
assembly.complex = new IrisComplex(engine);
|
||||
nextComplex = assembly.complex;
|
||||
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext();
|
||||
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
|
||||
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
|
||||
next = previous.withComplex(assembly.cacheId, assembly.complex, assembly.upperContext, biomeMaxes);
|
||||
next = previous.withComplex(
|
||||
assembly.cacheId,
|
||||
assembly.complex,
|
||||
assembly.upperContext,
|
||||
assembly.dimensionStackContext,
|
||||
biomeMaxes);
|
||||
} finally {
|
||||
engine.runtimeAssembly.remove();
|
||||
}
|
||||
@@ -110,6 +117,7 @@ final class EngineHotloader {
|
||||
boolean published = false;
|
||||
try {
|
||||
engine.sealForTransition("hotload", false);
|
||||
engine.prepareRuntimeHotload();
|
||||
replacementData = IrisData.openRuntime(previousData.getDataFolder());
|
||||
IrisDimension replacement = replacementData.getDimensionLoader().load(previousDimension.getLoadKey());
|
||||
if (replacement == null) {
|
||||
|
||||
@@ -34,6 +34,7 @@ record EngineRuntime(
|
||||
EngineTarget target,
|
||||
IrisComplex complex,
|
||||
UpperDimensionContext upperContext,
|
||||
DimensionStackContext dimensionStackContext,
|
||||
EngineEffects effects,
|
||||
EngineMode mode,
|
||||
EngineWorldManager worldManager,
|
||||
@@ -44,6 +45,7 @@ record EngineRuntime(
|
||||
int nextCacheId,
|
||||
IrisComplex nextComplex,
|
||||
UpperDimensionContext nextUpperContext,
|
||||
DimensionStackContext nextDimensionStackContext,
|
||||
BiomeMaxes nextBiomeMaxes
|
||||
) {
|
||||
return new EngineRuntime(
|
||||
@@ -51,6 +53,7 @@ record EngineRuntime(
|
||||
target,
|
||||
nextComplex,
|
||||
nextUpperContext,
|
||||
nextDimensionStackContext,
|
||||
effects,
|
||||
mode,
|
||||
worldManager,
|
||||
|
||||
@@ -22,12 +22,14 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
|
||||
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
||||
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
|
||||
import art.arcane.iris.engine.framework.EngineEffects;
|
||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||
import art.arcane.iris.engine.modifier.IrisCustomModifier;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||
import art.arcane.iris.engine.object.IrisDecorator;
|
||||
@@ -74,6 +76,9 @@ final class EngineRuntimeBuilder {
|
||||
assembly.complex = new IrisComplex(engine);
|
||||
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
assembly.dimensionStackContext = buildDimensionStackContext();
|
||||
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
assembly.upperContext = buildUpperContext();
|
||||
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
@@ -91,6 +96,14 @@ final class EngineRuntimeBuilder {
|
||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||
staticObjects.apply(engine, x, z, blocks));
|
||||
if (assembly.dimensionStackContext != null) {
|
||||
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
|
||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||
dimensionStack.actuate(x, z, blocks, multicore, context));
|
||||
IrisCustomModifier stackCustom = new IrisCustomModifier(engine);
|
||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
||||
stackCustom.modify(x, z, blocks, multicore, context));
|
||||
}
|
||||
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
|
||||
started = M.ms();
|
||||
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
|
||||
@@ -205,6 +218,21 @@ final class EngineRuntimeBuilder {
|
||||
return null;
|
||||
}
|
||||
|
||||
DimensionStackContext buildDimensionStackContext() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
if (!dimension.hasDimensionStack()) {
|
||||
return null;
|
||||
}
|
||||
if (dimension.hasUpperDimension()) {
|
||||
throw new IllegalStateException("Dimension '" + dimension.getLoadKey()
|
||||
+ "' cannot enable both dimensionStack and upperDimension.");
|
||||
}
|
||||
DimensionStackContext context = DimensionStackContext.create(engine, dimension.getDimensionStack());
|
||||
IrisLogging.info("Dimension stack enabled: "
|
||||
+ String.join(" -> ", dimension.getDimensionStack().getDimensions()));
|
||||
return context;
|
||||
}
|
||||
|
||||
private EngineMode createMode() {
|
||||
Throwable configuredFailure = null;
|
||||
try {
|
||||
@@ -300,6 +328,7 @@ final class EngineRuntimeBuilder {
|
||||
}
|
||||
IrisComplex complex;
|
||||
UpperDimensionContext upperContext;
|
||||
DimensionStackContext dimensionStackContext;
|
||||
EngineEffects effects;
|
||||
EngineMode mode;
|
||||
EngineWorldManager worldManager;
|
||||
@@ -319,6 +348,7 @@ final class EngineRuntimeBuilder {
|
||||
target,
|
||||
complex,
|
||||
upperContext,
|
||||
dimensionStackContext,
|
||||
effects,
|
||||
mode,
|
||||
worldManager,
|
||||
|
||||
@@ -501,10 +501,14 @@ public class IrisEngine implements Engine {
|
||||
}
|
||||
|
||||
public void hotloadSilently() {
|
||||
nativeStructureVolumeMemo.clear();
|
||||
hotloader.hotloadSilently();
|
||||
}
|
||||
|
||||
void prepareRuntimeHotload() {
|
||||
nativeStructureVolumeMemo.clear();
|
||||
platformHooks.prepareRuntimeHotload(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<NativeStructureVolume> getNativeStructureVolumes(int minX, int minZ, int maxX, int maxZ) {
|
||||
if (!nativeStructureVolumeQueriesEnabled.get()) {
|
||||
@@ -632,6 +636,16 @@ public class IrisEngine implements Engine {
|
||||
return current == null ? null : current.upperContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DimensionStackContext getDimensionStackContext() {
|
||||
RuntimeAssembly assembly = runtimeAssembly.get();
|
||||
if (assembly != null) {
|
||||
return assembly.dimensionStackContext;
|
||||
}
|
||||
EngineRuntime current = runtime;
|
||||
return current == null ? null : current.dimensionStackContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Long> getHash32() {
|
||||
RuntimeAssembly assembly = runtimeAssembly.get();
|
||||
|
||||
@@ -1,311 +1,46 @@
|
||||
/*
|
||||
* 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.engine;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.engine.object.InferredType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.project.interpolation.NoiseBounds;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import art.arcane.iris.util.common.data.DataProvider;
|
||||
|
||||
public class UpperDimensionContext implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private final IrisDimension dimension;
|
||||
private final IrisData data;
|
||||
private final DimensionTerrainContext terrainContext;
|
||||
private final int chunkHeight;
|
||||
private final ProceduralStream<Double> heightStream;
|
||||
private final ProceduralStream<IrisBiome> biomeStream;
|
||||
private final ProceduralStream<IrisRegion> regionStream;
|
||||
private final ProceduralStream<PlatformBlockState> rockStream;
|
||||
private final IrisImageMapRuntime imageMapRuntime;
|
||||
private final boolean selfReferencing;
|
||||
|
||||
private UpperDimensionContext(ContextState state) {
|
||||
this.dimension = state.dimension();
|
||||
this.data = state.data();
|
||||
this.chunkHeight = state.chunkHeight();
|
||||
this.heightStream = state.heightStream();
|
||||
this.biomeStream = state.biomeStream();
|
||||
this.regionStream = state.regionStream();
|
||||
this.rockStream = state.rockStream();
|
||||
this.imageMapRuntime = state.imageMapRuntime();
|
||||
this.selfReferencing = state.selfReferencing();
|
||||
private UpperDimensionContext(DimensionTerrainContext terrainContext, int chunkHeight) {
|
||||
this.terrainContext = terrainContext;
|
||||
this.chunkHeight = chunkHeight;
|
||||
}
|
||||
|
||||
public static UpperDimensionContext create(Engine engine, IrisDimension upperDim) {
|
||||
boolean selfRef = upperDim.getLoadKey().equals(engine.getDimension().getLoadKey());
|
||||
int chunkHeight = engine.getHeight();
|
||||
if (selfRef) {
|
||||
return createSelfReferencing(engine, chunkHeight);
|
||||
}
|
||||
return createCrossReferencing(engine, upperDim, chunkHeight);
|
||||
}
|
||||
|
||||
private static UpperDimensionContext createSelfReferencing(Engine engine, int chunkHeight) {
|
||||
IrisComplex complex = engine.getComplex();
|
||||
return new UpperDimensionContext(new ContextState(
|
||||
engine.getDimension(),
|
||||
engine.getData(),
|
||||
chunkHeight,
|
||||
complex.getNaturalHeightStream(),
|
||||
complex.getNaturalTrueBiomeStream(),
|
||||
complex.getRegionStream(),
|
||||
complex.getRockStream(),
|
||||
complex.getImageMapRuntime(),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
private static UpperDimensionContext createCrossReferencing(Engine engine, IrisDimension upperDim, int chunkHeight) {
|
||||
IrisData resolvedData = upperDim.getLoader();
|
||||
if (resolvedData == null) {
|
||||
resolvedData = engine.getData();
|
||||
}
|
||||
IrisData upperData = resolvedData;
|
||||
IrisImageMapRuntime imageMapRuntime = IrisImageMapRuntime.compile(
|
||||
upperData,
|
||||
upperDim,
|
||||
engine.getMinHeight()
|
||||
public static UpperDimensionContext create(Engine engine, IrisDimension upperDimension) {
|
||||
return new UpperDimensionContext(
|
||||
DimensionTerrainContext.forUpper(engine, upperDimension),
|
||||
engine.getHeight()
|
||||
);
|
||||
long seedOffset = upperDim.getLoadKey().hashCode();
|
||||
RNG rng = new RNG(engine.getSeedManager().getComplex() ^ seedOffset);
|
||||
double fluidHeight = upperDim.getFluidHeight();
|
||||
int cacheSize = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
||||
DataProvider dataProvider = () -> upperData;
|
||||
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators = new HashMap<>();
|
||||
Set<IrisBiome> allBiomes = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans =
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
upperDim.getRegions().forEach(regionKey -> {
|
||||
IrisRegion region = upperData.getRegionLoader().load(regionKey);
|
||||
if (region != null) {
|
||||
region.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
});
|
||||
for (IrisRegion mappedRegion : imageMapRuntime.getMappedRegions()) {
|
||||
mappedRegion.getNaturalBiomes(dataProvider).forEach(biome -> registerBiomeGenerators(
|
||||
biome, dataProvider, allBiomes, generators));
|
||||
}
|
||||
for (IrisBiome mappedBiome : imageMapRuntime.getMappedBiomes()) {
|
||||
registerBiomeGenerators(mappedBiome, dataProvider, allBiomes, generators);
|
||||
}
|
||||
|
||||
IrisComplex.GeneratorGroup[] generatorGroups = IrisComplex.freezeGeneratorGroups(generators);
|
||||
Map<IrisInterpolator, IdentityHashMap<IrisBiome, NoiseBounds>> generatorBounds = new HashMap<>();
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16));
|
||||
for (IrisBiome biome : allBiomes) {
|
||||
double min = 0D;
|
||||
double max = 0D;
|
||||
for (IrisGenerator gen : group.generators()) {
|
||||
String key = gen.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
max += biome.getGenLinkMax(key, engine);
|
||||
min += biome.getGenLinkMin(key, engine);
|
||||
}
|
||||
interpolatorBounds.put(biome, new NoiseBounds(min, max));
|
||||
}
|
||||
generatorBounds.put(group.interpolator(), interpolatorBounds);
|
||||
}
|
||||
|
||||
ProceduralStream<Double> regionStyleStream = upperDim.getRegionStyle()
|
||||
.create(rng.nextParallelRNG(883), upperData).stream()
|
||||
.zoom(upperDim.getRegionZoom());
|
||||
ProceduralStream<IrisRegion> proceduralRegionStream = regionStyleStream
|
||||
.selectRarity(upperData.getRegionLoader().loadAll(upperDim.getRegions()));
|
||||
ProceduralStream<IrisRegion> regionStream = proceduralRegionStream
|
||||
.convertAware2D((region, x, z) -> mappedRegion(imageMapRuntime, region, x, z))
|
||||
.cache2D("upperImageMappedRegionStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> landBiomeStream = regionStream
|
||||
.convert(r -> upperDim.getLandBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.LAND.ordinal()), upperData).stream()
|
||||
.zoom(upperDim.getBiomeZoom())
|
||||
.zoom(upperDim.getLandZoom())
|
||||
.zoom(r.getLandBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(upperData, r.getLandBiomes(), InferredType.LAND)))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
ProceduralStream<IrisBiome> seaBiomeStream = regionStream
|
||||
.convert(r -> upperDim.getSeaBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SEA.ordinal()), upperData).stream()
|
||||
.zoom(upperDim.getBiomeZoom())
|
||||
.zoom(upperDim.getSeaZoom())
|
||||
.zoom(r.getSeaBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(upperData, r.getSeaBiomes(), InferredType.SEA)))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
ProceduralStream<IrisBiome> shoreBiomeStream = regionStream
|
||||
.convert(r -> upperDim.getShoreBiomeStyle()
|
||||
.create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), upperData).stream()
|
||||
.zoom(upperDim.getBiomeZoom())
|
||||
.zoom(r.getShoreBiomeZoom())
|
||||
.selectRarity(loadInferredBiomes(upperData, r.getShoreBiomes(), InferredType.SHORE)))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
|
||||
Map<InferredType, ProceduralStream<IrisBiome>> inferredStreams = new HashMap<>();
|
||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
|
||||
ProceduralStream<InferredType> bridgeStream = upperDim.getContinentalStyle()
|
||||
.create(rng.nextParallelRNG(234234565), upperData)
|
||||
.bake().scale(1D / upperDim.getContinentZoom()).bake().stream()
|
||||
.convert(v -> v >= upperDim.getLandChance() ? InferredType.SEA : InferredType.LAND);
|
||||
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = bridgeStream
|
||||
.convertAware2D((t, x, z) -> {
|
||||
ProceduralStream<IrisBiome> stream = inferredStreams.get(t);
|
||||
return stream != null ? stream.get(x, z) : inferredStreams.get(InferredType.LAND).get(x, z);
|
||||
})
|
||||
.convertAware2D((biome, x, z) -> implode(
|
||||
biome, x, z, rng, dataProvider, childSelectionPlans, 3));
|
||||
ProceduralStream<IrisBiome> baseBiomeStream = proceduralBaseBiomeStream
|
||||
.convertAware2D((biome, x, z) -> mappedBiome(imageMapRuntime, biome, x, z))
|
||||
.cache2D("upperImageMappedBaseBiomeStream", engine, cacheSize);
|
||||
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise = upperDim.getOverlayNoise();
|
||||
ProceduralStream<Double> overlayStream = overlayNoise.isEmpty()
|
||||
? ProceduralStream.ofDouble((x, z) -> 0.0D)
|
||||
: ProceduralStream.ofDouble((x, z) -> {
|
||||
double value = 0D;
|
||||
for (IrisShapedGeneratorStyle style : overlayNoise) {
|
||||
value += style.get(rng, upperData, x, z);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
long heightSeed = engine.getSeedManager().getHeight() ^ seedOffset;
|
||||
|
||||
ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) -> {
|
||||
IrisBiome b = baseBiomeStream.get(x, z);
|
||||
if (b == null) {
|
||||
return mappedTerrainHeight(imageMapRuntime, fluidHeight, x, z);
|
||||
}
|
||||
double interpolatedHeight = 0;
|
||||
for (IrisComplex.GeneratorGroup group : generatorGroups) {
|
||||
IrisInterpolator interpolator = group.interpolator();
|
||||
IrisGenerator[] groupGenerators = group.generators();
|
||||
if (groupGenerators.length == 0) {
|
||||
continue;
|
||||
}
|
||||
IdentityHashMap<IrisBiome, NoiseBounds> cachedBounds = generatorBounds.get(interpolator);
|
||||
NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (xx, zz) -> {
|
||||
try {
|
||||
IrisBiome bx = baseBiomeStream.get(xx, zz);
|
||||
if (bx == null) {
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
NoiseBounds bounds = cachedBounds != null ? cachedBounds.get(bx) : null;
|
||||
if (bounds != null) {
|
||||
return bounds;
|
||||
}
|
||||
double bMin = 0D;
|
||||
double bMax = 0D;
|
||||
for (IrisGenerator gen : groupGenerators) {
|
||||
String key = gen.getLoadKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
bMax += bx.getGenLinkMax(key, engine);
|
||||
bMin += bx.getGenLinkMin(key, engine);
|
||||
}
|
||||
return new NoiseBounds(bMin, bMax);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
});
|
||||
double hi = sampledBounds.max();
|
||||
double lo = sampledBounds.min();
|
||||
interpolatedHeight += IrisComplex.averageGeneratorHeights(
|
||||
groupGenerators,
|
||||
lo,
|
||||
hi,
|
||||
x,
|
||||
z,
|
||||
heightSeed + 239945
|
||||
);
|
||||
}
|
||||
double proceduralHeight = Math.max(
|
||||
Math.min(interpolatedHeight + fluidHeight + overlayStream.get(x, z), chunkHeight),
|
||||
0D
|
||||
);
|
||||
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
|
||||
}, Interpolated.DOUBLE).cache2DDouble("upperImageMappedHeightStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<IrisBiome> finalBiomeStream = heightStream.convertAware2D((height, x, z) -> {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
|
||||
if (mappedBiome != null) {
|
||||
return mappedBiome;
|
||||
}
|
||||
IrisBiome baseBiome = baseBiomeStream.get(x, z);
|
||||
IrisBiome resolved = IrisComplex.resolveSurfaceBiome(
|
||||
height,
|
||||
baseBiome,
|
||||
regionStream.get(x, z),
|
||||
x,
|
||||
z,
|
||||
fluidHeight,
|
||||
landBiomeStream,
|
||||
seaBiomeStream,
|
||||
shoreBiomeStream);
|
||||
return resolved == baseBiome
|
||||
? baseBiome
|
||||
: implode(resolved, x, z, rng, dataProvider, childSelectionPlans, 3);
|
||||
}).cache2D("upperImageMappedFinalBiomeStream", engine, cacheSize);
|
||||
|
||||
ProceduralStream<PlatformBlockState> rockStream = upperDim.getRockPalette()
|
||||
.getLayerGenerator(rng.nextParallelRNG(45), upperData).stream()
|
||||
.select(upperDim.getRockPalette().getBlockData(upperData));
|
||||
|
||||
return new UpperDimensionContext(new ContextState(
|
||||
upperDim,
|
||||
upperData,
|
||||
chunkHeight,
|
||||
heightStream,
|
||||
finalBiomeStream,
|
||||
regionStream,
|
||||
rockStream,
|
||||
imageMapRuntime,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
private static void registerBiomeGenerators(
|
||||
IrisBiome biome,
|
||||
DataProvider dataProvider,
|
||||
Set<IrisBiome> allBiomes,
|
||||
Map<IrisInterpolator, Set<IrisGenerator>> generators
|
||||
) {
|
||||
allBiomes.add(biome);
|
||||
biome.getGenerators().forEach(link -> {
|
||||
IrisGenerator generator = link.getCachedGenerator(dataProvider);
|
||||
if (generator != null) {
|
||||
generators.computeIfAbsent(generator.getInterpolator(), key -> new HashSet<>()).add(generator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static IrisRegion mappedRegion(
|
||||
@@ -314,8 +49,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisRegion mappedRegion = imageMapRuntime.sampleRegion(worldX, worldZ);
|
||||
return mappedRegion == null ? proceduralRegion : mappedRegion;
|
||||
return DimensionTerrainContext.mappedRegion(
|
||||
imageMapRuntime, proceduralRegion, worldX, worldZ);
|
||||
}
|
||||
|
||||
static double mappedTerrainHeight(
|
||||
@@ -324,7 +59,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
return imageMapRuntime.sampleTerrainHeight(worldX, worldZ, proceduralHeight);
|
||||
return DimensionTerrainContext.mappedTerrainHeight(
|
||||
imageMapRuntime, proceduralHeight, worldX, worldZ);
|
||||
}
|
||||
|
||||
static IrisBiome mappedBiome(
|
||||
@@ -333,8 +69,8 @@ public class UpperDimensionContext implements DataProvider {
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(worldX, worldZ);
|
||||
return mappedBiome == null ? proceduralBiome : mappedBiome;
|
||||
return DimensionTerrainContext.mappedBiome(
|
||||
imageMapRuntime, proceduralBiome, worldX, worldZ);
|
||||
}
|
||||
|
||||
static PlatformBlockState mappedSurfaceBlock(
|
||||
@@ -343,108 +79,40 @@ public class UpperDimensionContext implements DataProvider {
|
||||
double worldX,
|
||||
double worldZ
|
||||
) {
|
||||
PlatformBlockState mappedBlock = imageMapRuntime.sampleSurfaceBlock(worldX, worldZ);
|
||||
return mappedBlock == null ? proceduralBlock : mappedBlock;
|
||||
}
|
||||
|
||||
private static KList<IrisBiome> loadInferredBiomes(IrisData data, KList<String> keys, InferredType type) {
|
||||
KList<IrisBiome> inferred = new KList<>();
|
||||
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
|
||||
inferred.add(biome.withInferredType(type));
|
||||
}
|
||||
return inferred;
|
||||
}
|
||||
|
||||
private static IrisBiome implode(
|
||||
IrisBiome biome,
|
||||
double x,
|
||||
double z,
|
||||
RNG rng,
|
||||
DataProvider dataProvider,
|
||||
Map<IrisBiome, IrisComplex.ChildSelectionPlan> childSelectionPlans,
|
||||
int remainingDepth
|
||||
) {
|
||||
if (biome == null || remainingDepth < 0 || biome.getChildren().isEmpty()) {
|
||||
return biome;
|
||||
}
|
||||
|
||||
IrisComplex.ChildSelectionPlan selectionPlan = childSelectionPlans.get(biome);
|
||||
if (selectionPlan == null) {
|
||||
synchronized (childSelectionPlans) {
|
||||
selectionPlan = childSelectionPlans.get(biome);
|
||||
if (selectionPlan == null) {
|
||||
KList<IrisBiome> options = new KList<>();
|
||||
for (IrisBiome child : biome.getRealChildren(dataProvider)) {
|
||||
if (child != null) {
|
||||
options.add(child);
|
||||
}
|
||||
}
|
||||
options.add(biome);
|
||||
selectionPlan = IrisComplex.ChildSelectionPlan.create(options);
|
||||
childSelectionPlans.put(biome, selectionPlan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome selected = selectionPlan.select(
|
||||
biome.getChildrenGenerator(rng, 123, biome.getChildShrinkFactor()), x, z);
|
||||
if (selected == null) {
|
||||
return biome;
|
||||
}
|
||||
return implode(
|
||||
selected.withInferredType(biome.getInferredType()),
|
||||
x,
|
||||
z,
|
||||
rng,
|
||||
dataProvider,
|
||||
childSelectionPlans,
|
||||
remainingDepth - 1);
|
||||
return DimensionTerrainContext.mappedSurfaceBlock(
|
||||
imageMapRuntime, proceduralBlock, worldX, worldZ);
|
||||
}
|
||||
|
||||
public int getUpperSurfaceY(int x, int z) {
|
||||
double rawHeight = heightStream.get((double) x, (double) z);
|
||||
return chunkHeight - 1 - (int) Math.round(rawHeight);
|
||||
return chunkHeight - 1 - (int) Math.round(terrainContext.getNormalTerrainHeight(x, z));
|
||||
}
|
||||
|
||||
public IrisBiome getUpperBiome(int x, int z) {
|
||||
return biomeStream.get((double) x, (double) z);
|
||||
return terrainContext.getBiome(x, z);
|
||||
}
|
||||
|
||||
public IrisRegion getUpperRegion(int x, int z) {
|
||||
return regionStream == null ? null : regionStream.get((double) x, (double) z);
|
||||
return terrainContext.getRegion(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getRockBlock(int x, int z) {
|
||||
return rockStream.get((double) x, (double) z);
|
||||
return terrainContext.getRockBlock(x, z);
|
||||
}
|
||||
|
||||
public PlatformBlockState getSurfaceBlock(int x, int z) {
|
||||
return imageMapRuntime.sampleSurfaceBlock(x, z);
|
||||
return terrainContext.getSurfaceBlock(x, z);
|
||||
}
|
||||
|
||||
public IrisDimension getDimension() {
|
||||
return dimension;
|
||||
return terrainContext.getDimension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IrisData getData() {
|
||||
return data;
|
||||
return terrainContext.getData();
|
||||
}
|
||||
|
||||
public boolean isSelfReferencing() {
|
||||
return selfReferencing;
|
||||
}
|
||||
|
||||
private record ContextState(
|
||||
IrisDimension dimension,
|
||||
IrisData data,
|
||||
int chunkHeight,
|
||||
ProceduralStream<Double> heightStream,
|
||||
ProceduralStream<IrisBiome> biomeStream,
|
||||
ProceduralStream<IrisRegion> regionStream,
|
||||
ProceduralStream<PlatformBlockState> rockStream,
|
||||
IrisImageMapRuntime imageMapRuntime,
|
||||
boolean selfReferencing
|
||||
) {
|
||||
return terrainContext.isSelfReferencing();
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,13 @@
|
||||
|
||||
package art.arcane.iris.engine.actuator;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.context.ChunkedDataCache;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -38,6 +41,8 @@ import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
||||
private final RNG rng;
|
||||
private final ChronoLatch cl = new ChronoLatch(5000);
|
||||
@@ -60,33 +65,167 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
||||
Engine engine = getEngine();
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
|
||||
|
||||
for (int xf = 0; xf < width; xf++) {
|
||||
IrisBiome ib;
|
||||
for (int zf = 0; zf < depth; zf++) {
|
||||
ib = biomeCache.get(xf, zf);
|
||||
String key;
|
||||
int worldX = x + xf;
|
||||
int worldZ = z + zf;
|
||||
IrisBiome biome = biomeCache.get(xf, zf);
|
||||
ResolvedBiome resolved = resolve(biomeKey(
|
||||
biome,
|
||||
getDimension(),
|
||||
engine,
|
||||
worldX,
|
||||
0,
|
||||
worldZ
|
||||
));
|
||||
PlatformBiome platformBiome = resolved.biome();
|
||||
|
||||
if (ib.isCustom()) {
|
||||
IrisBiomeCustom custom = ib.getCustomBiome(rng, engine, x + xf, 0, z + zf);
|
||||
key = getDimension().getLoadKey() + ":" + custom.getId();
|
||||
} else {
|
||||
key = ib.getSkyBiomeKey(rng, engine, x + xf, 0, z + zf);
|
||||
if (platformBiome != null) {
|
||||
h.set(xf, 0, zf, xf, height - 1, zf, platformBiome);
|
||||
}
|
||||
|
||||
ResolvedBiome resolved = resolve(key);
|
||||
PlatformBiome biome = resolved.biome();
|
||||
|
||||
if (biome != null) {
|
||||
h.set(xf, 0, zf, xf, height - 1, zf, biome);
|
||||
mantle.set(worldX, 0, worldZ, resolved.matter());
|
||||
if (dimensionStackContext != null) {
|
||||
PlatformBiome bottomBiome = platformBiome == null
|
||||
? h.getRaw(xf, 0, zf)
|
||||
: platformBiome;
|
||||
applyDimensionStackBiomes(
|
||||
xf,
|
||||
zf,
|
||||
worldX,
|
||||
worldZ,
|
||||
h,
|
||||
mantle,
|
||||
engine,
|
||||
new ResolvedBiome(bottomBiome, resolved.matter()),
|
||||
context.getDimensionStackLayout(xf, zf)
|
||||
);
|
||||
}
|
||||
|
||||
mantle.set(x + xf, 0, z + zf, resolved.matter());
|
||||
}
|
||||
}
|
||||
engine.getMetrics().getBiome().put(p.getMilliseconds());
|
||||
}
|
||||
|
||||
private void applyDimensionStackBiomes(
|
||||
int localX,
|
||||
int localZ,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
Hunk<PlatformBiome> output,
|
||||
Mantle<Matter> mantle,
|
||||
Engine engine,
|
||||
ResolvedBiome bottom,
|
||||
DimensionStackLayout layout
|
||||
) {
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
for (int layerIndex = 1; layerIndex < layers.size(); layerIndex++) {
|
||||
DimensionStackLayout.Layer lower = layers.get(layerIndex - 1);
|
||||
DimensionStackLayout.Layer layer = layers.get(layerIndex);
|
||||
int gapMinY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
|
||||
int gapMaxY = (int) Math.min(
|
||||
(long) output.getHeight() - 1L,
|
||||
(long) layer.localBaseY() - 1L
|
||||
);
|
||||
applyBiomeRange(
|
||||
localX,
|
||||
localZ,
|
||||
worldX,
|
||||
worldZ,
|
||||
gapMinY,
|
||||
gapMaxY,
|
||||
output,
|
||||
mantle,
|
||||
bottom
|
||||
);
|
||||
if (!layer.visible()) {
|
||||
continue;
|
||||
}
|
||||
ResolvedBiome resolved = bottom;
|
||||
if (layer.biome() != null) {
|
||||
IrisDimension dimension = layer.terrainContext().getDimension();
|
||||
resolved = resolve(biomeKey(
|
||||
layer.biome(),
|
||||
dimension,
|
||||
engine,
|
||||
worldX,
|
||||
layer.clippedSurfaceY(),
|
||||
worldZ
|
||||
));
|
||||
}
|
||||
applyBiomeRange(
|
||||
localX,
|
||||
localZ,
|
||||
worldX,
|
||||
worldZ,
|
||||
layer.renderMinY(),
|
||||
layer.renderMaxY(),
|
||||
output,
|
||||
mantle,
|
||||
resolved
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyBiomeRange(
|
||||
int localX,
|
||||
int localZ,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
int minimumY,
|
||||
int maximumY,
|
||||
Hunk<PlatformBiome> output,
|
||||
Mantle<Matter> mantle,
|
||||
ResolvedBiome resolved
|
||||
) {
|
||||
if (minimumY > maximumY) {
|
||||
return;
|
||||
}
|
||||
clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY);
|
||||
if (resolved.biome() != null) {
|
||||
output.set(
|
||||
localX,
|
||||
minimumY,
|
||||
localZ,
|
||||
localX,
|
||||
maximumY,
|
||||
localZ,
|
||||
resolved.biome()
|
||||
);
|
||||
}
|
||||
if (resolved.matter() != null) {
|
||||
mantle.set(worldX, minimumY, worldZ, resolved.matter());
|
||||
}
|
||||
}
|
||||
|
||||
static void clearBiomeMatterRange(
|
||||
Mantle<Matter> mantle,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
int minimumY,
|
||||
int maximumY
|
||||
) {
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
mantle.remove(worldX, y, worldZ, MatterBiomeInject.class);
|
||||
}
|
||||
}
|
||||
|
||||
private String biomeKey(
|
||||
IrisBiome biome,
|
||||
IrisDimension dimension,
|
||||
Engine engine,
|
||||
int x,
|
||||
int y,
|
||||
int z
|
||||
) {
|
||||
if (biome.isCustom()) {
|
||||
IrisBiomeCustom custom = biome.getCustomBiome(rng, engine, x, y, z);
|
||||
return dimension.getCustomBiomeKey(custom.getId());
|
||||
}
|
||||
return biome.getSkyBiomeKey(rng, engine, x, y, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry lookup and the biome id only depend on the scatter resolved key, so cache the pair
|
||||
* instead of paying two string keyed lookups per column. Unresolved biomes are never cached, so a
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 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.engine.actuator;
|
||||
|
||||
import art.arcane.iris.core.link.Identifier;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedActuator;
|
||||
import art.arcane.iris.engine.framework.TreeBlockMaterial;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterMarker;
|
||||
import art.arcane.volmlib.util.matter.MatterSlice;
|
||||
import art.arcane.volmlib.util.matter.MatterStructurePOI;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class IrisDimensionStackActuator extends EngineAssignedActuator<PlatformBlockState> {
|
||||
private static final PlatformBlockState AIR = B.getState("AIR");
|
||||
private static final PlatformBlockState BEDROCK = B.getState("BEDROCK");
|
||||
private static final List<Class<?>> REPLACED_METADATA = List.of(
|
||||
TileWrapper.class,
|
||||
Identifier.class,
|
||||
String.class,
|
||||
TreeBlockMaterial.class,
|
||||
MatterCavern.class,
|
||||
HydrologyCaveCell.class,
|
||||
MatterMarker.class,
|
||||
MatterStructurePOI.class,
|
||||
MatterUpdate.class
|
||||
);
|
||||
|
||||
private final RNG rng;
|
||||
|
||||
public IrisDimensionStackActuator(Engine engine) {
|
||||
super(engine, "Dimension Stack");
|
||||
rng = new RNG(engine.getSeedManager().getTerrain());
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@Override
|
||||
public void onActuate(
|
||||
int x,
|
||||
int z,
|
||||
Hunk<PlatformBlockState> blocks,
|
||||
boolean multicore,
|
||||
ChunkContext context
|
||||
) {
|
||||
if (getEngine().getDimensionStackContext() == null) {
|
||||
return;
|
||||
}
|
||||
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle()
|
||||
.getChunk(x >> 4, z >> 4).use();
|
||||
try {
|
||||
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
|
||||
for (int localX = 0; localX < blocks.getWidth(); localX++) {
|
||||
int worldX = x + localX;
|
||||
for (int localZ = 0; localZ < blocks.getDepth(); localZ++) {
|
||||
renderLayers(
|
||||
localX,
|
||||
localZ,
|
||||
worldX,
|
||||
z + localZ,
|
||||
blocks,
|
||||
context.getDimensionStackLayout(localX, localZ),
|
||||
metadata
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mantleChunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderLayers(
|
||||
int localX,
|
||||
int localZ,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
Hunk<PlatformBlockState> blocks,
|
||||
DimensionStackLayout layout,
|
||||
MetadataCleaner metadata
|
||||
) {
|
||||
if (layout == null) {
|
||||
return;
|
||||
}
|
||||
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
|
||||
for (int layerIndex = 1; layerIndex < layers.size(); layerIndex++) {
|
||||
DimensionStackLayout.Layer layer = layers.get(layerIndex);
|
||||
clearGap(localX, localZ, blocks, layers.get(layerIndex - 1), layer, metadata);
|
||||
if (layer.visible()) {
|
||||
renderLayer(localX, localZ, worldX, worldZ, blocks, layer, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearGap(
|
||||
int localX,
|
||||
int localZ,
|
||||
Hunk<PlatformBlockState> output,
|
||||
DimensionStackLayout.Layer lower,
|
||||
DimensionStackLayout.Layer upper,
|
||||
MetadataCleaner metadata
|
||||
) {
|
||||
int minimumY = (int) Math.max(0L, (long) lower.contentTopY() + 1L);
|
||||
int maximumY = (int) Math.min(
|
||||
(long) output.getHeight() - 1L,
|
||||
(long) upper.localBaseY() - 1L
|
||||
);
|
||||
for (int y = minimumY; y <= maximumY; y++) {
|
||||
metadata.clear(localX, y, localZ);
|
||||
output.setRaw(localX, y, localZ, AIR);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderLayer(
|
||||
int localX,
|
||||
int localZ,
|
||||
int worldX,
|
||||
int worldZ,
|
||||
Hunk<PlatformBlockState> output,
|
||||
DimensionStackLayout.Layer layer,
|
||||
MetadataCleaner metadata
|
||||
) {
|
||||
DimensionTerrainContext terrainContext = layer.terrainContext();
|
||||
IrisDimension dimension = terrainContext.getDimension();
|
||||
IrisData data = terrainContext.getData();
|
||||
IrisBiome biome = layer.biome();
|
||||
int surfaceDepth = Math.max(0, layer.normalTerrainHeight());
|
||||
int fluidDepth = Math.max(0, layer.fluidHeight() - layer.normalTerrainHeight());
|
||||
KList<PlatformBlockState> surfaceBlocks = biome == null
|
||||
? null
|
||||
: biome.generateLayersWithSlope(
|
||||
dimension,
|
||||
worldX,
|
||||
worldZ,
|
||||
rng,
|
||||
surfaceDepth,
|
||||
layer.normalTerrainHeight(),
|
||||
data,
|
||||
terrainContext.getSlopeStream()
|
||||
);
|
||||
KList<PlatformBlockState> seaBlocks = biome == null || fluidDepth == 0
|
||||
? null
|
||||
: biome.generateSeaLayers(worldX, worldZ, rng, fluidDepth, data);
|
||||
|
||||
for (int y = layer.renderMaxY(); y >= layer.renderMinY(); y--) {
|
||||
int sourceY = y - layer.localBaseY();
|
||||
if (sourceY == 0 && dimension.isBedrock()) {
|
||||
writeBlock(output, metadata, localX, y, localZ, BEDROCK);
|
||||
continue;
|
||||
}
|
||||
if (sourceY > layer.normalTerrainHeight() && sourceY <= layer.fluidHeight()) {
|
||||
int fluidLayerDepth = layer.fluidY() - y;
|
||||
writeBlock(output, metadata, localX, y, localZ, HydrologyFluidLayerSelector.select(
|
||||
seaBlocks,
|
||||
fluidLayerDepth,
|
||||
layer.fluidBlock(),
|
||||
false
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if (sourceY > layer.normalTerrainHeight()) {
|
||||
continue;
|
||||
}
|
||||
int depth = layer.surfaceY() - y;
|
||||
if (depth == 0 && layer.surfaceBlock() != null) {
|
||||
writeBlock(output, metadata, localX, y, localZ, layer.surfaceBlock());
|
||||
} else if (surfaceBlocks != null && surfaceBlocks.hasIndex(depth)) {
|
||||
writeBlock(output, metadata, localX, y, localZ, surfaceBlocks.get(depth));
|
||||
} else {
|
||||
writeBlock(output, metadata, localX, y, localZ, layer.rockBlock());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeBlock(
|
||||
Hunk<PlatformBlockState> output,
|
||||
MetadataCleaner metadata,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
PlatformBlockState block
|
||||
) {
|
||||
metadata.clear(x, y, z);
|
||||
output.setRaw(x, y, z, block);
|
||||
}
|
||||
|
||||
static final class MetadataCleaner {
|
||||
private final MantleChunk<Matter> chunk;
|
||||
private final List<MatterSlice<?>>[] slicesBySection;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
MetadataCleaner(MantleChunk<Matter> chunk, int height) {
|
||||
this.chunk = chunk;
|
||||
slicesBySection = (List<MatterSlice<?>>[]) new List<?>[(height + 15) >> 4];
|
||||
}
|
||||
|
||||
void clear(int x, int y, int z) {
|
||||
int sectionIndex = y >> 4;
|
||||
List<MatterSlice<?>> slices = slicesBySection[sectionIndex];
|
||||
if (slices == null) {
|
||||
slices = resolveSlices(sectionIndex);
|
||||
slicesBySection[sectionIndex] = slices;
|
||||
}
|
||||
for (MatterSlice<?> slice : slices) {
|
||||
slice.set(x, y & 15, z, null);
|
||||
}
|
||||
}
|
||||
|
||||
private List<MatterSlice<?>> resolveSlices(int sectionIndex) {
|
||||
Matter section = chunk.get(sectionIndex);
|
||||
if (section == null) {
|
||||
return List.of();
|
||||
}
|
||||
ArrayList<MatterSlice<?>> slices = new ArrayList<>(REPLACED_METADATA.size());
|
||||
for (Class<?> type : REPLACED_METADATA) {
|
||||
MatterSlice<?> slice = section.getSlice(type);
|
||||
if (slice != null) {
|
||||
slices.add(slice);
|
||||
}
|
||||
}
|
||||
return List.copyOf(slices);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,9 @@ import art.arcane.iris.core.nms.container.BlockPos;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -90,6 +93,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return null;
|
||||
}
|
||||
|
||||
default @Nullable DimensionStackContext getDimensionStackContext() {
|
||||
return null;
|
||||
}
|
||||
|
||||
EngineMode getMode();
|
||||
|
||||
EnginePlatformHooks getPlatformHooks();
|
||||
@@ -255,6 +262,25 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisRegion getRegion(int x, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).surfaceLayer();
|
||||
if (layer != null && layer.region() != null) {
|
||||
return layer.region();
|
||||
}
|
||||
}
|
||||
return getComplex().getRegionStream().get(x, z);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisRegion getRegion(int x, int y, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).layerAt(y);
|
||||
if (layer != null && layer.region() != null) {
|
||||
return layer.region();
|
||||
}
|
||||
}
|
||||
return getComplex().getRegionStream().get(x, z);
|
||||
}
|
||||
|
||||
@@ -301,7 +327,45 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getCaveBiome(int x, int y, int z, IrisDimensionCarvingResolver.State state) {
|
||||
IrisBiome surfaceBiome = getSurfaceBiome(x, z);
|
||||
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
|
||||
if (configuredBiome != null) {
|
||||
return configuredBiome;
|
||||
}
|
||||
boolean naturalFallback = answersFromNaturalTerrain(x, z);
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
IrisBiome surfaceBiome = naturalFallback || dimensionStackContext == null
|
||||
? naturalFallback
|
||||
? getComplex().naturalSurfaceBiome(x, z)
|
||||
: getSurfaceBiome(x, z)
|
||||
: getComplex().getTrueBiomeStream().get(x, z);
|
||||
int surfaceY = naturalFallback
|
||||
? getComplex().naturalTrueHeight(x, z)
|
||||
: getComplex().getHeightStream().get(x, z).intValue();
|
||||
return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getCaveBiome(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State state,
|
||||
IrisBiome surfaceBiome,
|
||||
int surfaceY
|
||||
) {
|
||||
IrisBiome configuredBiome = resolveConfiguredCaveBiome(x, y, z, state);
|
||||
if (configuredBiome != null) {
|
||||
return configuredBiome;
|
||||
}
|
||||
return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY);
|
||||
}
|
||||
|
||||
private IrisBiome resolveConfiguredCaveBiome(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisDimensionCarvingResolver.State state
|
||||
) {
|
||||
int worldY = y + getWorld().minHeight();
|
||||
IrisDimensionCarvingEntry rootCarvingEntry = IrisDimensionCarvingResolver.resolveRootEntry(this, worldY, state);
|
||||
if (rootCarvingEntry != null) {
|
||||
@@ -311,13 +375,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return resolvedCarvingBiome;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private IrisBiome resolveDepthCaveBiome(
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
IrisBiome surfaceBiome,
|
||||
int surfaceY
|
||||
) {
|
||||
IrisBiome caveBiome = getCaveBiome(x, z);
|
||||
if (caveBiome == null) {
|
||||
return surfaceBiome;
|
||||
}
|
||||
|
||||
int surfaceY = getComplex().getHeightStream().get(x, z).intValue();
|
||||
int depthBelowSurface = surfaceY - y;
|
||||
if (depthBelowSurface <= 0) {
|
||||
return surfaceBiome;
|
||||
@@ -333,6 +405,18 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getSurfaceBiome(int x, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout.Layer layer = dimensionStackContext.getLayout(x, z).surfaceLayer();
|
||||
if (layer != null && layer.biome() != null) {
|
||||
return layer.biome();
|
||||
}
|
||||
}
|
||||
return getHostSurfaceBiome(x, z);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getHostSurfaceBiome(int x, int z) {
|
||||
if (answersFromNaturalTerrain(x, z)) {
|
||||
return getComplex().naturalSurfaceBiome(x, z);
|
||||
}
|
||||
@@ -364,6 +448,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default int getHeight(int x, int z, boolean ignoreFluid) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
return ignoreFluid
|
||||
? dimensionStackContext.getStackTerrainHeight(x, z)
|
||||
: dimensionStackContext.getStackTopHeight(x, z);
|
||||
}
|
||||
if (answersFromNaturalTerrain(x, z)) {
|
||||
int natural = getComplex().naturalTrueHeight(x, z);
|
||||
return ignoreFluid ? natural : Math.max(natural, getMantle().getFluidHeight());
|
||||
@@ -371,6 +461,24 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return getMantle().getHighest(x, z, getData(), ignoreFluid);
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
static int hostHeight(Engine engine, int x, int z, boolean ignoreFluid) {
|
||||
EngineMantle mantle = engine.getMantle();
|
||||
if (mantle == null) {
|
||||
return engine.getHeight(x, z, ignoreFluid);
|
||||
}
|
||||
if (engine.answersFromNaturalTerrain(x, z)) {
|
||||
int naturalHeight = engine.getComplex().naturalTrueHeight(x, z);
|
||||
return ignoreFluid
|
||||
? naturalHeight
|
||||
: Math.max(naturalHeight, mantle.getFluidHeight());
|
||||
}
|
||||
int terrainHeight = mantle.trueHeight(x, z);
|
||||
return ignoreFluid
|
||||
? terrainHeight
|
||||
: Math.max(terrainHeight, mantle.getFluidHeight(x, z));
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
@Override
|
||||
default void catchBlockUpdates(int x, int y, int z, PlatformBlockState data) {
|
||||
@@ -410,6 +518,15 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
IrisDimension dim = getDimension();
|
||||
dim.getReachableBiomes(this).forEach((i) -> v.put(i.getLoadKey(), i));
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
for (DimensionTerrainContext terrainContext : dimensionStackContext.getLayersBottomToTop()) {
|
||||
if (!terrainContext.isSelfReferencing()) {
|
||||
terrainContext.getDimension().getReachableBiomes(terrainContext)
|
||||
.forEach((i) -> v.put(i.getLoadKey(), i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return v.v();
|
||||
}
|
||||
@@ -454,6 +571,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
boolean isStudio();
|
||||
|
||||
default IrisBiome getBiome(int x, int y, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
|
||||
IrisBiome stackedBiome = dimensionStackBiome(layout, y);
|
||||
if (stackedBiome != null) {
|
||||
return stackedBiome;
|
||||
}
|
||||
DimensionStackLayout.Layer bottomLayer = layout.layersBottomToTop().get(0);
|
||||
if (y > bottomLayer.renderMaxY()) {
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= bottomLayer.clippedSurfaceY() - 2) {
|
||||
return getCaveBiome(x, y, z);
|
||||
}
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= getHeight(x, z) - 2) {
|
||||
return getCaveBiome(x, y, z);
|
||||
}
|
||||
@@ -462,6 +595,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
}
|
||||
|
||||
default IrisBiome getBiomeOrMantle(int x, int y, int z) {
|
||||
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||
if (dimensionStackContext != null) {
|
||||
DimensionStackLayout layout = dimensionStackContext.getLayout(x, z);
|
||||
IrisBiome stackedBiome = dimensionStackBiome(layout, y);
|
||||
if (stackedBiome != null) {
|
||||
return stackedBiome;
|
||||
}
|
||||
DimensionStackLayout.Layer bottomLayer = layout.layersBottomToTop().get(0);
|
||||
if (y > bottomLayer.renderMaxY()) {
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= bottomLayer.clippedSurfaceY() - 2) {
|
||||
return getCaveOrMantleBiome(x, y, z);
|
||||
}
|
||||
return bottomLayer.biome();
|
||||
}
|
||||
if (y <= getHeight(x, z) - 2) {
|
||||
return getCaveOrMantleBiome(x, y, z);
|
||||
}
|
||||
@@ -469,6 +618,12 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
return getSurfaceBiome(x, z);
|
||||
}
|
||||
|
||||
private static IrisBiome dimensionStackBiome(DimensionStackLayout layout, int y) {
|
||||
DimensionStackLayout.Layer bottom = layout.layersBottomToTop().get(0);
|
||||
DimensionStackLayout.Layer owner = layout.layerAt(y);
|
||||
return owner == bottom ? null : owner.biome();
|
||||
}
|
||||
|
||||
default String getObjectPlacementKey(int x, int y, int z) {
|
||||
PlacedObject o = getObjectPlacement(x, y, z);
|
||||
|
||||
@@ -524,7 +679,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
}
|
||||
|
||||
|
||||
IrisRegion region = getRegion(x, z);
|
||||
IrisRegion region = getComplex().getRegionStream().get(x, z);
|
||||
|
||||
for (IrisObjectPlacement i : region.getObjects()) {
|
||||
if (i.compatPlace(getData()).contains(object)) {
|
||||
@@ -532,7 +687,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
}
|
||||
}
|
||||
|
||||
IrisBiome biome = getSurfaceBiome(x, z);
|
||||
IrisBiome biome = getComplex().getTrueBiomeStream().get(x, z);
|
||||
|
||||
for (IrisObjectPlacement i : biome.getObjects()) {
|
||||
if (i.compatPlace(getData()).contains(object)) {
|
||||
|
||||
@@ -151,7 +151,16 @@ public interface EngineMode extends Staged {
|
||||
default void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, boolean multicore, long generationSessionId) {
|
||||
boolean cacheContext = !getEngine().getPlatformHooks().shouldDisableChunkContextCache(getEngine());
|
||||
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
|
||||
ChunkContext ctx = new ChunkContext(x, z, getComplex(), generationSessionId, cacheContext, prefillPlan, getEngine().getMetrics());
|
||||
ChunkContext ctx = new ChunkContext(
|
||||
x,
|
||||
z,
|
||||
getComplex(),
|
||||
generationSessionId,
|
||||
cacheContext,
|
||||
prefillPlan,
|
||||
getEngine().getMetrics(),
|
||||
getEngine().getDimensionStackContext()
|
||||
);
|
||||
|
||||
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
||||
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
|
||||
|
||||
@@ -46,6 +46,9 @@ public interface EnginePlatformHooks {
|
||||
default void validateDimensionHotload(Engine engine, IrisDimension replacement) {
|
||||
}
|
||||
|
||||
default void prepareRuntimeHotload(Engine engine) {
|
||||
}
|
||||
|
||||
default void applyWorldBoundary(Engine engine) {
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -282,6 +284,13 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
if (engine.getFocus() != null || engine.getFocusRegion() != null) {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
if (engine.getDimensionStackContext() != null) {
|
||||
for (IrisBiome biome : engine.getAllBiomes()) {
|
||||
if (biome != null && biomeKey.equals(biome.getLoadKey())) {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisComplex complex = engine.getComplex();
|
||||
BiomeSource source = new BiomeSource(engine);
|
||||
@@ -380,6 +389,17 @@ public final class HintedLocator<T> implements Locator<T> {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
|
||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||
if (stackContext != null) {
|
||||
for (DimensionTerrainContext terrainContext : stackContext.getLayersTopToBottom()) {
|
||||
for (IrisRegion region : terrainContext.getDimension().getAllRegions(terrainContext)) {
|
||||
if (region != null && regionKey.equals(region.getLoadKey())) {
|
||||
return SearchPlan.unpruned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!engine.getDimension().getRegions().contains(regionKey)) {
|
||||
return SearchPlan.impossible();
|
||||
}
|
||||
|
||||
@@ -655,7 +655,7 @@ public final class IrisStructureLocator {
|
||||
return null;
|
||||
}
|
||||
if (placement.resolvedAnchor() == IrisStructureAnchorMode.SURFACE) {
|
||||
int surfaceY = engine.getHeight(originX, originZ, true) + engine.getMinHeight();
|
||||
int surfaceY = Engine.hostHeight(engine, originX, originZ, true) + engine.getMinHeight();
|
||||
return surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight() ? null : surfaceY;
|
||||
}
|
||||
int bandMin = Math.max(worldMin, Math.min(placement.getMinHeight(), placement.getMaxHeight()));
|
||||
@@ -766,7 +766,7 @@ public final class IrisStructureLocator {
|
||||
if (surfaceHeights.size() >= MAX_BURIAL_COLUMNS) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
surfaceY = engine.getHeight(x, z, true) + engine.getMinHeight();
|
||||
surfaceY = Engine.hostHeight(engine, x, z, true) + engine.getMinHeight();
|
||||
surfaceHeights.put(columnKey, surfaceY);
|
||||
}
|
||||
int allowedTopY = surfaceY - UNDERGROUND_SURFACE_CLEARANCE;
|
||||
|
||||
+2
-2
@@ -142,7 +142,7 @@ public final class NativeStructurePlacementPlanner {
|
||||
return null;
|
||||
}
|
||||
if (!placement.isUnderground()) {
|
||||
int surfaceY = engine.getHeight(blockX, blockZ, true) + engine.getMinHeight();
|
||||
int surfaceY = Engine.hostHeight(engine, blockX, blockZ, true) + engine.getMinHeight();
|
||||
if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) {
|
||||
return null;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public final class NativeStructurePlacementPlanner {
|
||||
int localFluidHeight = engine.getComplex() == null
|
||||
? engine.getDimension().getFluidHeight()
|
||||
: (int) Math.round(engine.getComplex().getRiverWaterSurfaceStream().get(blockX, blockZ));
|
||||
return engine.getHeight(blockX, blockZ, true) < localFluidHeight;
|
||||
return Engine.hostHeight(engine, blockX, blockZ, true) < localFluidHeight;
|
||||
}
|
||||
|
||||
private static int comparePlacementPriority(IrisStructurePlacement left, IrisStructurePlacement right) {
|
||||
|
||||
@@ -21,6 +21,8 @@ package art.arcane.iris.engine.mantle;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
@@ -36,6 +38,7 @@ import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
@@ -196,17 +199,37 @@ public interface EngineMantle extends MatterGenerator {
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
default void insertMatter(int x, int z, Hunk<PlatformBlockState> blocks, boolean multicore) {
|
||||
default void insertMatter(
|
||||
int x,
|
||||
int z,
|
||||
Hunk<PlatformBlockState> blocks,
|
||||
boolean multicore,
|
||||
ChunkContext context
|
||||
) {
|
||||
if (!getEngine().getDimension().isUseMantle()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
||||
boolean protectUpper = upperCtx != null;
|
||||
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
|
||||
|
||||
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
|
||||
try {
|
||||
if (protectUpper) {
|
||||
if (stackContext != null) {
|
||||
DimensionStackLayout[] layouts = new DimensionStackLayout[256];
|
||||
for (int i = 0; i < layouts.length; i++) {
|
||||
int localX = i >> 4;
|
||||
int localZ = i & 15;
|
||||
layouts[i] = context.getDimensionStackLayout(localX, localZ);
|
||||
}
|
||||
chunk.iterate(PlatformBlockState.class, (localX, y, localZ, value) -> {
|
||||
DimensionStackLayout layout = layouts[(localX << 4) | (localZ & 15)];
|
||||
if (!layout.isHostFeatureProtectedY(y)) {
|
||||
blocks.set(localX, y, localZ, value);
|
||||
}
|
||||
});
|
||||
} else if (protectUpper) {
|
||||
int chunkBlockX = x << 4;
|
||||
int chunkBlockZ = z << 4;
|
||||
int gap = getEngine().getDimension().getUpperDimensionGap();
|
||||
|
||||
+1
-1
@@ -227,7 +227,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||
if (cached != CACHE_MISS) {
|
||||
return cached;
|
||||
}
|
||||
int surfaceY = engine.getHeight(x, z, true);
|
||||
int surfaceY = Engine.hostHeight(engine, x, z, true);
|
||||
surfaceHeights.put(key, surfaceY);
|
||||
return surfaceY;
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ public class IrisCaveCarver3D {
|
||||
if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) {
|
||||
columnSurfaceY = precomputedSurfaceHeights[index];
|
||||
} else {
|
||||
columnSurfaceY = engine.getHeight(x, z);
|
||||
columnSurfaceY = Engine.hostHeight(engine, x, z, true);
|
||||
}
|
||||
int unclampedClearanceTopY = columnSurfaceY - surfaceClearance;
|
||||
int clearanceTopY = Math.min(maxY, Math.max(minY, unclampedClearanceTopY));
|
||||
|
||||
+7
-5
@@ -189,7 +189,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
int zzz = 8 + (z << 4);
|
||||
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
|
||||
IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(xxx, zzz);
|
||||
int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true);
|
||||
int surfaceY = complex.getRoundedHeighteightStream().get(xxx, zzz);
|
||||
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
|
||||
if (IrisSettings.get().getGeneral().isDebug() && (x & 31) == 0 && (z & 31) == 0) {
|
||||
int carvedBlocks = 0;
|
||||
@@ -948,7 +948,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
Engine engine = getEngineMantle().getEngine();
|
||||
IrisBiome at = engine.getCaveBiome(x, y, z);
|
||||
IrisBiome surface = engine.getSurfaceBiome(x, z);
|
||||
IrisBiome surface = engine.getComplex().getTrueBiomeStream().get(x, z);
|
||||
return caveAnchorBiomeConflicts(at, surface, expectedCaveBiomeKey);
|
||||
}
|
||||
|
||||
@@ -1631,8 +1631,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
public Set<String> guess(int x, int z) {
|
||||
// todo The guess doesnt bring into account that the placer may return -1
|
||||
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
|
||||
IrisBiome biome = getEngineMantle().getEngine().getSurfaceBiome((x << 4) + 8, (z << 4) + 8);
|
||||
IrisRegion region = getEngineMantle().getEngine().getRegion((x << 4) + 8, (z << 4) + 8);
|
||||
int blockX = (x << 4) + 8;
|
||||
int blockZ = (z << 4) + 8;
|
||||
IrisBiome biome = getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
IrisRegion region = getComplex().getRegionStream().get(blockX, blockZ);
|
||||
Set<String> v = new KSet<>();
|
||||
for (IrisObjectPlacement i : biome.getSurfaceObjects()) {
|
||||
if (rng.chance(i.getChance() + rng.d(-0.005, 0.005)) && !i.isCompatExcluded(getData())) {
|
||||
@@ -1879,7 +1881,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return surfaceY;
|
||||
}
|
||||
|
||||
surfaceY = getEngineMantle().getEngine().getHeight(x, z);
|
||||
surfaceY = getEngineMantle().trueHeight(x, z);
|
||||
surfaceHeights.put(columnKey, surfaceY);
|
||||
return surfaceY;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
|
||||
public class ModeEnclosure extends IrisEngineMode implements EngineMode {
|
||||
public ModeEnclosure(Engine engine) {
|
||||
super(engine);
|
||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
var biome = new IrisBiomeActuator(getEngine());
|
||||
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||
|
||||
registerStage(burst(
|
||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
||||
|
||||
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
|
||||
public class ModeIslands extends IrisEngineMode implements EngineMode {
|
||||
public ModeIslands(Engine engine) {
|
||||
super(engine);
|
||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
var biome = new IrisBiomeActuator(getEngine());
|
||||
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||
|
||||
registerStage(burst(
|
||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
||||
|
||||
@@ -87,7 +87,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
if (shouldBypassMantleStages()) {
|
||||
return;
|
||||
}
|
||||
getMantle().insertMatter(x >> 4, z >> 4, K, m);
|
||||
getMantle().insertMatter(x >> 4, z >> 4, K, m, c);
|
||||
};
|
||||
EngineStage sFloatingTerrainSolid = (x, z, k, p, m, c) -> floatingChildBiomes.modify(x, z, k, m, c);
|
||||
EngineStage sFloatingDecorate = (x, z, k, p, m, c) -> floatingChildBiomes.decorateColumns(x, z, k, m, c);
|
||||
@@ -118,7 +118,9 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
|
||||
registerStage(sDecorant);
|
||||
registerStage(sFloatingDecorate);
|
||||
registerStage(sPerfection);
|
||||
registerStage(sCustom);
|
||||
if (getEngine().getDimensionStackContext() == null) {
|
||||
registerStage(sCustom);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldBypassMantleStages() {
|
||||
|
||||
@@ -27,8 +27,8 @@ import art.arcane.iris.engine.framework.IrisEngineMode;
|
||||
public class ModeSuperFlat extends IrisEngineMode implements EngineMode {
|
||||
public ModeSuperFlat(Engine engine) {
|
||||
super(engine);
|
||||
var terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
var biome = new IrisBiomeActuator(getEngine());
|
||||
IrisTerrainNormalActuator terrain = new IrisTerrainNormalActuator(getEngine());
|
||||
IrisBiomeActuator biome = new IrisBiomeActuator(getEngine());
|
||||
|
||||
registerStage(burst(
|
||||
(x, z, k, p, m, c) -> terrain.actuate(x, z, k, m, c),
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.UpperDimensionContext;
|
||||
import art.arcane.iris.engine.actuator.IrisDecorantActuator;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
@@ -91,6 +93,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
UpperDimensionContext upperCtx = getEngine().getUpperContext();
|
||||
boolean protectUpper = upperCtx != null && !getEngine().getDimension().isUpperDimensionCarving();
|
||||
int[] upperSurfaceHeights = protectUpper ? scratch.getOrCreateUpperSurfaceHeights() : null;
|
||||
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
|
||||
DimensionStackLayout[] stackLayouts = stackContext == null ? null : new DimensionStackLayout[256];
|
||||
int chunkBlockX = PowerOfTwoCoordinates.chunkToBlock(x);
|
||||
int chunkBlockZ = PowerOfTwoCoordinates.chunkToBlock(z);
|
||||
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
|
||||
@@ -104,6 +108,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
int gap = getEngine().getDimension().getUpperDimensionGap();
|
||||
upperSurfaceHeights[columnIndex] = Math.max(rawUpper, surfaceHeights[columnIndex] + gap);
|
||||
}
|
||||
if (stackLayouts != null) {
|
||||
stackLayouts[columnIndex] = context.getDimensionStackLayout(localX, localZ);
|
||||
}
|
||||
}
|
||||
|
||||
MantleChunk<Matter> mantleChunk = mantle.getChunk(x, z).use();
|
||||
@@ -117,6 +124,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
scratch,
|
||||
columnMasks,
|
||||
upperSurfaceHeights,
|
||||
stackLayouts,
|
||||
worldHeightSpan,
|
||||
caveLavaHeight,
|
||||
chunkBlockX,
|
||||
@@ -348,6 +356,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
if (context.upperSurfaceHeights() != null && y >= context.upperSurfaceHeights()[columnIndex]) {
|
||||
return;
|
||||
}
|
||||
if (context.stackLayouts() != null
|
||||
&& context.stackLayouts()[columnIndex].isHostFeatureProtectedY(y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlatformBlockState current = context.output().getRaw(localX, y, localZ);
|
||||
if (hydrology != null && hydrology.action() == HydrologyCaveAction.SEAL_GUARD) {
|
||||
@@ -405,6 +417,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
|
||||
IrisCarveScratch scratch,
|
||||
CarveColumnMask[] columnMasks,
|
||||
int[] upperSurfaceHeights,
|
||||
DimensionStackLayout[] stackLayouts,
|
||||
int worldHeightSpan,
|
||||
int caveLavaHeight,
|
||||
int chunkBlockX,
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.modifier;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineAssignedModifier;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDepositGenerator;
|
||||
import art.arcane.iris.engine.object.IrisDepositHeightDistribution;
|
||||
@@ -186,6 +187,11 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
|
||||
continue;
|
||||
}
|
||||
|
||||
DimensionStackLayout stackLayout = context.getDimensionStackLayout(nx, nz);
|
||||
if (stackLayout != null && stackLayout.isHostFeatureProtectedY(ny)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PlatformBlockState current = data.get(nx, ny, nz);
|
||||
if (!canReplaceDepositTarget(current)) {
|
||||
continue;
|
||||
|
||||
+28
-3
@@ -20,6 +20,7 @@ package art.arcane.iris.engine.modifier;
|
||||
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.decorator.FloatingDecorator;
|
||||
import art.arcane.iris.engine.decorator.IrisSeaSurfaceDecorator;
|
||||
@@ -285,7 +286,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
||||
}
|
||||
}
|
||||
|
||||
writeIslandSkyBiomes(parent, wx, wz, sample, chunkHeight, data);
|
||||
writeIslandSkyBiomes(
|
||||
parent,
|
||||
wx,
|
||||
wz,
|
||||
sample,
|
||||
chunkHeight,
|
||||
data,
|
||||
context.getDimensionStackLayout(xf, zf)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +376,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
||||
}
|
||||
}
|
||||
|
||||
private void writeIslandSkyBiomes(IrisBiome parent, int wx, int wz, FloatingIslandSample sample, int chunkHeight, IrisData data) {
|
||||
private void writeIslandSkyBiomes(
|
||||
IrisBiome parent,
|
||||
int wx,
|
||||
int wz,
|
||||
FloatingIslandSample sample,
|
||||
int chunkHeight,
|
||||
IrisData data,
|
||||
DimensionStackLayout stackLayout
|
||||
) {
|
||||
try {
|
||||
IdentityHashMap<IrisFloatingChildBiomes, MatterBiomeInject> matterByEntry = new IdentityHashMap<>();
|
||||
for (int k = 0; k <= sample.topIdx; k++) {
|
||||
@@ -378,6 +395,9 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
||||
if (y < 0 || y >= chunkHeight) {
|
||||
continue;
|
||||
}
|
||||
if (!shouldWriteHostBiomeMarker(stackLayout, y)) {
|
||||
continue;
|
||||
}
|
||||
IrisFloatingChildBiomes entry = sample.entryAt(k);
|
||||
MatterBiomeInject matter = matterByEntry.get(entry);
|
||||
if (matter == null) {
|
||||
@@ -395,10 +415,15 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldWriteHostBiomeMarker(DimensionStackLayout stackLayout, int y) {
|
||||
return stackLayout == null || !stackLayout.isHostFeatureProtectedY(y);
|
||||
}
|
||||
|
||||
private MatterBiomeInject createSkyBiomeMatter(IrisBiome target, int wx, int wz) {
|
||||
if (target.isCustom()) {
|
||||
IrisBiomeCustom custom = target.getCustomBiome(rng, getEngine(), wx, 0, wz);
|
||||
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(getDimension().getLoadKey() + ":" + custom.getId()));
|
||||
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(
|
||||
getDimension().getCustomBiomeKey(custom.getId())));
|
||||
}
|
||||
|
||||
return BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz)));
|
||||
|
||||
@@ -40,6 +40,7 @@ import art.arcane.iris.util.common.data.DataProvider;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -495,6 +496,10 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
|
||||
}
|
||||
|
||||
public KList<PlatformBlockState> generateLayersWithSlope(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, ProceduralStream<Double> slopeStream) {
|
||||
return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, slopeStream);
|
||||
}
|
||||
|
||||
public KList<PlatformBlockState> generateCeilingLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
||||
return IrisBiomeLayerGenerator.generateCeilingLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.project.noise.CNG;
|
||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
@@ -42,8 +43,16 @@ final class IrisBiomeLayerGenerator {
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
||||
return generateLayers(biome, dim, wx, wz, random, maxDepth, height, rdata, complex, null);
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, ProceduralStream<Double> slopeStream) {
|
||||
return generateLayers(biome, dim, wx, wz, random, maxDepth, height, rdata, null, slopeStream);
|
||||
}
|
||||
|
||||
private static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex, ProceduralStream<Double> slopeStream) {
|
||||
if (biome.isLockLayers()) {
|
||||
return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex);
|
||||
return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex, slopeStream);
|
||||
}
|
||||
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
@@ -70,7 +79,7 @@ final class IrisBiomeLayerGenerator {
|
||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||
|
||||
if (!sc.isDefault()) {
|
||||
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) {
|
||||
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
|
||||
d = 0;
|
||||
}
|
||||
}
|
||||
@@ -168,6 +177,10 @@ final class IrisBiomeLayerGenerator {
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex) {
|
||||
return generateLockedLayers(biome, wx, wz, random, maxDepthf, height, rdata, complex, null);
|
||||
}
|
||||
|
||||
private static KList<PlatformBlockState> generateLockedLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepthf, int height, IrisData rdata, IrisComplex complex, ProceduralStream<Double> slopeStream) {
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
KList<PlatformBlockState> real = new KList<>();
|
||||
int maxDepth = Math.min(maxDepthf, biome.getLockLayersMax());
|
||||
@@ -190,7 +203,7 @@ final class IrisBiomeLayerGenerator {
|
||||
IrisSlopeClip sc = layer.getSlopeCondition();
|
||||
|
||||
if (!sc.isDefault()) {
|
||||
if (!sc.isValid(complex.getSlopeStream().get(wx, wz))) {
|
||||
if (!sc.isValid(resolveSlopeStream(complex, slopeStream).getDouble(wx, wz))) {
|
||||
d = 0;
|
||||
}
|
||||
}
|
||||
@@ -222,6 +235,13 @@ final class IrisBiomeLayerGenerator {
|
||||
return real;
|
||||
}
|
||||
|
||||
private static ProceduralStream<Double> resolveSlopeStream(
|
||||
IrisComplex complex,
|
||||
ProceduralStream<Double> slopeStream
|
||||
) {
|
||||
return slopeStream == null ? complex.getSlopeStream() : slopeStream;
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateSeaLayers(IrisBiome biome, double wx, double wz, RNG random, int maxDepth, IrisData rdata) {
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
|
||||
|
||||
@@ -222,6 +222,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private boolean upperDimensionObjects = false;
|
||||
@Desc("When true, upper-dimension objects force-place regardless of placement restrictions (slope, underwater, clamp, collisions, carving). Normal dimension objects always place first; upper objects place second and may clip or occlude lower-dimension placements when this is enabled.")
|
||||
private boolean upperObjectsForcePlace = false;
|
||||
@Desc("Stack dimension terrain upright in declared top-to-bottom order. The final dimension key must reference this dimension.")
|
||||
private IrisDimensionStack dimensionStack = null;
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
@Desc("Keep this either undefined or empty. Setting any biome name into this will force iris to only generate the specified biome. Great for testing.")
|
||||
private String focus = "";
|
||||
@@ -528,6 +530,12 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasFocusRegion()) {
|
||||
IrisRegion focus = data.getRegionLoader().load(getFocusRegion());
|
||||
if (focus != null && !focus.isCompatExcluded()) {
|
||||
regions.put(getFocusRegion(), focus);
|
||||
}
|
||||
}
|
||||
return regions.v();
|
||||
}
|
||||
|
||||
@@ -564,6 +572,7 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
Deque<String> pending = new ArrayDeque<>();
|
||||
addReachableBiomeKey(pending, getFocus());
|
||||
IrisHydrology configuredHydrology = getHydrology();
|
||||
IrisRiverHydrology configuredRivers = configuredHydrology == null ? null : configuredHydrology.getRivers();
|
||||
boolean hydrologyPoliciesActive = configuredRivers != null && configuredRivers.isEnabled();
|
||||
@@ -721,8 +730,14 @@ public class IrisDimension extends IrisRegistrant {
|
||||
}
|
||||
|
||||
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots, KSet<String> biomes) throws IOException {
|
||||
String namespace = getLoadKey().toLowerCase(Locale.ROOT);
|
||||
installBiomes(fixer, data, datapackRoots, namespace, "", biomes);
|
||||
String keyPrefix = getCustomBiomeKeyPrefix();
|
||||
int separator = keyPrefix.indexOf(':');
|
||||
String namespace = keyPrefix.substring(0, separator);
|
||||
String pathPrefix = keyPrefix.substring(separator + 1);
|
||||
if (pathPrefix.endsWith("/")) {
|
||||
pathPrefix = pathPrefix.substring(0, pathPrefix.length() - 1);
|
||||
}
|
||||
installBiomes(fixer, data, datapackRoots, namespace, pathPrefix, biomes);
|
||||
}
|
||||
|
||||
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> datapackRoots,
|
||||
@@ -747,18 +762,19 @@ public class IrisDimension extends IrisRegistrant {
|
||||
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
String customBiomeId = customBiome.getId();
|
||||
String customBiomePath = sanitizeRegistryPath(customBiomeId, "biome");
|
||||
String json = customBiome.generateJson(fixer, contentGate);
|
||||
|
||||
synchronized (biomes) {
|
||||
if (!biomes.add(customBiomeId)) {
|
||||
if (!biomes.add(customBiomePath)) {
|
||||
IrisLogging.debug("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
String biomePath = pathPrefix.isBlank()
|
||||
? customBiomeId
|
||||
: pathPrefix + "/" + customBiomeId;
|
||||
? customBiomePath
|
||||
: pathPrefix + "/" + customBiomePath;
|
||||
for (File datapackRoot : datapackRoots) {
|
||||
File output = new File(datapackRoot, "data/" + namespace + "/worldgen/biome/" + biomePath + ".json");
|
||||
|
||||
@@ -900,6 +916,28 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return sanitizeDimensionTypeKeyValue(getLoadKey());
|
||||
}
|
||||
|
||||
public String getCustomBiomeKey(String customBiomeId) {
|
||||
return customBiomeKey(getLoadKey(), customBiomeId);
|
||||
}
|
||||
|
||||
public String getCustomBiomeKeyPrefix() {
|
||||
return customBiomeKeyPrefix(getLoadKey());
|
||||
}
|
||||
|
||||
public static String customBiomeKey(String dimensionLoadKey, String customBiomeId) {
|
||||
return customBiomeKeyPrefix(dimensionLoadKey) + sanitizeRegistryPath(customBiomeId, "biome");
|
||||
}
|
||||
|
||||
private static String customBiomeKeyPrefix(String dimensionLoadKey) {
|
||||
String dimensionPath = sanitizeRegistryPath(dimensionLoadKey, "dimension");
|
||||
int separator = dimensionPath.indexOf('/');
|
||||
if (separator < 0) {
|
||||
return sanitizeRegistryNamespace(dimensionPath) + ":";
|
||||
}
|
||||
String namespace = sanitizeRegistryNamespace(dimensionPath.substring(0, separator));
|
||||
return namespace + ":" + dimensionPath.substring(separator + 1) + "/";
|
||||
}
|
||||
|
||||
public static String sanitizeDimensionTypeKeyValue(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return "dimension";
|
||||
@@ -918,6 +956,26 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return sanitized.isBlank() ? "dimension" : sanitized;
|
||||
}
|
||||
|
||||
private static String sanitizeRegistryNamespace(String value) {
|
||||
String sanitized = value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_.-]", "_");
|
||||
return sanitized.isBlank() ? "dimension" : sanitized;
|
||||
}
|
||||
|
||||
private static String sanitizeRegistryPath(String value, String fallback) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/");
|
||||
sanitized = sanitized.replaceAll("[^a-z0-9/._-]", "_");
|
||||
sanitized = sanitized.replaceAll("/+", "/");
|
||||
sanitized = sanitized.replaceAll("^/+", "");
|
||||
sanitized = sanitized.replaceAll("/+$", "");
|
||||
while (sanitized.contains("..")) {
|
||||
sanitized = sanitized.replace("..", "_");
|
||||
}
|
||||
return sanitized.isBlank() ? fallback : sanitized;
|
||||
}
|
||||
|
||||
public IrisDimensionType getDimensionType() {
|
||||
IrisDimensionTypeOptions options = getDimensionOptions();
|
||||
if (fullbright) {
|
||||
@@ -930,6 +988,10 @@ public class IrisDimension extends IrisRegistrant {
|
||||
return upperDimension != null && !upperDimension.isEmpty() && !upperDimension.equalsIgnoreCase("none");
|
||||
}
|
||||
|
||||
public boolean hasDimensionStack() {
|
||||
return dimensionStack != null;
|
||||
}
|
||||
|
||||
public void installDimensionType(IDataFixer fixer, KList<File> datapackRoots) throws IOException {
|
||||
IrisDimensionType type = getDimensionType();
|
||||
String json = type.toJson(fixer);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 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.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Stacks dimension terrain upright in declared top-to-bottom order")
|
||||
@Data
|
||||
public class IrisDimensionStack {
|
||||
@Required
|
||||
@ArrayType(min = 2, type = String.class)
|
||||
@RegistryListResource(IrisDimension.class)
|
||||
@Desc("Dimension load keys ordered from the highest layer to the lowest layer. The final key must be this dimension.")
|
||||
private KList<String> dimensions = new KList<>();
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(256)
|
||||
@Desc("Nominal air gap in blocks between adjacent dimension layers")
|
||||
private int spacer = 32;
|
||||
|
||||
@Desc("Optional noise that blends adjacent layer boundaries. Omit this object for a constant configured gap.")
|
||||
private IrisDimensionStackBlend blend = null;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2026 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.engine.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@Desc("Controls noise blending at each dimension stack boundary")
|
||||
@Data
|
||||
public class IrisDimensionStackBlend {
|
||||
@MinNumber(0)
|
||||
@MaxNumber(256)
|
||||
@Desc("Maximum vertical boundary displacement in blocks")
|
||||
private int amplitude = 8;
|
||||
|
||||
@Desc("Noise style sampled independently for each adjacent layer boundary")
|
||||
private IrisGeneratorStyle style = NoiseStyle.SIMPLEX.style();
|
||||
}
|
||||
@@ -553,11 +553,15 @@ public final class EngineBukkitOps {
|
||||
}
|
||||
|
||||
public static IrisRegion getRegion(Engine engine, Location l) {
|
||||
return engine.getRegion(l.getBlockX(), l.getBlockZ());
|
||||
return engine.getRegion(
|
||||
l.getBlockX(), l.getBlockY() - engine.getWorld().minHeight(), l.getBlockZ());
|
||||
}
|
||||
|
||||
public static IrisBiome getBiomeOrMantle(Engine engine, Location l) {
|
||||
return engine.getBiomeOrMantle(l.getBlockX(), l.getBlockY(), l.getBlockZ());
|
||||
return engine.getBiomeOrMantle(
|
||||
l.getBlockX(),
|
||||
l.getBlockY() - engine.getWorld().minHeight(),
|
||||
l.getBlockZ());
|
||||
}
|
||||
|
||||
public static IrisBiome getSurfaceBiome(Engine engine, Chunk c) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.util.project.context;
|
||||
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.EngineMetrics;
|
||||
@@ -27,6 +29,8 @@ public class ChunkContext {
|
||||
private final ChunkedDataCache<PlatformBlockState> fluid;
|
||||
private final ChunkedDataCache<IrisRegion> region;
|
||||
private final FloatingIslandBoundarySampler floatingIslandBoundarySampler;
|
||||
private final DimensionStackContext dimensionStackContext;
|
||||
private final DimensionStackLayout[] dimensionStackLayouts;
|
||||
|
||||
public ChunkContext(int x, int z, IrisComplex complex) {
|
||||
this(x, z, complex, 0L, true, PrefillPlan.NO_CAVE, null);
|
||||
@@ -45,6 +49,19 @@ public class ChunkContext {
|
||||
}
|
||||
|
||||
public ChunkContext(int x, int z, IrisComplex complex, long generationSessionId, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) {
|
||||
this(x, z, complex, generationSessionId, cache, prefillPlan, metrics, null);
|
||||
}
|
||||
|
||||
public ChunkContext(
|
||||
int x,
|
||||
int z,
|
||||
IrisComplex complex,
|
||||
long generationSessionId,
|
||||
boolean cache,
|
||||
PrefillPlan prefillPlan,
|
||||
EngineMetrics metrics,
|
||||
DimensionStackContext dimensionStackContext
|
||||
) {
|
||||
this.x = x;
|
||||
this.z = z;
|
||||
this.complex = complex;
|
||||
@@ -57,6 +74,8 @@ public class ChunkContext {
|
||||
this.fluid = new ChunkedDataCache<>(complex.getFluidStream(), x, z, cache);
|
||||
this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache);
|
||||
this.floatingIslandBoundarySampler = new FloatingIslandBoundarySampler((wx, wz) -> complex.getTrueBiomeStream().get(wx, wz));
|
||||
this.dimensionStackContext = dimensionStackContext;
|
||||
this.dimensionStackLayouts = dimensionStackContext == null ? null : new DimensionStackLayout[256];
|
||||
|
||||
if (cache) {
|
||||
PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan;
|
||||
@@ -165,6 +184,19 @@ public class ChunkContext {
|
||||
return region;
|
||||
}
|
||||
|
||||
public DimensionStackLayout getDimensionStackLayout(int localX, int localZ) {
|
||||
if (dimensionStackContext == null) {
|
||||
return null;
|
||||
}
|
||||
int index = (localZ << 4) | localX;
|
||||
DimensionStackLayout layout = dimensionStackLayouts[index];
|
||||
if (layout == null) {
|
||||
layout = dimensionStackContext.sample(x + localX, z + localZ);
|
||||
dimensionStackLayouts[index] = layout;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
public enum PrefillPlan {
|
||||
ALL(true, true, true, true, true, true),
|
||||
NO_CAVE(true, true, false, true, true, true),
|
||||
|
||||
Reference in new issue
Block a user