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:
Brian Neumann-Fopiano committed 2026-09-04 14:23:15 -04:00
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;
@@ -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();
@@ -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));
@@ -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;
@@ -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),