Datapack export

- /iris datapack export compiles one pack dimension into a worldgen datapack for Minecraft 26.3 and later
- Bukkit and mod loaders take the same version, mode, namespace, output, format, objects and replace arguments
- The default overworld mode replaces the Overworld of a new world with the default world type
- Preset and dimension modes write a selectable world preset or a separate dimension
- The export runs in the background and publishes the zip or folder atomically
- Pack compiler stages turn noise, terrain, caves, subterrain, biomes, surface, decoration, objects and structures into a version-neutral IR
- Capability lowering fits every field to 26.3 density function ranges
- Disjoint min and max nodes fold to the input that always wins
- The 26.3 writer emits density functions, noise settings, biomes, features, structures, presets and dimensions
- Every export carries the Iris logo as its pack.png
- Unused objects, templates and features are pruned from the output
- The pruner keeps pack.png without reading references from its bytes
- Imported pack datapacks are bundled into the export
- Every approximation reports an export limit diagnostic
- Generation cost is counted against a node budget and reported as a warning
- Cave biomes over the wall palette limit use the closest kept palette
- Jigsaw exports share the template encoder, block states, destinations and publisher with the worldgen export
- DataVersion carries the pack format minor and world data version
- Both platforms expose vanilla data from the server classpath
- GoldenHashEngine and the export probes share one chunk spiral scan
- Headless 26.3 probes measure export cost and compare column and chunk likeness against Iris
- Real-codec tests decode the exported resources with the 26.3 registries
- Paper registry tests run in isolated registry scopes
- Command help and messages cover the export in every language file
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-30 22:59:04 -04:00
1 parent b00e28e42e
commit 1ed1044474
385 files changed
+48102 -535

No files matched your search

@@ -0,0 +1,74 @@
/*
* 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.generation.validation;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.spi.IrisLogging;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public final class ChunkSpiralScan {
private ChunkSpiralScan() {
}
public static int scan(List<int[]> targets, int threads, ChunkVisitor visitor, Listener listener) throws InterruptedException {
Semaphore inFlight = new Semaphore(Math.max(1, threads));
CountDownLatch done = new CountDownLatch(targets.size());
AtomicInteger failures = new AtomicInteger();
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
boolean ok = false;
try {
visitor.visit(chunkX, chunkZ);
ok = true;
} catch (Throwable e) {
failures.incrementAndGet();
IrisLogging.reportError(e);
listener.chunkFailed(chunkX, chunkZ, e);
} finally {
listener.chunkDone(chunkX, chunkZ, ok);
inFlight.release();
done.countDown();
}
});
}
done.await();
return failures.get();
}
@FunctionalInterface
public interface ChunkVisitor {
void visit(int chunkX, int chunkZ) throws Exception;
}
public interface Listener {
void chunkDone(int chunkX, int chunkZ, boolean ok);
void chunkFailed(int chunkX, int chunkZ, Throwable error);
}
}
@@ -26,7 +26,6 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.ChunkSpiral;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -44,8 +43,6 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
public final class GoldenHashEngine {
@@ -210,40 +207,18 @@ public final class GoldenHashEngine {
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
Map<Long, String> lines = new ConcurrentHashMap<>();
Semaphore inFlight = new Semaphore(threads);
CountDownLatch done = new CountDownLatch(targets.size());
AtomicInteger completed = new AtomicInteger();
int total = targets.size();
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
boolean ok = false;
try {
ChunkSnapshot snapshot = source.generate(chunkX, chunkZ);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot));
if (request.deep()) {
writeDeepDump(chunkX, chunkZ, snapshot);
}
ok = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
progress.chunkFailed(chunkX, chunkZ, e);
} finally {
progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total);
inFlight.release();
done.countDown();
}
});
}
done.await();
ChunkSpiralScan.scan(targets, threads, (int chunkX, int chunkZ) -> hash(lines, chunkX, chunkZ), new ScanProgress(targets.size()));
return lines;
}
private void hash(Map<Long, String> lines, int chunkX, int chunkZ) throws Exception {
ChunkSnapshot snapshot = source.generate(chunkX, chunkZ);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot));
if (request.deep()) {
writeDeepDump(chunkX, chunkZ, snapshot);
}
}
private String hashChunk(int chunkX, int chunkZ, ChunkSnapshot snapshot) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
@@ -562,4 +537,23 @@ public final class GoldenHashEngine {
throw new IllegalStateException(e);
}
}
private final class ScanProgress implements ChunkSpiralScan.Listener {
private final AtomicInteger completed = new AtomicInteger();
private final int total;
private ScanProgress(int total) {
this.total = total;
}
@Override
public void chunkDone(int chunkX, int chunkZ, boolean ok) {
progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total);
}
@Override
public void chunkFailed(int chunkX, int chunkZ, Throwable error) {
progress.chunkFailed(chunkX, chunkZ, error);
}
}
}
@@ -33,6 +33,74 @@ public final class DirectorCommandMessages {
"iris.director.commanddatapack.param.datapack_id_folder_name_shown_by_iris_datapack_list",
"The datapack id (folder name) shown by /iris datapack list"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_DESCRIPTION = TextKey.of(
"iris.director.commanddatapack.director.export_dimension_vanilla_worldgen_datapack",
"Export a dimension as a vanilla worldgen datapack"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_DIMENSION = TextKey.of(
"iris.director.commanddatapack.param.export_dimension",
"The dimension to export"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_VERSION = TextKey.of(
"iris.director.commanddatapack.param.export_version",
"Target Minecraft version, 26.3 or later (blank uses the latest)"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_MODE = TextKey.of(
"iris.director.commanddatapack.param.export_mode",
"Output mode: preset, dimension, or overworld"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_NAMESPACE = TextKey.of(
"iris.director.commanddatapack.param.export_namespace",
"Namespace for the exported resources"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_OUTPUT = TextKey.of(
"iris.director.commanddatapack.param.export_output",
"Output name in the datapack exports folder (blank uses the dimension and version)"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_FORMAT = TextKey.of(
"iris.director.commanddatapack.param.export_format",
"zip or directory"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_OBJECTS = TextKey.of(
"iris.director.commanddatapack.param.export_objects",
"Include objects as structure templates"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_PARAM_REPLACE = TextKey.of(
"iris.director.commanddatapack.param.export_replace",
"Replace an existing export with the same name"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_STARTED = TextKey.of(
"iris.director.commanddatapack.export.started",
"Exporting {dimension} as a datapack for Minecraft {version}..."
);
public static final TextKey COMMAND_DATAPACK_EXPORT_STAGE = TextKey.of(
"iris.director.commanddatapack.export.stage",
"Datapack export: {stage}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_RESOURCES = TextKey.of(
"iris.director.commanddatapack.export.resources",
"Datapack export: {done}/{total} resources"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_DIAGNOSTIC = TextKey.of(
"iris.director.commanddatapack.export.diagnostic",
"{key}: {message}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_EXPORTED = TextKey.of(
"iris.director.commanddatapack.export.exported",
"Datapack exported: {path}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_REJECTED = TextKey.of(
"iris.director.commanddatapack.export.rejected",
"Datapack export rejected: {reason}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_FAILED = TextKey.of(
"iris.director.commanddatapack.export.failed",
"Datapack export failed: {reason}"
);
public static final TextKey COMMAND_DATAPACK_EXPORT_INVALID = TextKey.of(
"iris.director.commanddatapack.export.invalid",
"Invalid datapack export arguments: {reason}"
);
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER = TextKey.of(
"iris.director.commanddeveloper.director.iris_world_manager",
"Iris World Manager"
@@ -888,6 +956,23 @@ public final class DirectorCommandMessages {
COMMAND_DATAPACK_DIRECTOR_LIST_CONFIGURED_DATAPACK_IMPORTS_THEIR_INSTALLED_VERSIONS,
COMMAND_DATAPACK_DIRECTOR_REMOVE_INSTALLED_DATAPACK_BY_ID_ALSO_DELETE_ITS_URL_FROM_DATAPACKIMPORTS_KEEP,
COMMAND_DATAPACK_PARAM_DATAPACK_ID_FOLDER_NAME_SHOWN_BY_IRIS_DATAPACK_LIST,
COMMAND_DATAPACK_EXPORT_DESCRIPTION,
COMMAND_DATAPACK_EXPORT_PARAM_DIMENSION,
COMMAND_DATAPACK_EXPORT_PARAM_VERSION,
COMMAND_DATAPACK_EXPORT_PARAM_MODE,
COMMAND_DATAPACK_EXPORT_PARAM_NAMESPACE,
COMMAND_DATAPACK_EXPORT_PARAM_OUTPUT,
COMMAND_DATAPACK_EXPORT_PARAM_FORMAT,
COMMAND_DATAPACK_EXPORT_PARAM_OBJECTS,
COMMAND_DATAPACK_EXPORT_PARAM_REPLACE,
COMMAND_DATAPACK_EXPORT_STARTED,
COMMAND_DATAPACK_EXPORT_STAGE,
COMMAND_DATAPACK_EXPORT_RESOURCES,
COMMAND_DATAPACK_EXPORT_DIAGNOSTIC,
COMMAND_DATAPACK_EXPORT_EXPORTED,
COMMAND_DATAPACK_EXPORT_REJECTED,
COMMAND_DATAPACK_EXPORT_FAILED,
COMMAND_DATAPACK_EXPORT_INVALID,
COMMAND_DEVELOPER_DIRECTOR_IRIS_WORLD_MANAGER,
COMMAND_DEVELOPER_DIRECTOR_GET_LOADED_TECTONICPLATES_COUNT,
COMMAND_DEVELOPER_DIRECTOR_HASH_GENERATED_BLOCK_OUTPUT_FIXED_AREA_DETERMINISM_IDENTITY_TESTING,
@@ -13,20 +13,24 @@ import art.arcane.iris.pack.datapack.v263.DataFixerV263;
//https://minecraft.wiki/w/Pack_format
@Getter
public enum DataVersion {
UNSUPPORTED("0.0.0", 0, () -> null),
V1_21_1("1.21.1", 48, DataFixerV1206::new),
V26_1_2("26.1.2", 101, DataFixerV1217::new),
V26_2("26.2", 107, DataFixerV1217::new),
V26_3("26.3", 121, DataFixerV263::new);
UNSUPPORTED("0.0.0", 0, 0, 0, () -> null),
V1_21_1("1.21.1", 48, 0, 3955, DataFixerV1206::new),
V26_1_2("26.1.2", 101, 1, 4790, DataFixerV1217::new),
V26_2("26.2", 107, 1, 4903, DataFixerV1217::new),
V26_3("26.3", 121, 0, 5023, DataFixerV263::new);
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
@Getter(AccessLevel.NONE)
private final Supplier<IDataFixer> constructor;
private final String version;
private final int packFormat;
private final int packFormatMinor;
private final int worldDataVersion;
DataVersion(String version, int packFormat, Supplier<IDataFixer> constructor) {
DataVersion(String version, int packFormat, int packFormatMinor, int worldDataVersion, Supplier<IDataFixer> constructor) {
this.constructor = constructor;
this.packFormat = packFormat;
this.packFormatMinor = packFormatMinor;
this.worldDataVersion = worldDataVersion;
this.version = version;
}
@@ -0,0 +1,99 @@
package art.arcane.iris.pack.datapack;
import art.arcane.iris.pack.datapack.DatapackIngestService.Entry;
import art.arcane.iris.pack.datapack.ModrinthResolver.ResolvedDatapack;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.io.IO;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.LinkOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
public final class DatapackExportArchives {
private static final String DIRECT_VERSION_PREFIX = "direct-";
private DatapackExportArchives() {
}
public static Path resolve(String source, DataVersion target) throws IOException {
String normalized = DatapackImportSources.normalizeConfiguredSource(Objects.requireNonNull(source, "datapack import source"));
URI uri = DatapackStartupValidation.parseSourceUri(normalized);
if (uri == null) {
throw new IOException("Empty datapack import source");
}
if ("file".equalsIgnoreCase(uri.getScheme())) {
return DatapackArchive.requireLocalDatapackPath(uri);
}
IrisPlatform platform = IrisPlatforms.get();
File root = platform.dataFolder("datapacks");
File cacheDir = new File(root, "cache");
boolean running = DataVersion.forMinecraftVersion(platform.minecraftVersion()) == target;
DatapackTransactions.TRANSACTION_LOCK.lock();
try {
Entry entry = manifestEntry(root, normalized);
Path cached = ingestCache(cacheDir, entry, running);
if (cached != null) {
return cached;
}
return download(cacheDir, normalized, entry, target);
} finally {
DatapackTransactions.TRANSACTION_LOCK.unlock();
}
}
private static Entry manifestEntry(File root, String source) {
try {
return DatapackManifestStore.readCommittedManifest(root).find(source);
} catch (IOException exception) {
IrisLogging.reportError("The datapack manifest under " + root.getPath() + " could not be read; '" + source
+ "' is resolved again for the export", exception);
return null;
}
}
private static Path ingestCache(File cacheDir, Entry entry, boolean running) throws IOException {
if (entry == null || entry.id == null || entry.sha1 == null) {
return null;
}
boolean direct = entry.versionId != null && entry.versionId.startsWith(DIRECT_VERSION_PREFIX);
if (!direct && !running) {
return null;
}
File archive = new File(cacheDir, entry.id + "-" + DatapackSupport.safeFile(entry.versionId) + ".zip");
return matches(archive, entry.sha1) ? archive.toPath() : null;
}
private static Path download(File cacheDir, String source, Entry entry, DataVersion target) throws IOException {
ResolvedDatapack resolved = ModrinthResolver.resolve(source, target.getVersion());
String id = entry == null || entry.id == null ? DatapackArchive.deriveId(resolved) : entry.id;
File archive = new File(cacheDir, id + "-" + DatapackSupport.safeFile(resolved.getVersionId()) + ".zip");
String expected = resolved.getSha1();
if (expected != null && !expected.isBlank() && matches(archive, expected)) {
return archive.toPath();
}
DatapackSupport.ensureScratchDirectory(cacheDir, "datapack download cache");
DatapackArchive.download(resolved.getDownloadUrl(), archive, null, null);
if (expected != null && !expected.isBlank()) {
String checksum = DatapackArchive.sha1(archive);
if (!expected.equalsIgnoreCase(checksum)) {
IO.delete(archive);
throw new IOException("Checksum mismatch for " + id + " (expected " + expected + ", got " + checksum + ")");
}
}
return archive.toPath();
}
private static boolean matches(File archive, String sha1) throws IOException {
Path path = archive.toPath();
if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
return false;
}
return sha1.equalsIgnoreCase(DatapackArchive.sha1(archive));
}
}
@@ -1,4 +1,4 @@
package art.arcane.iris.structure.export;
package art.arcane.iris.pack.datapack.export;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
@@ -14,25 +14,26 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java.util.stream.Stream;
final class AtomicDatapackPublisher {
Publication publish(
Map<String, byte[]> resources,
Path output,
VanillaJigsawExportFormat format,
boolean replaceExisting
public final class AtomicDatapackPublisher {
public Publication publish(
Map<String, byte[]> files,
Path destination,
DatapackExportFormat format,
boolean replace,
String stagingPrefix
) throws IOException {
Path parent = output.getParent();
Path parent = destination.getParent();
if (parent == null) {
throw new IOException("Export output has no parent directory: " + output);
throw new IOException("Export output has no parent directory: " + destination);
}
Files.createDirectories(parent);
Path staging = Files.createTempDirectory(parent, ".iris-jigsaw-export-");
Path staging = Files.createTempDirectory(parent, stagingPrefix);
List<String> cleanupWarnings = new ArrayList<>();
try {
Path artifact = format == VanillaJigsawExportFormat.DIRECTORY
? createDirectoryArtifact(staging, resources)
: createZipArtifact(staging, resources);
replaceAtomically(artifact, output, replaceExisting, cleanupWarnings);
Path artifact = format == DatapackExportFormat.DIRECTORY
? createDirectoryArtifact(staging, files)
: createZipArtifact(staging, files);
replaceAtomically(artifact, destination, replace, cleanupWarnings);
} finally {
try {
deleteTree(staging);
@@ -152,8 +153,8 @@ final class AtomicDatapackPublisher {
}
}
record Publication(List<String> cleanupWarnings) {
Publication {
public record Publication(List<String> cleanupWarnings) {
public Publication {
cleanupWarnings = List.copyOf(cleanupWarnings);
}
}
@@ -0,0 +1,22 @@
package art.arcane.iris.pack.datapack.export;
public record ContentTrim(int filesBefore, long bytesBefore, int filesAfter, long bytesAfter) {
public static final ContentTrim NONE = new ContentTrim(0, 0L, 0, 0L);
public ContentTrim {
if (filesBefore < 0 || bytesBefore < 0L || filesAfter < 0 || bytesAfter < 0L) {
throw new IllegalArgumentException("Content trim counts cannot be negative");
}
if (filesAfter > filesBefore || bytesAfter > bytesBefore) {
throw new IllegalArgumentException("A content trim cannot add files or bytes");
}
}
public int filesRemoved() {
return filesBefore - filesAfter;
}
public long bytesRemoved() {
return bytesBefore - bytesAfter;
}
}
@@ -0,0 +1,53 @@
package art.arcane.iris.pack.datapack.export;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
public final class DatapackExportDestinations {
private static final Pattern OUTPUT_NAME = Pattern.compile("[a-zA-Z0-9_-][a-zA-Z0-9._-]{0,127}");
private static final Set<Path> CLAIMED = ConcurrentHashMap.newKeySet();
private DatapackExportDestinations() {
}
public static DatapackExportFormat parseFormat(String raw) {
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
case "zip" -> DatapackExportFormat.ZIP;
case "directory", "dir", "folder" -> DatapackExportFormat.DIRECTORY;
default -> throw new IllegalArgumentException("Export format must be directory or zip.");
};
}
public static Path resolve(Path exportRoot, String output, DatapackExportFormat format) {
Path root = Objects.requireNonNull(exportRoot, "Datapack export root").toAbsolutePath().normalize();
String requested = Objects.requireNonNull(output, "Datapack export output name");
String name = requested.trim();
if (!name.equals(requested) || !OUTPUT_NAME.matcher(name).matches()) {
throw new IllegalArgumentException(
"Export output must be one file name using letters, numbers, '.', '_', or '-'.");
}
String fileName = format == DatapackExportFormat.ZIP && !name.endsWith(".zip") ? name + ".zip" : name;
Path destination = root.resolve(fileName).normalize();
if (destination.equals(root) || !root.equals(destination.getParent())) {
throw new IllegalArgumentException("Export output must be one artifact inside the exports folder.");
}
return destination;
}
public static boolean claim(Path destination) {
return CLAIMED.add(normalized(destination));
}
public static void release(Path destination) {
CLAIMED.remove(normalized(destination));
}
private static Path normalized(Path destination) {
return Objects.requireNonNull(destination, "Datapack export destination").toAbsolutePath().normalize();
}
}
@@ -0,0 +1,6 @@
package art.arcane.iris.pack.datapack.export;
public enum DatapackExportFormat {
ZIP,
DIRECTORY
}
@@ -0,0 +1,83 @@
package art.arcane.iris.pack.datapack.export;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
public record GenerationCost(Map<GenerationCost.Root, GenerationCost.RootCost> roots, long densityFunctionBytes) {
public static final GenerationCost NONE = new GenerationCost(Map.of(), 0L);
private static final String DATA_PREFIX = "data/";
private static final String DENSITY_FUNCTION_FOLDER = "/worldgen/density_function/";
private static final String JSON_SUFFIX = ".json";
public GenerationCost {
EnumMap<Root, RootCost> copy = new EnumMap<>(Root.class);
copy.putAll(roots);
roots = Collections.unmodifiableMap(copy);
if (densityFunctionBytes < 0L) {
throw new IllegalArgumentException("Density function JSON size cannot be negative, got " + densityFunctionBytes);
}
}
public static long densityFunctionBytes(Map<String, byte[]> files) {
long bytes = 0L;
for (Map.Entry<String, byte[]> file : files.entrySet()) {
String path = file.getKey();
if (path.startsWith(DATA_PREFIX) && path.contains(DENSITY_FUNCTION_FOLDER) && path.endsWith(JSON_SUFFIX)) {
bytes += file.getValue().length;
}
}
return bytes;
}
public Optional<RootCost> root(Root root) {
return Optional.ofNullable(roots.get(root));
}
public GenerationCost withDensityFunctionBytes(long bytes) {
return new GenerationCost(roots, bytes);
}
public enum Root {
TEMPERATURE("temperature", true),
VEGETATION("vegetation", true),
CONTINENTS("continents", true),
EROSION("erosion", true),
DEPTH("depth", true),
RIDGES("ridges", true),
CHUNK_SURFACE_LEVEL("chunk_surface_level", false),
FINAL_DENSITY("final_density", false),
AQUIFER_BARRIER("aquifer.barrier", false),
AQUIFER_FLOODEDNESS("aquifer.fluid_level_floodedness", false),
AQUIFER_SPREAD("aquifer.fluid_level_spread", false),
AQUIFER_LAVA("aquifer.lava", false),
AQUIFER_EXCLUSION("aquifer.exclusion", false),
AQUIFER_SURFACE_LEVEL("aquifer.surface_level", false);
private final String id;
private final boolean climate;
Root(String id, boolean climate) {
this.id = id;
this.climate = climate;
}
public String id() {
return id;
}
public boolean climate() {
return climate;
}
}
public record RootCost(double worstCaseNodes, double expandedNodes, long distinctNodes) {
public RootCost {
if (!(worstCaseNodes >= 0.0) || !(expandedNodes >= 0.0) || distinctNodes < 0L) {
throw new IllegalArgumentException("Generation cost counts cannot be negative, got " + worstCaseNodes + " worst case, "
+ expandedNodes + " expanded and " + distinctNodes + " distinct");
}
}
}
}
@@ -0,0 +1,186 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.Vector3i;
import art.arcane.volmlib.util.nbt.io.NBTUtil;
import art.arcane.volmlib.util.nbt.io.NamedTag;
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.IntTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.GZIPOutputStream;
public final class StructureTemplateEncoder {
private static final Comparator<TemplateBlock> BLOCK_ORDER = Comparator
.comparingInt(TemplateBlock::y)
.thenComparingInt(TemplateBlock::x)
.thenComparingInt(TemplateBlock::z);
public static TemplateVolume volume(IrisObject object, String resource, List<ExportDiagnostic> diagnostics) {
List<TemplateBlock> blocks = new ArrayList<>();
Vector3i center = object.getCenter();
int droppedTiles = 0;
String firstDropped = null;
int outside = 0;
String firstOutside = null;
Set<String> customBlocks = new TreeSet<>();
for (Map.Entry<IrisBlockVector, NativeBlockState> entry : object.getBlocks()) {
IrisBlockVector signed = entry.getKey();
int x = signed.getBlockX() + center.getX();
int y = signed.getBlockY() + center.getY();
int z = signed.getBlockZ() + center.getZ();
if (x < 0 || y < 0 || z < 0 || x >= object.getW() || y >= object.getH() || z >= object.getD()) {
outside++;
if (firstOutside == null) {
firstOutside = x + "," + y + "," + z;
}
continue;
}
VanillaBlockState state;
try {
state = VanillaBlockState.parse(entry.getValue().key());
} catch (IllegalArgumentException exception) {
customBlocks.add(entry.getValue().key());
continue;
}
TileData tile = object.getStates().get(signed);
CompoundTag nbt = null;
if (tile != null) {
Optional<CompoundTag> converted = TileEntityNbt.toNbt(tile.getMaterialKey(), tile.getProperties());
if (converted.isPresent()) {
nbt = converted.get();
} else {
droppedTiles++;
if (firstDropped == null) {
firstDropped = state.name() + " at " + x + "," + y + "," + z;
}
}
}
blocks.add(new TemplateBlock(x, y, z, state, nbt));
}
if (!customBlocks.isEmpty()) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.CUSTOM_BLOCK, resource,
"Blocks outside Minecraft are left out of the template: " + String.join(", ", customBlocks)));
}
if (outside > 0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.OBJECT_BOUNDS, resource,
outside + " blocks outside the " + object.getW() + "x" + object.getH() + "x" + object.getD()
+ " object size are left out of the template, first at " + firstOutside));
}
if (droppedTiles > 0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.TILE_ENTITY, resource,
droppedTiles + " tile entities have no vanilla block entity form and export as plain blocks, first "
+ firstDropped));
}
return new TemplateVolume(object.getW(), object.getH(), object.getD(), blocks);
}
public byte[] encode(IrisObject object, List<TemplateBlock> extraBlocks, DataVersion target) throws IOException {
List<ExportDiagnostic> lossy = new ArrayList<>();
TemplateVolume volume = volume(object, object.getLoadKey(), lossy);
if (!lossy.isEmpty()) {
throw new IllegalArgumentException("Object '" + object.getLoadKey() + "' cannot be encoded: " + lossy.getFirst().message());
}
Map<BlockPosition, TemplateBlock> blocks = new LinkedHashMap<>();
for (TemplateBlock block : volume.blocks()) {
blocks.put(new BlockPosition(block.x(), block.y(), block.z()), block);
}
for (TemplateBlock extra : extraBlocks) {
blocks.put(new BlockPosition(extra.x(), extra.y(), extra.z()), extra);
}
return encode(new TemplateVolume(volume.sizeX(), volume.sizeY(), volume.sizeZ(), new ArrayList<>(blocks.values())), target);
}
public byte[] encode(TemplateVolume volume, DataVersion target) throws IOException {
MCABlockStateCodecSupport.Format format = paletteFormat(target);
List<TemplateBlock> orderedBlocks = new ArrayList<>(volume.blocks());
orderedBlocks.sort(BLOCK_ORDER);
for (int index = 1; index < orderedBlocks.size(); index++) {
if (BLOCK_ORDER.compare(orderedBlocks.get(index - 1), orderedBlocks.get(index)) == 0) {
TemplateBlock duplicate = orderedBlocks.get(index);
throw new IllegalArgumentException("Template has two blocks at " + duplicate.x() + "," + duplicate.y() + "," + duplicate.z());
}
}
Map<String, Integer> paletteIndexes = new LinkedHashMap<>();
List<VanillaBlockState> paletteStates = new ArrayList<>();
for (TemplateBlock block : orderedBlocks) {
String key = block.state().canonical();
if (!paletteIndexes.containsKey(key)) {
paletteIndexes.put(key, paletteStates.size());
paletteStates.add(block.state());
}
}
CompoundTag root = new CompoundTag();
root.put("size", intList(volume.sizeX(), volume.sizeY(), volume.sizeZ()));
root.put("palette", palette(paletteStates, format));
root.put("blocks", blocks(orderedBlocks, paletteIndexes));
root.put("entities", new ListTag<>(CompoundTag.class));
root.putInt("DataVersion", target.getWorldDataVersion());
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(output)) {
NBTUtil.write(new NamedTag("", root), gzip, false);
}
return output.toByteArray();
}
private static MCABlockStateCodecSupport.Format paletteFormat(DataVersion target) {
return switch (target) {
case V26_3 -> MCABlockStateCodecSupport.Format.LOWERCASE;
case V1_21_1, V26_1_2, V26_2 -> MCABlockStateCodecSupport.Format.CAPITALIZED;
case UNSUPPORTED -> throw new IllegalArgumentException("Structure templates need a supported target version");
};
}
private ListTag<CompoundTag> palette(List<VanillaBlockState> states, MCABlockStateCodecSupport.Format format) {
ListTag<CompoundTag> palette = new ListTag<>(CompoundTag.class);
for (VanillaBlockState state : states) {
palette.add(state.toNbt(format));
}
return palette;
}
private ListTag<CompoundTag> blocks(List<TemplateBlock> entries, Map<String, Integer> paletteIndexes) {
ListTag<CompoundTag> blocks = new ListTag<>(CompoundTag.class);
for (TemplateBlock entry : entries) {
CompoundTag block = new CompoundTag();
block.put("pos", intList(entry.x(), entry.y(), entry.z()));
block.putInt("state", paletteIndexes.get(entry.state().canonical()));
if (entry.nbt() != null) {
block.put("nbt", entry.nbt());
}
blocks.add(block);
}
return blocks;
}
private ListTag<IntTag> intList(int... values) {
ListTag<IntTag> list = new ListTag<>(IntTag.class);
for (int value : values) {
list.add(new IntTag(value));
}
return list;
}
private record BlockPosition(int x, int y, int z) {
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import java.util.Objects;
public record TemplateBlock(int x, int y, int z, VanillaBlockState state, CompoundTag nbt) {
public TemplateBlock {
Objects.requireNonNull(state, "Template block state");
}
}
@@ -0,0 +1,22 @@
package art.arcane.iris.pack.datapack.export;
import java.util.List;
public record TemplateVolume(int sizeX, int sizeY, int sizeZ, List<TemplateBlock> blocks) {
public TemplateVolume {
if (sizeX < 0 || sizeY < 0 || sizeZ < 0) {
throw new IllegalArgumentException("Template size must not be negative, got " + sizeX + "x" + sizeY + "x" + sizeZ);
}
blocks = List.copyOf(blocks);
for (TemplateBlock block : blocks) {
if (block.x() < 0 || block.y() < 0 || block.z() < 0 || block.x() >= sizeX || block.y() >= sizeY || block.z() >= sizeZ) {
throw new IllegalArgumentException("Template block at " + block.x() + "," + block.y() + "," + block.z()
+ " lies outside the " + sizeX + "x" + sizeY + "x" + sizeZ + " template");
}
}
}
public int footprint() {
return Math.max(sizeX, sizeZ);
}
}
@@ -0,0 +1,324 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.volmlib.util.nbt.io.SNBTDeserializer;
import art.arcane.volmlib.util.nbt.tag.ByteTag;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.tag.DoubleTag;
import art.arcane.volmlib.util.nbt.tag.EndTag;
import art.arcane.volmlib.util.nbt.tag.FloatTag;
import art.arcane.volmlib.util.nbt.tag.IntArrayTag;
import art.arcane.volmlib.util.nbt.tag.IntTag;
import art.arcane.volmlib.util.nbt.tag.ListTag;
import art.arcane.volmlib.util.nbt.tag.LongTag;
import art.arcane.volmlib.util.nbt.tag.ShortTag;
import art.arcane.volmlib.util.nbt.tag.StringTag;
import art.arcane.volmlib.util.nbt.tag.Tag;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
public final class TileEntityNbt {
private static final String SNBT_PROPERTY = "nbt";
private static final String ID = "id";
private static final String PROFILE = "profile";
private static final String UUID = "UUID";
private static final int MAX_DEPTH = 64;
private static final Set<String> POSITION_KEYS = Set.of("x", "y", "z");
private static final Map<String, String> BLOCK_ENTITIES = Map.ofEntries(
Map.entry("minecraft:chest", "minecraft:chest"),
Map.entry("minecraft:trapped_chest", "minecraft:trapped_chest"),
Map.entry("minecraft:ender_chest", "minecraft:ender_chest"),
Map.entry("minecraft:barrel", "minecraft:barrel"),
Map.entry("minecraft:furnace", "minecraft:furnace"),
Map.entry("minecraft:smoker", "minecraft:smoker"),
Map.entry("minecraft:blast_furnace", "minecraft:blast_furnace"),
Map.entry("minecraft:dispenser", "minecraft:dispenser"),
Map.entry("minecraft:dropper", "minecraft:dropper"),
Map.entry("minecraft:hopper", "minecraft:hopper"),
Map.entry("minecraft:brewing_stand", "minecraft:brewing_stand"),
Map.entry("minecraft:beacon", "minecraft:beacon"),
Map.entry("minecraft:lectern", "minecraft:lectern"),
Map.entry("minecraft:jukebox", "minecraft:jukebox"),
Map.entry("minecraft:enchanting_table", "minecraft:enchanting_table"),
Map.entry("minecraft:conduit", "minecraft:conduit"),
Map.entry("minecraft:bell", "minecraft:bell"),
Map.entry("minecraft:crafter", "minecraft:crafter"),
Map.entry("minecraft:daylight_detector", "minecraft:daylight_detector"),
Map.entry("minecraft:comparator", "minecraft:comparator"),
Map.entry("minecraft:end_gateway", "minecraft:end_gateway"),
Map.entry("minecraft:end_portal", "minecraft:end_portal"),
Map.entry("minecraft:sculk_sensor", "minecraft:sculk_sensor"),
Map.entry("minecraft:calibrated_sculk_sensor", "minecraft:calibrated_sculk_sensor"),
Map.entry("minecraft:sculk_catalyst", "minecraft:sculk_catalyst"),
Map.entry("minecraft:sculk_shrieker", "minecraft:sculk_shrieker"),
Map.entry("minecraft:chiseled_bookshelf", "minecraft:chiseled_bookshelf"),
Map.entry("minecraft:decorated_pot", "minecraft:decorated_pot"),
Map.entry("minecraft:creaking_heart", "minecraft:creaking_heart"),
Map.entry("minecraft:trial_spawner", "minecraft:trial_spawner"),
Map.entry("minecraft:vault", "minecraft:vault"),
Map.entry("minecraft:beehive", "minecraft:beehive"),
Map.entry("minecraft:bee_nest", "minecraft:beehive"),
Map.entry("minecraft:spawner", "minecraft:mob_spawner"),
Map.entry("minecraft:campfire", "minecraft:campfire"),
Map.entry("minecraft:soul_campfire", "minecraft:campfire"),
Map.entry("minecraft:command_block", "minecraft:command_block"),
Map.entry("minecraft:chain_command_block", "minecraft:command_block"),
Map.entry("minecraft:repeating_command_block", "minecraft:command_block"),
Map.entry("minecraft:suspicious_sand", "minecraft:brushable_block"),
Map.entry("minecraft:suspicious_gravel", "minecraft:brushable_block"));
private static final List<Map.Entry<String, String>> SUFFIX_BLOCK_ENTITIES = List.of(
Map.entry("_hanging_sign", "minecraft:hanging_sign"),
Map.entry("_sign", "minecraft:sign"),
Map.entry("_banner", "minecraft:banner"),
Map.entry("_head", "minecraft:skull"),
Map.entry("_skull", "minecraft:skull"),
Map.entry("_bed", "minecraft:bed"),
Map.entry("shulker_box", "minecraft:shulker_box"),
Map.entry("_shelf", "minecraft:shelf"),
Map.entry("copper_golem_statue", "minecraft:copper_golem_statue"));
private static final Set<String> BYTE_FIELDS = Set.of(
"Slot", "Count", "has_glowing_text", "is_waxed", "auto", "conditionMet", "powered", "TrackOutput",
"UpdateLastExecution", "OnGround", "Invulnerable", "Silent", "NoGravity", "Glowing",
"PersistenceRequired", "CanPickUpLoot", "LeftHanded", "NoAI", "ExactTeleport", "show_particles",
"show_icon", "ambient");
private static final Set<String> SHORT_FIELDS = Set.of(
"Delay", "MinSpawnDelay", "MaxSpawnDelay", "SpawnCount", "MaxNearbyEntities", "RequiredPlayerRange",
"SpawnRange", "BurnTime", "CookTime", "CookTimeTotal", "lit_time_remaining", "lit_total_time",
"cooking_time_spent", "cooking_total_time", "Fire", "Air", "HurtTime", "DeathTime");
private static final Set<String> LONG_FIELDS = Set.of("LootTableSeed", "Age", "LastExecution", "ticks_since_song_started");
private static final Set<String> FLOAT_FIELDS = Set.of("Health", "AbsorptionAmount", "Rotation", "HandDropChances", "ArmorDropChances");
private TileEntityNbt() {
}
public static Optional<String> blockEntityId(String blockKey) {
String key = normalizeBlockKey(blockKey);
if (key.isEmpty()) {
return Optional.empty();
}
String exact = BLOCK_ENTITIES.get(key);
if (exact != null) {
return Optional.of(exact);
}
if (!key.startsWith("minecraft:")) {
return Optional.empty();
}
for (Map.Entry<String, String> suffix : SUFFIX_BLOCK_ENTITIES) {
if (key.endsWith(suffix.getKey())) {
return Optional.of(suffix.getValue());
}
}
return Optional.empty();
}
public static Optional<CompoundTag> toNbt(String blockKey, Map<String, Object> properties) {
Optional<String> blockEntity = blockEntityId(blockKey);
if (blockEntity.isEmpty() || properties == null) {
return Optional.empty();
}
CompoundTag nbt = parseSnbt(properties.get(SNBT_PROPERTY));
if (nbt == null) {
nbt = compound(properties, "", 0);
}
for (String key : POSITION_KEYS) {
nbt.remove(key);
}
nbt.remove(SNBT_PROPERTY);
if (nbt.size() == 0) {
return Optional.empty();
}
if (!nbt.containsKey(ID)) {
nbt.putString(ID, blockEntity.get());
}
return Optional.of(nbt);
}
private static String normalizeBlockKey(String blockKey) {
if (blockKey == null) {
return "";
}
String trimmed = blockKey.trim().toLowerCase(Locale.ROOT);
int properties = trimmed.indexOf('[');
String base = properties < 0 ? trimmed : trimmed.substring(0, properties);
if (base.isEmpty()) {
return "";
}
return base.indexOf(':') < 0 ? "minecraft:" + base : base;
}
private static CompoundTag parseSnbt(Object snbt) {
if (!(snbt instanceof String text) || text.isBlank()) {
return null;
}
try {
Tag<?> parsed = new SNBTDeserializer().fromString(text);
return parsed instanceof CompoundTag compound ? compound : null;
} catch (IOException | RuntimeException exception) {
return null;
}
}
private static CompoundTag compound(Map<?, ?> values, String parent, int depth) {
CompoundTag compound = new CompoundTag();
if (depth > MAX_DEPTH) {
return compound;
}
Map<String, Object> sorted = new TreeMap<>();
for (Map.Entry<?, ?> entry : values.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null) {
sorted.put(String.valueOf(entry.getKey()), entry.getValue());
}
}
if (depth == 0) {
sorted.remove(SNBT_PROPERTY);
}
for (Map.Entry<String, Object> entry : sorted.entrySet()) {
Tag<?> tag = tag(entry.getKey(), parent, entry.getValue(), depth + 1);
if (tag != null) {
compound.put(entry.getKey(), tag);
}
}
return compound;
}
private static Tag<?> tag(String key, String parent, Object value, int depth) {
if (value instanceof Map<?, ?> map) {
return compound(map, key, depth);
}
if (value instanceof List<?> list) {
return list(key, parent, list, depth);
}
if (value instanceof Boolean bool) {
return new ByteTag(bool);
}
if (value instanceof Number number) {
return number(numberKind(key, number), number);
}
return new StringTag(String.valueOf(value));
}
private static Tag<?> list(String key, String parent, List<?> values, int depth) {
if (intArray(key, parent) && allNumbers(values)) {
int[] ints = new int[values.size()];
for (int index = 0; index < ints.length; index++) {
ints[index] = ((Number) values.get(index)).intValue();
}
return new IntArrayTag(ints);
}
NumberKind listKind = allNumbers(values) ? listNumberKind(key, values) : null;
ListTag<?> list = ListTag.createUnchecked(EndTag.class);
Class<?> elementType = null;
for (Object value : values) {
if (value == null) {
continue;
}
Tag<?> element = listKind == null ? tag(key, parent, value, depth + 1) : number(listKind, (Number) value);
if (element == null) {
continue;
}
if (elementType == null) {
elementType = element.getClass();
}
if (elementType != element.getClass()) {
return stringList(values);
}
list.addUnchecked(element);
}
return list;
}
private static ListTag<?> stringList(List<?> values) {
ListTag<?> list = ListTag.createUnchecked(EndTag.class);
for (Object value : values) {
if (value != null) {
list.addUnchecked(new StringTag(String.valueOf(value)));
}
}
return list;
}
private static boolean intArray(String key, String parent) {
return UUID.equals(key) || (ID.equals(key) && PROFILE.equals(parent));
}
private static boolean allNumbers(List<?> values) {
if (values.isEmpty()) {
return false;
}
for (Object value : values) {
if (!(value instanceof Number)) {
return false;
}
}
return true;
}
private static NumberKind listNumberKind(String key, List<?> values) {
NumberKind declared = declaredKind(key);
if (declared != null) {
return declared;
}
NumberKind widest = NumberKind.INT;
for (Object value : values) {
NumberKind kind = inferredKind((Number) value);
if (kind.ordinal() > widest.ordinal()) {
widest = kind;
}
}
return widest;
}
private static NumberKind numberKind(String key, Number number) {
NumberKind declared = declaredKind(key);
return declared == null ? inferredKind(number) : declared;
}
private static NumberKind declaredKind(String key) {
if (BYTE_FIELDS.contains(key)) {
return NumberKind.BYTE;
}
if (SHORT_FIELDS.contains(key)) {
return NumberKind.SHORT;
}
if (LONG_FIELDS.contains(key)) {
return NumberKind.LONG;
}
if (FLOAT_FIELDS.contains(key)) {
return NumberKind.FLOAT;
}
return null;
}
private static NumberKind inferredKind(Number number) {
if (number instanceof Double || number instanceof Float) {
return NumberKind.DOUBLE;
}
long value = number.longValue();
return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE ? NumberKind.INT : NumberKind.LONG;
}
private static Tag<?> number(NumberKind kind, Number number) {
return switch (kind) {
case BYTE -> new ByteTag(number.byteValue());
case SHORT -> new ShortTag(number.shortValue());
case INT -> new IntTag(number.intValue());
case LONG -> new LongTag(number.longValue());
case FLOAT -> new FloatTag(number.floatValue());
case DOUBLE -> new DoubleTag(number.doubleValue());
};
}
private enum NumberKind {
BYTE,
SHORT,
INT,
LONG,
FLOAT,
DOUBLE
}
}
@@ -1,4 +1,4 @@
package art.arcane.iris.structure.export;
package art.arcane.iris.pack.datapack.export;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.volmlib.util.nbt.mca.MCABlockStateCodecSupport;
@@ -9,15 +9,15 @@ import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
record VanillaBlockState(String name, Map<String, String> properties) {
public record VanillaBlockState(String name, Map<String, String> properties) {
private static final Pattern PROPERTY_NAME = Pattern.compile("[a-z0-9_.-]+");
private static final Pattern PROPERTY_VALUE = Pattern.compile("[a-z0-9_.-]+");
VanillaBlockState {
public VanillaBlockState {
properties = Collections.unmodifiableMap(new LinkedHashMap<>(properties));
}
static VanillaBlockState parse(String value) {
public static VanillaBlockState parse(String value) {
if (value == null) {
throw new IllegalArgumentException("Block state must not be null");
}
@@ -58,7 +58,7 @@ record VanillaBlockState(String name, Map<String, String> properties) {
return new VanillaBlockState(name, properties);
}
String canonical() {
public String canonical() {
if (properties.isEmpty()) {
return name;
}
@@ -74,7 +74,7 @@ record VanillaBlockState(String name, Map<String, String> properties) {
return builder.append(']').toString();
}
CompoundTag toNbt(MCABlockStateCodecSupport.Format format) {
public CompoundTag toNbt(MCABlockStateCodecSupport.Format format) {
CompoundTag state = new CompoundTag();
state.putString(format.nameKey(), name);
if (!properties.isEmpty()) {
@@ -0,0 +1,104 @@
package art.arcane.iris.pack.datapack.export;
import java.util.Locale;
import java.util.StringJoiner;
import java.util.regex.Pattern;
public final class VanillaResourceIdentifier {
private static final Pattern NAMESPACE = Pattern.compile("[a-z0-9_.-]+");
private static final Pattern PATH = Pattern.compile("[a-z0-9/._-]+");
private static final Pattern INVALID_PATH_CHARACTER = Pattern.compile("[^a-z0-9._-]");
private VanillaResourceIdentifier() {
}
public static boolean validNamespace(String value) {
return value != null && NAMESPACE.matcher(value).matches();
}
public static boolean validPath(String value) {
if (value == null
|| !PATH.matcher(value).matches()
|| value.startsWith("/")
|| value.endsWith("/")
|| value.contains("//")) {
return false;
}
for (String segment : value.split("/")) {
if (segment.equals(".") || segment.equals("..")) {
return false;
}
}
return true;
}
public static boolean validIdentifier(String value) {
if (value == null) {
return false;
}
int separator = value.indexOf(':');
if (separator <= 0 || separator != value.lastIndexOf(':')) {
return false;
}
return validNamespace(value.substring(0, separator)) && validPath(value.substring(separator + 1));
}
public static String requireNamespace(String value, String role) {
if (!validNamespace(value)) {
throw invalid(value, role);
}
return value;
}
public static String requirePath(String value, String role) {
if (!validPath(value)) {
throw invalid(value, role);
}
return value;
}
public static String requireIdentifier(String value, String role) {
if (!validIdentifier(value)) {
throw invalid(value, role);
}
return value;
}
public static String sanitizePath(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Cannot derive a resource path from '" + value + "'");
}
String lowered = value.trim().replace('\\', '/').toLowerCase(Locale.ROOT);
StringJoiner path = new StringJoiner("/");
for (String segment : lowered.split("/")) {
if (segment.isEmpty()) {
continue;
}
String cleaned = INVALID_PATH_CHARACTER.matcher(segment).replaceAll("_");
path.add(cleaned.equals(".") || cleaned.equals("..") ? cleaned.replace('.', '_') : cleaned);
}
String result = path.toString();
if (result.isEmpty()) {
throw new IllegalArgumentException("Cannot derive a resource path from '" + value + "'");
}
return result;
}
public static String normalizeConnectorIdentifier(String value) {
if (value == null || value.isBlank()) {
return "minecraft:empty";
}
String normalized = value.trim();
if (normalized.indexOf(':') < 0) {
normalized = "minecraft:" + normalized;
}
if (!validIdentifier(normalized)) {
throw new IllegalArgumentException("Invalid resource identifier '" + value + "'");
}
return normalized;
}
private static IllegalArgumentException invalid(String value, String role) {
return new IllegalArgumentException("Invalid " + role + " '" + value + "'");
}
}
@@ -0,0 +1,170 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.PackCompiler;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseStyleCompiler;
import art.arcane.iris.pack.datapack.export.emit.UnusedResourcePruner;
import art.arcane.iris.pack.datapack.export.emit.WorldgenJsonWriter;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import art.arcane.iris.pack.datapack.export.lower.GenerationBudget;
import art.arcane.iris.pack.datapack.export.lower.GenerationCostCounter;
import art.arcane.iris.pack.datapack.export.lower.VanillaBounds;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisLogging;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.function.Function;
import java.util.regex.Pattern;
public final class WorldgenDatapackExporter implements WorldgenExportService {
private static final String STAGING_PREFIX = ".iris-worldgen-export-";
private static final Pattern UNSAFE_OUTPUT_CHARACTER = Pattern.compile("[^A-Za-z0-9._-]");
private final Function<DataVersion, WorldgenJsonWriter> writers;
private final PackCompiler compiler;
public WorldgenDatapackExporter(Function<DataVersion, WorldgenJsonWriter> writers, PackCompiler compiler) {
this.writers = Objects.requireNonNull(writers);
this.compiler = Objects.requireNonNull(compiler);
}
public static WorldgenDatapackExporter create() {
return new WorldgenDatapackExporter(WorldgenJsonWriter::forTarget, PackCompiler.create());
}
@Override
public WorldgenExportResult export(WorldgenExportRequest request, WorldgenExportProgress progress) {
Objects.requireNonNull(request);
Objects.requireNonNull(progress);
ExportPlan plan;
try {
plan = plan(request);
} catch (IllegalArgumentException exception) {
return WorldgenExportResult.rejected(null, exception.getMessage(), List.of());
}
Path destination = plan.destination();
if (Files.exists(destination) && !request.replace()) {
return rejected(destination, "Export output already exists: " + destination + ". Pass replace=true to overwrite it.");
}
if (!DatapackExportDestinations.claim(destination)) {
return rejected(destination, "An export to " + destination + " is already running.");
}
try {
return exportClaimed(request, plan, progress);
} finally {
DatapackExportDestinations.release(destination);
}
}
private ExportPlan plan(WorldgenExportRequest request) {
DataVersion target = WorldgenExportTargets.resolve(request.version());
WorldgenExportMode mode = WorldgenExportMode.parse(request.mode());
DatapackExportFormat format = DatapackExportDestinations.parseFormat(request.format());
String namespace = VanillaResourceIdentifier.requireNamespace(request.namespace(), "export namespace");
String output = request.output().isEmpty() ? defaultOutput(request.dimension(), target) : request.output();
return new ExportPlan(target, mode, format, namespace, DatapackExportDestinations.resolve(request.exportRoot(), output, format));
}
private WorldgenExportResult exportClaimed(WorldgenExportRequest request, ExportPlan plan, WorldgenExportProgress progress) {
Path destination = plan.destination();
File pack = request.pack().toFile();
try {
progress.stage("validate");
PackValidationResult validation = PackValidator.validateForPackaging(pack);
if (!validation.isLoadable()) {
return rejected(destination, "Pack '" + validation.getPackName() + "' failed validation: "
+ String.join("; ", validation.getBlockingErrors()));
}
progress.stage("load");
IrisData data = IrisData.openRuntime(pack);
try {
IrisDimension dimension = data.getDimensionLoader().load(request.dimension(), false);
if (dimension == null) {
return rejected(destination, "Dimension '" + request.dimension() + "' was not found in pack '" + pack.getName() + "'.");
}
PackCompileContext context = compileContext(data, dimension, plan, request.objects());
ExportIr ir = compiler.compile(context, progress);
return publish(request, plan, ir, context.biomeCodes().loadKeyByBiomeId(), progress);
} finally {
data.close();
}
} catch (IOException exception) {
IrisLogging.reportError("Worldgen datapack publication to " + destination + " failed", exception);
return WorldgenExportResult.failed(destination, "Datapack publication failed: " + exception.getMessage(), List.of());
} catch (RuntimeException exception) {
IrisLogging.reportError("Worldgen datapack export of dimension '" + request.dimension() + "' failed", exception);
return WorldgenExportResult.failed(destination, "Datapack export failed: " + exception.getMessage(), List.of());
}
}
private WorldgenExportResult publish(WorldgenExportRequest request, ExportPlan plan, ExportIr ir, Map<String, String> loadKeyByBiomeId,
WorldgenExportProgress progress) throws IOException {
progress.stage("write");
GenerationCost routerCost = GenerationCostCounter.count(ir.router(), VanillaBounds.of(ir.namespace(), ir.noises()), ir.target());
SortedMap<String, byte[]> files = writers.apply(plan.target()).write(ir);
progress.resources(files.size(), files.size());
progress.stage("prune");
UnusedResourcePruner.Pruned pruned = UnusedResourcePruner.prune(files);
progress.trimmed(pruned.trim());
GenerationCost cost = routerCost.withDensityFunctionBytes(GenerationCost.densityFunctionBytes(pruned.files()));
progress.stage("publish");
AtomicDatapackPublisher.Publication publication = new AtomicDatapackPublisher().publish(
pruned.files(),
plan.destination(),
plan.format(),
request.replace(),
STAGING_PREFIX);
List<ExportDiagnostic> costDiagnostics = GenerationBudget.check(cost, ir.dimension().name());
List<ExportDiagnostic> diagnostics = new ArrayList<>(ir.diagnostics().size() + pruned.diagnostics().size() + costDiagnostics.size());
diagnostics.addAll(ir.diagnostics());
diagnostics.addAll(costDiagnostics);
diagnostics.addAll(pruned.diagnostics());
return new WorldgenExportResult(
WorldgenExportResult.Status.EXPORTED,
plan.destination(),
String.join("; ", publication.cleanupWarnings()),
diagnostics,
loadKeyByBiomeId,
pruned.trim(),
cost);
}
private static PackCompileContext compileContext(IrisData data, IrisDimension dimension, ExportPlan plan, boolean objects) {
List<ExportDiagnostic> diagnostics = new ArrayList<>();
return PackCompileContext.builder()
.data(data)
.dimension(dimension)
.capabilities(ExportCapabilities.forTarget(plan.target()))
.namespace(plan.namespace())
.mode(plan.mode())
.objects(objects)
.diagnostics(diagnostics)
.styles(NoiseStyleCompiler.create(diagnostics))
.build();
}
private static String defaultOutput(String dimension, DataVersion target) {
return UNSAFE_OUTPUT_CHARACTER.matcher(dimension.replace('/', '-')).replaceAll("-") + "-" + target.getVersion();
}
private static WorldgenExportResult rejected(Path destination, String message) {
return WorldgenExportResult.rejected(destination, message, List.of());
}
private record ExportPlan(DataVersion target, WorldgenExportMode mode, DatapackExportFormat format, String namespace, Path destination) {
}
}
@@ -0,0 +1,21 @@
package art.arcane.iris.pack.datapack.export;
import java.util.Locale;
public enum WorldgenExportMode {
PRESET,
DIMENSION,
OVERWORLD;
public static WorldgenExportMode parse(String raw) {
if (raw == null || raw.isBlank()) {
return OVERWORLD;
}
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
case "preset" -> PRESET;
case "dimension" -> DIMENSION;
case "overworld" -> OVERWORLD;
default -> throw new IllegalArgumentException("Unknown export mode '" + raw + "'. Expected: preset, dimension, overworld");
};
}
}
@@ -0,0 +1,20 @@
package art.arcane.iris.pack.datapack.export;
public interface WorldgenExportProgress {
WorldgenExportProgress NONE = new WorldgenExportProgress() {
@Override
public void stage(String stage) {
}
@Override
public void resources(int done, int total) {
}
};
void stage(String stage);
void resources(int done, int total);
default void trimmed(ContentTrim trim) {
}
}
@@ -0,0 +1,112 @@
package art.arcane.iris.pack.datapack.export;
import java.nio.file.Path;
import java.util.Objects;
public record WorldgenExportRequest(
Path pack,
String dimension,
String version,
String mode,
String namespace,
String output,
String format,
boolean objects,
boolean replace,
Path exportRoot
) {
public static final String DEFAULT_MODE = "overworld";
public static final String DEFAULT_NAMESPACE = "iris_export";
public static final String DEFAULT_FORMAT = "zip";
public WorldgenExportRequest {
if (pack == null) {
throw new IllegalArgumentException("Worldgen export request requires a pack directory");
}
if (dimension == null || dimension.isBlank()) {
throw new IllegalArgumentException("Worldgen export request requires a dimension");
}
if (exportRoot == null) {
throw new IllegalArgumentException("Worldgen export request requires an export root");
}
Objects.requireNonNull(version);
Objects.requireNonNull(mode);
Objects.requireNonNull(namespace);
Objects.requireNonNull(output);
Objects.requireNonNull(format);
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private Path pack;
private String dimension = "";
private String version = "";
private String mode = DEFAULT_MODE;
private String namespace = DEFAULT_NAMESPACE;
private String output = "";
private String format = DEFAULT_FORMAT;
private boolean objects = true;
private boolean replace;
private Path exportRoot;
private Builder() {
}
public Builder pack(Path value) {
pack = Objects.requireNonNull(value);
return this;
}
public Builder dimension(String value) {
dimension = Objects.requireNonNull(value).trim();
return this;
}
public Builder version(String value) {
version = Objects.requireNonNull(value).trim();
return this;
}
public Builder mode(String value) {
mode = Objects.requireNonNull(value).trim();
return this;
}
public Builder namespace(String value) {
namespace = Objects.requireNonNull(value).trim();
return this;
}
public Builder output(String value) {
output = Objects.requireNonNull(value).trim();
return this;
}
public Builder format(String value) {
format = Objects.requireNonNull(value).trim();
return this;
}
public Builder objects(boolean value) {
objects = value;
return this;
}
public Builder replace(boolean value) {
replace = value;
return this;
}
public Builder exportRoot(Path value) {
exportRoot = Objects.requireNonNull(value);
return this;
}
public WorldgenExportRequest build() {
return new WorldgenExportRequest(pack, dimension, version, mode, namespace, output, format, objects, replace, exportRoot);
}
}
}
@@ -0,0 +1,48 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
public record WorldgenExportResult(Status status, Path output, String message, List<ExportDiagnostic> diagnostics,
Map<String, String> loadKeyByBiomeId, ContentTrim trim, GenerationCost cost) {
public WorldgenExportResult {
Objects.requireNonNull(status);
if (status == Status.EXPORTED) {
Objects.requireNonNull(output);
}
message = message == null ? "" : message;
diagnostics = List.copyOf(diagnostics);
loadKeyByBiomeId = Collections.unmodifiableSortedMap(new TreeMap<>(loadKeyByBiomeId));
Objects.requireNonNull(trim);
Objects.requireNonNull(cost);
}
public static WorldgenExportResult exported(Path output, List<ExportDiagnostic> diagnostics, Map<String, String> loadKeyByBiomeId,
ContentTrim trim, GenerationCost cost) {
return new WorldgenExportResult(Status.EXPORTED, output, "", diagnostics, loadKeyByBiomeId, trim, cost);
}
public static WorldgenExportResult rejected(Path output, String message, List<ExportDiagnostic> diagnostics) {
return new WorldgenExportResult(Status.REJECTED, output, message, diagnostics, Map.of(), ContentTrim.NONE, GenerationCost.NONE);
}
public static WorldgenExportResult failed(Path output, String message, List<ExportDiagnostic> diagnostics) {
return new WorldgenExportResult(Status.FAILED, output, message, diagnostics, Map.of(), ContentTrim.NONE, GenerationCost.NONE);
}
public boolean isSuccess() {
return status == Status.EXPORTED;
}
public enum Status {
EXPORTED,
REJECTED,
FAILED
}
}
@@ -0,0 +1,5 @@
package art.arcane.iris.pack.datapack.export;
public interface WorldgenExportService {
WorldgenExportResult export(WorldgenExportRequest request, WorldgenExportProgress progress);
}
@@ -0,0 +1,23 @@
package art.arcane.iris.pack.datapack.export;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public final class WorldgenExportServices {
private static final AtomicReference<WorldgenExportService> SERVICE = new AtomicReference<>();
private WorldgenExportServices() {
}
public static void install(WorldgenExportService service) {
SERVICE.set(Objects.requireNonNull(service, "Datapack export service"));
}
public static WorldgenExportService get() {
WorldgenExportService service = SERVICE.get();
if (service != null) {
return service;
}
return SERVICE.updateAndGet(current -> current == null ? WorldgenDatapackExporter.create() : current);
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.pack.datapack.export;
import art.arcane.iris.pack.datapack.DataVersion;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
public final class WorldgenExportTargets {
private static final DataVersion OLDEST = DataVersion.V26_3;
public static final List<DataVersion> SUPPORTED = supported();
private WorldgenExportTargets() {
}
public static DataVersion resolve(String raw) {
if (raw == null || raw.isBlank()) {
return SUPPORTED.getLast();
}
DataVersion version = DataVersion.forMinecraftVersion(raw.trim());
if (!SUPPORTED.contains(version)) {
throw unsupported(raw);
}
return version;
}
public static IllegalArgumentException unsupported(String raw) {
StringJoiner supported = new StringJoiner(", ");
for (DataVersion version : SUPPORTED) {
supported.add(version.getVersion());
}
return new IllegalArgumentException("Unsupported datapack export version '" + raw + "'. Supported: " + supported);
}
private static List<DataVersion> supported() {
List<DataVersion> versions = new ArrayList<>();
for (DataVersion version : DataVersion.values()) {
if (version.ordinal() >= OLDEST.ordinal()) {
versions.add(version);
}
}
return List.copyOf(versions);
}
}
@@ -0,0 +1,128 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.pack.ContentKeyValidator;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.validation.ContentGate;
import art.arcane.iris.pack.validation.LegacyBlockRenames;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public final class BlockStateResolver {
private final IrisData data;
private final PlatformRegistries registries;
private final Set<String> knownBlocks;
private final List<ExportDiagnostic> diagnostics;
private final Map<String, Resolution> resolutions = new HashMap<>();
public BlockStateResolver(IrisData data, PlatformRegistries registries, List<ExportDiagnostic> diagnostics) {
this.data = Objects.requireNonNull(data);
this.registries = registries;
this.knownBlocks = registries == null ? Set.of() : knownBlocks(registries.blockTypeKeys());
this.diagnostics = Objects.requireNonNull(diagnostics);
}
public static BlockStateResolver forContext(PackCompileContext context) {
PlatformRegistries registries = IrisPlatforms.isBound() ? IrisPlatforms.get().registries() : null;
return new BlockStateResolver(context.data(), registries, context.diagnostics());
}
public Optional<VanillaBlockState> resolve(IrisBlockData block, String resource) {
if (block == null || block.getBlock() == null || block.getBlock().isBlank()) {
return Optional.empty();
}
return resolve(stateKey(block), resource);
}
public Optional<VanillaBlockState> resolve(String rawState, String resource) {
String normalized = ContentGate.normalizeState(rawState);
if (normalized == null) {
return Optional.empty();
}
Resolution resolution = resolutions.computeIfAbsent(normalized, this::parse);
if (resolution.problem() != null) {
ExportDiagnostic diagnostic = new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.BLOCK_STATE, resource, resolution.problem());
if (!diagnostics.contains(diagnostic)) {
diagnostics.add(diagnostic);
}
}
return Optional.ofNullable(resolution.state());
}
private String stateKey(IrisBlockData block) {
IrisBlockData custom = data.getBlockLoader() == null ? null : data.getBlockLoader().load(block.getBlock(), false);
if (custom == null || custom.getBlock() == null || custom.getBlock().equals(block.getBlock())) {
return block.stateKey();
}
IrisBlockData merged = IrisBlockData.from(custom.stateKey());
merged.getData().putAll(block.getData());
return merged.stateKey();
}
private Resolution parse(String normalized) {
String renamed = renamed(normalized);
if (!renamed.startsWith("minecraft:")) {
return new Resolution(null, "Block '" + renamed + "' is not a Minecraft block and is not exported");
}
String problem = null;
String validated = renamed;
List<String> propertyProblems = registries == null ? List.of() : ContentKeyValidator.validateBlockStateProperties(registries, List.of(renamed));
if (!propertyProblems.isEmpty()) {
validated = ContentGate.baseKey(renamed);
problem = String.join("; ", propertyProblems) + "; the block exports in its default state";
}
try {
return new Resolution(VanillaBlockState.parse(validated), problem);
} catch (IllegalArgumentException exception) {
return new Resolution(null, "Block state '" + renamed + "' is not exported: " + exception.getMessage());
}
}
private String renamed(String normalized) {
if (knownBlocks.isEmpty() || knownBlocks.contains(ContentGate.baseKey(normalized))) {
return normalized;
}
String properties = normalized.substring(ContentGate.baseKey(normalized).length());
String current = normalized;
for (int hop = 0; hop < LegacyBlockRenames.MAX_HOPS; hop++) {
String supplement = LegacyBlockRenames.supplementFor(current);
if (supplement == null) {
return normalized;
}
String next = ContentGate.normalizeState(supplement);
if (next == null) {
return normalized;
}
if (knownBlocks.contains(ContentGate.baseKey(next))) {
return ContentGate.baseKey(next) + properties;
}
current = next;
}
return normalized;
}
private static Set<String> knownBlocks(List<String> keys) {
Set<String> known = new HashSet<>(keys.size());
for (String key : keys) {
String normalized = ContentGate.normalizeState(key);
if (normalized != null) {
known.add(ContentGate.baseKey(normalized));
}
}
return known;
}
private record Resolution(VanillaBlockState state, String problem) {
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.pack.datapack.export.compile;
public interface CompileStage {
String name();
void apply(PackCompileContext context);
}
@@ -0,0 +1,75 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionType;
import art.arcane.iris.generation.terrain.IrisDimensionTypeOptions;
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.util.List;
import java.util.Locale;
public final class DimensionStage implements CompileStage {
private static final String DEFAULT_ROCK = "minecraft:stone";
private static final String DEFAULT_FLUID = "minecraft:water";
private static final float FULLBRIGHT_AMBIENT_LIGHT = 1.0f;
@Override
public String name() {
return "dimension";
}
@Override
public void apply(PackCompileContext context) {
IrisDimension dimension = context.dimension();
int minY = dimension.getMinHeight();
DimensionDef aligned = DimensionDef.aligned(
VanillaResourceIdentifier.sanitizePath(dimension.getLoadKey()),
minY,
dimension.getMaxHeight() - minY,
dimension.getLogicalHeight(),
dimension.getFluidHeight() + minY,
paletteBlock(dimension.getRockPalette(), DEFAULT_ROCK, "rock"),
paletteBlock(dimension.getFluidPalette(), DEFAULT_FLUID, "fluid"),
new JsonObject(),
context.diagnostics());
IrisDimensionType type = new IrisDimensionType(
dimension.getBaseDimension(),
typeOptions(dimension),
aligned.logicalHeight(),
aligned.height(),
aligned.minY());
JsonObject typeJson = JsonParser.parseString(type.toJson(context.capabilities().target().get())).getAsJsonObject();
context.setDimensionDef(new DimensionDef(
aligned.name(),
aligned.minY(),
aligned.height(),
aligned.logicalHeight(),
aligned.fluidHeight(),
aligned.defaultBlock(),
aligned.defaultFluid(),
typeJson));
}
private static IrisDimensionTypeOptions typeOptions(IrisDimension dimension) {
IrisDimensionTypeOptions options = dimension.getDimensionOptions();
return dimension.isFullbright() ? options.copy().ambientLight(FULLBRIGHT_AMBIENT_LIGHT) : options;
}
private static String paletteBlock(IrisMaterialPalette palette, String fallback, String role) {
List<IrisBlockData> entries = palette == null ? null : palette.getPalette();
if (entries == null || entries.isEmpty() || entries.getFirst() == null) {
return fallback;
}
String raw = entries.getFirst().getBlock();
String block = raw == null ? "" : raw.trim().toLowerCase(Locale.ROOT);
int properties = block.indexOf('[');
String id = properties < 0 ? block : block.substring(0, properties);
String namespaced = id.indexOf(':') < 0 ? "minecraft:" + id : id;
return VanillaResourceIdentifier.requireIdentifier(namespaced, "dimension " + role + " block");
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public final class ExportedBiomes {
private final SortedMap<String, IrisBiome> byId;
private final Map<String, String> loadKeys;
private final Map<String, Integer> codeOrder;
private final Map<String, IrisRegion> regionByLoadKey;
private ExportedBiomes(SortedMap<String, IrisBiome> byId, Map<String, String> loadKeys, Map<String, Integer> codeOrder,
Map<String, IrisRegion> regionByLoadKey) {
this.byId = Collections.unmodifiableSortedMap(byId);
this.loadKeys = Map.copyOf(loadKeys);
this.codeOrder = Map.copyOf(codeOrder);
this.regionByLoadKey = Map.copyOf(regionByLoadKey);
}
public static ExportedBiomes load(PackCompileContext context) {
SortedMap<String, IrisBiome> byId = new TreeMap<>();
Map<String, String> loadKeys = new HashMap<>();
for (Map.Entry<String, String> entry : context.biomeCodes().loadKeyByBiomeId().entrySet()) {
IrisBiome biome = Objects.requireNonNull(context.data().getBiomeLoader().load(entry.getValue(), false),
"Biome '" + entry.getValue() + "' could not be loaded");
byId.put(entry.getKey(), biome);
loadKeys.put(entry.getKey(), entry.getValue());
}
return new ExportedBiomes(byId, loadKeys, codeOrder(context.biomeCodes().layout(), byId.keySet()), regions(context));
}
public SortedMap<String, IrisBiome> byId() {
return byId;
}
public SortedSet<String> ids() {
return new TreeSet<>(byId.keySet());
}
public SortedSet<String> idsFor(Collection<String> loadKeys) {
SortedSet<String> ids = new TreeSet<>();
for (Map.Entry<String, String> entry : this.loadKeys.entrySet()) {
if (loadKeys.contains(entry.getValue())) {
ids.add(entry.getKey());
}
}
return ids;
}
public String loadKey(String biomeId) {
return loadKeys.get(biomeId);
}
public String resource(String biomeId) {
return "biomes/" + loadKeys.get(biomeId);
}
public IrisRegion region(String biomeId) {
return regionByLoadKey.get(loadKeys.get(biomeId));
}
public List<String> inCodeOrder(Iterable<String> biomeIds) {
List<String> ordered = new ArrayList<>();
for (String biomeId : biomeIds) {
ordered.add(biomeId);
}
ordered.sort(Comparator.comparingInt((String id) -> codeOrder.getOrDefault(id, Integer.MAX_VALUE)).thenComparing(Comparator.naturalOrder()));
return ordered;
}
private static Map<String, Integer> codeOrder(ClimateLayout layout, Iterable<String> biomeIds) {
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>(layout.entries());
entries.sort(Comparator.comparingInt((ClimateLayout.ClimateEntry entry) -> entry.axis().ordinal())
.thenComparingDouble(ClimateLayout.ClimateEntry::min)
.thenComparing(ClimateLayout.ClimateEntry::biomeId));
Map<String, Integer> order = new LinkedHashMap<>();
for (ClimateLayout.ClimateEntry entry : entries) {
order.putIfAbsent(entry.biomeId(), order.size());
}
for (String biomeId : biomeIds) {
order.putIfAbsent(biomeId, order.size());
}
return order;
}
private static Map<String, IrisRegion> regions(PackCompileContext context) {
Map<String, IrisRegion> regions = new HashMap<>();
for (IrisRegion region : context.dimension().getAllRegions(() -> context.data())) {
if (region == null) {
continue;
}
for (IrisBiome biome : region.getAllBiomes(() -> context.data())) {
if (biome != null && biome.getLoadKey() != null) {
regions.putIfAbsent(biome.getLoadKey(), region);
}
}
}
return regions;
}
}
@@ -0,0 +1,117 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public final class ImageMapStage implements CompileStage {
private static final String DIMENSION_MESSAGE = "Image maps are not exported; the dimension generates from its procedural layers";
private static final String STYLE_MESSAGE = "Generator style image maps are not exported; the base noise style is used";
@Override
public String name() {
return "image-maps";
}
@Override
public void apply(PackCompileContext context) {
IrisDimension dimension = context.dimension();
IrisData data = context.data();
Gson gson = data.getGson();
if (usesImageMaps(gson.toJsonTree(dimension))) {
report(context, "dimensions/" + dimension.getLoadKey(), DIMENSION_MESSAGE);
}
dimension.setImageMaps(new KList<>());
SortedMap<String, IrisRegion> regions = new TreeMap<>();
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
regions.put(region.getLoadKey(), region);
}
reportStyles(context, gson, "regions/", regions);
SortedMap<String, IrisBiome> biomes = new TreeMap<>();
SortedSet<String> generatorKeys = new TreeSet<>();
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
biomes.put(biome.getLoadKey(), biome);
collectGenerators(biome.getGenerators(), generatorKeys);
}
reportStyles(context, gson, "biomes/", biomes);
SortedMap<String, IrisGenerator> generators = new TreeMap<>();
for (String key : generatorKeys) {
IrisGenerator generator = data.getGeneratorLoader().load(key, false);
if (generator != null) {
generators.put(key, generator);
}
}
reportStyles(context, gson, "generators/", generators);
}
private static void collectGenerators(List<IrisBiomeGeneratorLink> links, SortedSet<String> generatorKeys) {
if (links == null) {
return;
}
for (IrisBiomeGeneratorLink link : links) {
if (link != null && link.getGenerator() != null && !link.getGenerator().isBlank()) {
generatorKeys.add(link.getGenerator().trim());
}
}
}
private static void reportStyles(PackCompileContext context, Gson gson, String folder, SortedMap<String, ?> resources) {
for (Map.Entry<String, ?> entry : resources.entrySet()) {
if (usesImageMaps(gson.toJsonTree(entry.getValue()))) {
report(context, folder + entry.getKey(), STYLE_MESSAGE);
}
}
}
private static void report(PackCompileContext context, String resource, String message) {
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.IMAGE_MAP, resource, message));
}
private static boolean usesImageMaps(JsonElement element) {
if (element.isJsonArray()) {
for (JsonElement child : element.getAsJsonArray()) {
if (usesImageMaps(child)) {
return true;
}
}
return false;
}
if (!element.isJsonObject()) {
return false;
}
JsonObject object = element.getAsJsonObject();
if (namesImageMap(object.get("imageMap")) || bindsImageMaps(object.get("imageMaps"))) {
return true;
}
for (Map.Entry<String, JsonElement> member : object.entrySet()) {
if (usesImageMaps(member.getValue())) {
return true;
}
}
return false;
}
private static boolean namesImageMap(JsonElement value) {
return value != null && value.isJsonPrimitive() && !value.getAsString().isBlank();
}
private static boolean bindsImageMaps(JsonElement value) {
return value != null && value.isJsonArray() && !value.getAsJsonArray().isEmpty();
}
}
@@ -0,0 +1,390 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.WorldgenExportMode;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeDecision;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeIdSanitizer;
import art.arcane.iris.pack.datapack.export.compile.decoration.BiomeFeatureLists;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
import art.arcane.iris.pack.datapack.export.ir.BinaryResource;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.datapack.export.ir.MaterialProgram;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.validation.ContentGate;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public final class PackCompileContext {
private static final Field NEUTRAL_VOLUME = new Field.Const(0.0);
private static final Field NO_CAVES = new Field.Const(1.0e6);
private static final Field ZERO_CODE = new Field.Const(0.0);
private static final double CLIMATE_SPAN = 1.0;
private static final List<String> LEGACY_BIOME_KEYS = List.of("surface_builder", "depth", "scale", "precipitation", "category", "starts");
private static final StyleCompiler NO_STYLE_COMPILER = (style, seedSalt, registry, capabilities) -> {
throw new IllegalStateException("No style compiler installed");
};
private final IrisData data;
private final IrisDimension dimension;
private final ExportCapabilities capabilities;
private final String namespace;
private final WorldgenExportMode mode;
private final boolean objects;
private final StyleCompiler styles;
private final NoiseRegistry noises;
private final List<ExportDiagnostic> diagnostics;
private final BiomeFeatureLists features = new BiomeFeatureLists();
private final List<JsonResource> jsonResources = new ArrayList<>();
private final List<BinaryResource> binaryResources = new ArrayList<>();
private DimensionDef dimensionDef;
private BiomeCodes biomeCodes;
private BiomeDecision biomeDecision;
private BiomeCodes boundBiomeCodes;
private Field boundHeight;
private List<BiomeDef> biomes;
private Field height;
private Field carvedHeight;
private Field climateHeight;
private Field surfaceEstimate;
private Field volumetric;
private Field caves;
private Field.Shared caveField;
private double wallShellBand;
private Field.Shared deepCaves;
private AquiferDef aquifer;
private MaterialProgram material;
private DefaultBiome defaultBiome;
private PackCompileContext(Builder builder) {
data = Objects.requireNonNull(builder.data, "Pack compile context requires pack data");
dimension = Objects.requireNonNull(builder.dimension, "Pack compile context requires a dimension");
capabilities = Objects.requireNonNull(builder.capabilities, "Pack compile context requires export capabilities");
namespace = VanillaResourceIdentifier.requireNamespace(builder.namespace, "export namespace");
mode = Objects.requireNonNull(builder.mode, "Pack compile context requires an export mode");
objects = builder.objects;
styles = builder.styles;
diagnostics = builder.diagnostics;
noises = new NoiseRegistry(namespace);
}
public static Builder builder() {
return new Builder();
}
public IrisData data() {
return data;
}
public IrisDimension dimension() {
return dimension;
}
public ExportCapabilities capabilities() {
return capabilities;
}
public String namespace() {
return namespace;
}
public WorldgenExportMode mode() {
return mode;
}
public boolean objects() {
return objects;
}
public StyleCompiler styles() {
return styles;
}
public NoiseRegistry noises() {
return noises;
}
public List<ExportDiagnostic> diagnostics() {
return diagnostics;
}
public BiomeFeatureLists features() {
return features;
}
public List<JsonResource> jsonResources() {
return jsonResources;
}
public List<BinaryResource> binaryResources() {
return binaryResources;
}
public DimensionDef dimensionDef() {
if (dimensionDef == null) {
throw new IllegalStateException("Dimension definition for '" + dimension.getLoadKey() + "' is read before the dimension stage ran");
}
return dimensionDef;
}
public void setDimensionDef(DimensionDef value) {
dimensionDef = Objects.requireNonNull(value);
}
public BiomeCodes biomeCodes() {
if (biomeDecision != null) {
return height == null ? biomeDecision.base() : heightBoundBiomeCodes();
}
return biomeCodes == null ? defaultBiome().codes() : biomeCodes;
}
public BiomeCodes baseBiomeCodes() {
if (biomeDecision != null) {
return biomeDecision.base();
}
return biomeCodes == null ? defaultBiome().codes() : biomeCodes;
}
public void setBiomeCodes(BiomeCodes value) {
biomeCodes = Objects.requireNonNull(value);
biomeDecision = null;
}
public void setBiomeDecision(BiomeDecision value) {
biomeDecision = Objects.requireNonNull(value);
biomeCodes = null;
boundBiomeCodes = null;
boundHeight = null;
}
public List<BiomeDef> biomes() {
return biomes == null ? defaultBiome().biomes() : biomes;
}
public void setBiomes(List<BiomeDef> value) {
biomes = List.copyOf(value);
}
public Field height() {
return carvedHeight == null ? naturalHeight() : carvedHeight;
}
public Field naturalHeight() {
return height == null ? new Field.Const(dimensionDef().fluidHeight()) : height;
}
public void setHeight(Field value) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "height");
height = value;
carvedHeight = null;
climateHeight = null;
surfaceEstimate = null;
}
public Field climateHeight() {
return climateHeight == null ? naturalHeight() : climateHeight;
}
public void setClimateHeight(Field value) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "climate height");
climateHeight = value;
}
public void setCarvedHeight(Field value) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "carved height");
carvedHeight = value;
}
public Field surfaceEstimate() {
return surfaceEstimate == null ? climateHeight() : surfaceEstimate;
}
public void setSurfaceEstimate(Field value) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(value), "surface estimate");
surfaceEstimate = value;
}
public Field volumetric() {
return volumetric == null ? NEUTRAL_VOLUME : volumetric;
}
public void setVolumetric(Field value) {
volumetric = Objects.requireNonNull(value);
}
public Field caves() {
return caves == null ? NO_CAVES : caves;
}
public void setCaves(Field value) {
caves = Objects.requireNonNull(value);
}
public Optional<Field.Shared> caveField() {
return Optional.ofNullable(caveField);
}
public void setCaveField(Field.Shared value, double wallBand) {
if (!(wallBand >= 0.0) || Double.isInfinite(wallBand)) {
throw new IllegalArgumentException("Wall shell band must be finite and non-negative, got " + wallBand);
}
caveField = Objects.requireNonNull(value);
wallShellBand = wallBand;
}
public double wallShellBand() {
return wallShellBand;
}
public Optional<Field.Shared> deepCaves() {
return Optional.ofNullable(deepCaves);
}
public void setDeepCaves(Field.Shared value) {
deepCaves = Objects.requireNonNull(value);
}
public AquiferDef aquifer() {
return aquifer == null ? AquiferDef.dry(surfaceEstimate()) : aquifer;
}
public void setAquifer(AquiferDef value) {
aquifer = Objects.requireNonNull(value);
}
public MaterialProgram material() {
return material == null ? MaterialProgram.rock(dimensionDef().defaultBlock()) : material;
}
public void setMaterial(MaterialProgram value) {
material = Objects.requireNonNull(value);
}
private BiomeCodes heightBoundBiomeCodes() {
Field bindingHeight = climateHeight();
if (boundHeight != bindingHeight) {
boundBiomeCodes = biomeDecision.bind(bindingHeight);
boundHeight = bindingHeight;
}
return boundBiomeCodes;
}
private DefaultBiome defaultBiome() {
if (defaultBiome == null) {
defaultBiome = createDefaultBiome();
}
return defaultBiome;
}
private DefaultBiome createDefaultBiome() {
String loadKey = firstLandBiome();
String id = BiomeIdSanitizer.sanitize(namespace, loadKey, new HashSet<>());
String body = new IrisBiomeCustom().generateJson(capabilities.target().get(), new ContentGate(null, Map.of(), null));
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
for (String key : LEGACY_BIOME_KEYS) {
json.remove(key);
}
BiomeDef biome = new BiomeDef(id.substring(namespace.length() + 1), json);
ClimateLayout layout = new ClimateLayout(List.of(
new ClimateLayout.ClimateEntry(id, ClimateLayout.ClimateAxis.TEMPERATURE, -CLIMATE_SPAN, CLIMATE_SPAN)));
BiomeCodes codes = new BiomeCodes(ZERO_CODE, ZERO_CODE, ZERO_CODE, layout, Map.of(id, loadKey));
return new DefaultBiome(List.of(biome), codes);
}
private String firstLandBiome() {
for (String regionKey : dimension.getRegions()) {
IrisRegion region = data.getRegionLoader().load(regionKey, false);
if (region == null || region.isCompatExcluded()) {
continue;
}
for (String biomeKey : region.getLandBiomes()) {
IrisBiome biome = biomeKey == null ? null : data.getBiomeLoader().load(biomeKey, false);
if (biome != null && !biome.isCompatExcluded()) {
return biome.getLoadKey();
}
}
}
throw new IllegalStateException("Dimension '" + dimension.getLoadKey() + "' has no exportable land biome in any of its regions");
}
private record DefaultBiome(List<BiomeDef> biomes, BiomeCodes codes) {
}
public static final class Builder {
private IrisData data;
private IrisDimension dimension;
private ExportCapabilities capabilities;
private String namespace;
private WorldgenExportMode mode = WorldgenExportMode.OVERWORLD;
private boolean objects = true;
private StyleCompiler styles = NO_STYLE_COMPILER;
private List<ExportDiagnostic> diagnostics = new ArrayList<>();
private Builder() {
}
public Builder data(IrisData value) {
data = Objects.requireNonNull(value);
return this;
}
public Builder dimension(IrisDimension value) {
dimension = Objects.requireNonNull(value);
return this;
}
public Builder capabilities(ExportCapabilities value) {
capabilities = Objects.requireNonNull(value);
return this;
}
public Builder namespace(String value) {
namespace = Objects.requireNonNull(value);
return this;
}
public Builder mode(WorldgenExportMode value) {
mode = Objects.requireNonNull(value);
return this;
}
public Builder objects(boolean value) {
objects = value;
return this;
}
public Builder styles(StyleCompiler value) {
styles = Objects.requireNonNull(value);
return this;
}
public Builder diagnostics(List<ExportDiagnostic> value) {
diagnostics = Objects.requireNonNull(value);
return this;
}
public PackCompileContext build() {
return new PackCompileContext(this);
}
}
}
@@ -0,0 +1,73 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.pack.datapack.export.WorldgenExportProgress;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeStage;
import art.arcane.iris.pack.datapack.export.compile.cave.CaveStage;
import art.arcane.iris.pack.datapack.export.compile.decoration.DecorationStage;
import art.arcane.iris.pack.datapack.export.compile.object.ObjectStage;
import art.arcane.iris.pack.datapack.export.compile.structure.StructureStage;
import art.arcane.iris.pack.datapack.export.compile.subterrain.SubterrainStage;
import art.arcane.iris.pack.datapack.export.compile.surface.SurfaceStage;
import art.arcane.iris.pack.datapack.export.compile.terrain.RiverStage;
import art.arcane.iris.pack.datapack.export.compile.terrain.TerrainStage;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportIr;
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
import java.util.List;
public final class PackCompiler {
private static final String ROUTER_STAGE = "router";
private final List<CompileStage> stages;
public PackCompiler(List<CompileStage> stages) {
this.stages = List.copyOf(stages);
}
public static PackCompiler create() {
return new PackCompiler(List.of(
new DimensionStage(),
new ImageMapStage(),
new BiomeStage(),
new TerrainStage(),
new RiverStage(),
new CaveStage(),
new SurfaceStage(),
new DecorationStage(),
new StructureStage(),
new SubterrainStage(),
new ObjectStage()));
}
public ExportIr compile(PackCompileContext context, WorldgenExportProgress progress) {
for (CompileStage stage : stages) {
progress.stage(stage.name());
stage.apply(context);
}
progress.stage(ROUTER_STAGE);
DimensionDef dimension = context.dimensionDef();
BiomeCodes codes = context.biomeCodes();
RouterDef router = RouterCompiler.compile(
context.height(),
context.volumetric(),
context.caves(),
context.aquifer(),
codes,
dimension);
return new ExportIr(
context.namespace(),
context.capabilities().target(),
context.mode(),
dimension,
context.noises().definitions(),
router,
codes.layout(),
context.features().apply(context.biomes(), context.namespace()),
context.material(),
context.diagnostics(),
context.jsonResources(),
context.binaryResources());
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import art.arcane.iris.pack.datapack.export.ir.RouterDef;
public final class RouterCompiler {
private static final Field DENSITY_PER_BLOCK = new Field.Const(1.0);
private static final Field UNUSED_CLIMATE = new Field.Const(0.0);
private static final double HALF_BLOCK = 0.5;
private static final int VOLUME_CELL_XZ = 4;
private static final int VOLUME_CELL_Y = 8;
private static final int SURFACE_CELL_XZ = 16;
private static final int SURFACE_CELL_Y = 1;
private RouterCompiler() {
}
public static RouterDef compile(Field height, Field volumetric, Field caves, AquiferDef aquifer, BiomeCodes codes, DimensionDef dimension) {
FieldValidator.requireTwoDimensional(height, "height");
Field ramp = new Field.Binary(Field.BinaryOp.MUL, DENSITY_PER_BLOCK, new Field.Binary(Field.BinaryOp.SUB, height, blockCenterY(dimension)));
Field terrain = new Field.Binary(Field.BinaryOp.ADD, ramp, new Field.Grid(volumetric, VOLUME_CELL_XZ, VOLUME_CELL_Y));
return new RouterDef(
codes.surfaceCode(),
codes.caveCode(),
UNUSED_CLIMATE,
UNUSED_CLIMATE,
codes.class3d(),
UNUSED_CLIMATE,
new Field.Grid(height, SURFACE_CELL_XZ, SURFACE_CELL_Y),
new Field.Binary(Field.BinaryOp.MIN, terrain, caves),
aquifer);
}
private static Field blockCenterY(DimensionDef dimension) {
double bottom = dimension.minY();
double top = dimension.minY() + dimension.height();
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, bottom, top, bottom - HALF_BLOCK, top - HALF_BLOCK);
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import java.util.Objects;
public record WeightedState(VanillaBlockState state, int weight) {
public WeightedState {
Objects.requireNonNull(state);
if (weight < 1) {
throw new IllegalArgumentException("Block state weight must be positive, got " + weight + " for " + state.canonical());
}
}
}
@@ -0,0 +1,76 @@
package art.arcane.iris.pack.datapack.export.compile;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Map;
public final class WorldgenSyntax {
private final DataVersion target;
private WorldgenSyntax(DataVersion target) {
this.target = target;
}
public static WorldgenSyntax forTarget(DataVersion target) {
if (!WorldgenExportTargets.SUPPORTED.contains(target)) {
throw WorldgenExportTargets.unsupported(target.getVersion());
}
return new WorldgenSyntax(target);
}
public DataVersion target() {
return target;
}
public String featureFolder() {
return "worldgen/feature";
}
public JsonObject blockState(VanillaBlockState state) {
JsonObject json = new JsonObject();
json.addProperty("id", state.name());
if (!state.properties().isEmpty()) {
JsonObject properties = new JsonObject();
for (Map.Entry<String, String> property : state.properties().entrySet()) {
properties.addProperty(property.getKey(), property.getValue());
}
json.add("properties", properties);
}
return json;
}
public JsonObject provider(List<WeightedState> entries) {
if (entries.isEmpty()) {
throw new IllegalArgumentException("A block state provider needs at least one state");
}
if (entries.size() == 1) {
return blockState(entries.getFirst().state());
}
JsonArray weighted = new JsonArray(entries.size());
for (WeightedState entry : entries) {
JsonObject item = new JsonObject();
item.add("data", blockState(entry.state()));
item.addProperty("weight", entry.weight());
weighted.add(item);
}
JsonObject provider = new JsonObject();
provider.addProperty("type", "minecraft:weighted");
provider.add("entries", weighted);
return provider;
}
public JsonObject feature(String type, JsonObject config) {
JsonObject feature = new JsonObject();
feature.addProperty("type", type);
for (Map.Entry<String, JsonElement> field : config.entrySet()) {
feature.add(field.getKey(), field.getValue().deepCopy());
}
return feature;
}
}
@@ -0,0 +1,31 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
public record BiomeCodes(Field surfaceCode, Field caveCode, Field class3d, ClimateLayout layout, Map<String, String> loadKeyByBiomeId) {
public static final String BASE_CODE = "biome/base_code";
public BiomeCodes {
Objects.requireNonNull(surfaceCode);
Objects.requireNonNull(caveCode);
Objects.requireNonNull(class3d);
Objects.requireNonNull(layout);
FieldValidator.requireTwoDimensional(surfaceCode, "surface code");
FieldValidator.requireTwoDimensional(caveCode, "cave code");
SortedMap<String, String> loadKeys = new TreeMap<>(loadKeyByBiomeId);
for (Map.Entry<String, String> entry : loadKeys.entrySet()) {
VanillaResourceIdentifier.requireIdentifier(entry.getKey(), "biome id");
Objects.requireNonNull(entry.getValue());
}
loadKeyByBiomeId = Collections.unmodifiableSortedMap(loadKeys);
}
}
@@ -0,0 +1,141 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class BiomeDecision {
static final int LAND = 0;
static final int SEA = 1;
static final int SHORE = 2;
static final int SURFACE_SLOT = 0;
private static final double UNDERGROUND_DEPTH = 8.0;
private static final int BRIDGE_WEIGHT = 8;
private static final int LEVEL_WEIGHT = 2;
private static final int ZONE_WEIGHT = 4;
private static final int DEPTH_WEIGHT = 2;
private static final Field UNUSED_CODE = new Field.Const(0.0);
private final BiomeCodes base;
private final Parts parts;
BiomeDecision(BiomeCodes base, Parts parts) {
this.base = Objects.requireNonNull(base);
this.parts = Objects.requireNonNull(parts);
}
public BiomeCodes base() {
return base;
}
public BiomeCodes bind(Field height) {
FieldValidator.requireTwoDimensional(Objects.requireNonNull(height), "height");
return new BiomeCodes(surfaceCode(height), base.caveCode(), class3d(height), base.layout(), base.loadKeyByBiomeId());
}
private Field surfaceCode(Field height) {
double fluid = parts.fluid();
double fluidTop = Math.nextUp((float) fluid);
IndexedField levels = IndexedField.partition(height, List.of(fluid - 1.0, fluid, fluidTop), List.of(0, LEVEL_WEIGHT, 2 * LEVEL_WEIGHT, 3 * LEVEL_WEIGHT));
IndexedField above = IndexedField.partition(new Field.Binary(Field.BinaryOp.SUB, height, parts.shoreHeight()), List.of(fluidTop), List.of(0, 1));
IndexedField key = IndexedField.sum(List.of(parts.bridge().remap(bridge -> bridge * BRIDGE_WEIGHT), levels, above));
IndexedField list = key.remap(BiomeDecision::listFor);
List<Field> codes = new ArrayList<>(parts.chains().size());
for (IndexedField chain : parts.chains()) {
codes.add(chain.lookup(index -> parts.surfaceCodes()[index]));
}
if (list.isConstant()) {
return codes.get(list.values().getFirst());
}
List<Field> cases = new ArrayList<>(list.values().getLast() + 1);
for (int value = 0; value <= list.values().getLast(); value++) {
cases.add(list.values().contains(value) ? codes.get(value) : UNUSED_CODE);
}
return new Field.Mux(list.field(), cases);
}
private Field class3d(Field height) {
if (parts.caveSlot().equals(IndexedField.constant(SURFACE_SLOT)) && parts.carvingSlots().isEmpty()) {
return new Field.Const(0.0);
}
IndexedField zones = parts.zoneKeys();
Field caveExtra = new Field.Binary(Field.BinaryOp.SUB, parts.requiredDepth(), new Field.Const(UNDERGROUND_DEPTH));
Field caveZone = zones.lookup(key -> key / ZONE_WEIGHT == CarvingZones.CAVE_ZONE ? 1.0 : 0.0);
Field depthGate = new Field.Binary(Field.BinaryOp.ADD, new Field.Const(UNDERGROUND_DEPTH), new Field.Gate(caveZone, caveExtra));
Field clearance = new Field.Binary(Field.BinaryOp.SUB, new Field.Binary(Field.BinaryOp.SUB, height, parts.y()), depthGate);
IndexedField deepEnough = IndexedField.partition(clearance, List.of(0.0), List.of(0, 1));
IndexedField key = IndexedField.sum(List.of(zones.remap(zone -> zone / ZONE_WEIGHT * DEPTH_WEIGHT), deepEnough));
IndexedField classIndex = key.remap(BiomeDecision::classFor);
List<IndexedField> cases = new ArrayList<>(parts.carvingSlots().size() + 2);
cases.add(IndexedField.constant(SURFACE_SLOT));
cases.add(parts.caveSlot());
cases.addAll(parts.carvingSlots());
IndexedField slot = IndexedField.select(classIndex, cases);
return slot.lookup(value -> value == SURFACE_SLOT ? 0.0 : parts.undergroundCodes()[value - 1]);
}
static int listFor(int key) {
boolean landBase = key / BRIDGE_WEIGHT == LAND;
int level = (key % BRIDGE_WEIGHT) / LEVEL_WEIGHT;
boolean aboveShoreBand = key % LEVEL_WEIGHT == 1;
boolean nearFluid = level >= 1;
boolean belowFluid = level <= 1;
boolean atFluid = level == 2;
int base = landBase ? LAND : SEA;
if (nearFluid) {
if (!aboveShoreBand) {
return SHORE;
}
if (!landBase) {
return LAND;
}
} else if (!landBase && aboveShoreBand) {
return LAND;
}
if (belowFluid && landBase) {
return SEA;
}
if (atFluid) {
return SHORE;
}
return base;
}
static int classFor(int key) {
int zone = key / DEPTH_WEIGHT;
boolean deepEnough = key % DEPTH_WEIGHT == 1;
if (zone == CarvingZones.SHALLOW_ZONE || !deepEnough) {
return 0;
}
return zone == CarvingZones.CAVE_ZONE ? 1 : zone;
}
record Parts(
double fluid,
IndexedField bridge,
Field shoreHeight,
List<IndexedField> chains,
double[] surfaceCodes,
Field y,
IndexedField zoneKeys,
IndexedField caveSlot,
Field requiredDepth,
List<IndexedField> carvingSlots,
double[] undergroundCodes) {
Parts {
Objects.requireNonNull(bridge);
Objects.requireNonNull(shoreHeight);
chains = List.copyOf(chains);
Objects.requireNonNull(surfaceCodes);
Objects.requireNonNull(y);
Objects.requireNonNull(zoneKeys);
Objects.requireNonNull(caveSlot);
Objects.requireNonNull(requiredDepth);
carvingSlots = List.copyOf(carvingSlots);
Objects.requireNonNull(undergroundCodes);
}
}
}
@@ -0,0 +1,299 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.loading.IrisData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Function;
public final class BiomeDecisionCompiler {
private static final long REGION_SALT = 883L;
private static final long CONTINENT_SALT = 234_234_565L;
private static final long PICK_SALT = 1_000L;
private static final long CHILD_SALT = 123L;
private static final long SHORE_SALT = 3_458_612L;
private static final double SURFACE_SPAN = 1.0;
private static final double UNDERGROUND_MIN = 1.25;
private static final double UNDERGROUND_MAX = 2.0;
private static final double MINIMUM_CAVE_DEPTH = 8.0;
private static final double NO_CAVE_DEPTH = 100_000.0;
private static final int DEEP_FLOOR_Y = -8;
private static final int DEEP_ABOVE_MIN_Y = 40;
private final StyleCompiler styles;
public BiomeDecisionCompiler(StyleCompiler styles) {
this.styles = Objects.requireNonNull(styles);
}
public BiomeDecision compile(PackCompileContext context) {
IrisDimension dimension = context.dimension();
IrisData data = context.data();
DimensionDef dimensionDef = context.dimensionDef();
BiomePools pools = BiomePools.load(dimension, data);
CarvingZones carving = CarvingZones.plan(dimension, data, Math.max(DEEP_FLOOR_Y, dimensionDef.minY() + DEEP_ABOVE_MIN_Y));
List<String> surfaceKeys = List.copyOf(pools.surface());
SortedSet<String> undergroundSet = new TreeSet<>(pools.cave());
undergroundSet.addAll(carving.biomes());
List<String> undergroundKeys = List.copyOf(undergroundSet);
Map<String, Integer> surfaceIndex = indexOf(surfaceKeys);
Map<String, Integer> undergroundIndex = indexOf(undergroundKeys);
Map<String, String> ids = biomeIds(context.namespace(), surfaceKeys, undergroundKeys);
double[] surfaceCodes = codes(-SURFACE_SPAN, SURFACE_SPAN, surfaceKeys.size());
double[] undergroundCodes = codes(UNDERGROUND_MIN, UNDERGROUND_MAX, undergroundKeys.size());
Build build = new Build(pools, surfaceKeys, surfaceIndex,
new SelectorCompiler(styles.selectors(), new SelectorCompiler.Sources(data, context.noises(), context.capabilities())));
IndexedField region = build.region(dimension);
IndexedField bridge = build.selectors.scaled(dimension.getContinentalStyle(), 1.0 / dimension.getContinentZoom(), CONTINENT_SALT)
.partition(List.of(dimension.getLandChance()), List.of(BiomeDecision.LAND, BiomeDecision.SEA))
.shared("biome/bridge");
RegionPicks picks = build.regionPicks(dimension, bridge, undergroundIndex, context.diagnostics());
List<IndexedField> chains = List.of(
build.implode("biome/land", IndexedField.select(region, picks.land()).shared("biome/land/pick")),
build.implode("biome/sea", IndexedField.select(region, picks.sea()).shared("biome/sea/pick")),
build.implode("biome/shore", IndexedField.select(region, picks.shore()).shared("biome/shore/pick")));
IndexedField baseSurface = IndexedField.select(bridge, chains.subList(0, 2));
IndexedField caveSlot = IndexedField.select(region, picks.caves()).shared("biome/cave");
Field caveCode = caveSlot.lookup(value -> value == BiomeDecision.SURFACE_SLOT ? 0.0 : undergroundCodes[value - 1]);
Field requiredDepth = caveSlot.lookup(value -> value == BiomeDecision.SURFACE_SLOT
? NO_CAVE_DEPTH
: Math.max(MINIMUM_CAVE_DEPTH, pools.biome(undergroundKeys.get(value - 1)).getCaveMinDepthBelowSurface()));
Field y = blockY(dimensionDef);
List<IndexedField> carvingSlots = carving.slots(build.selectors, data, key -> undergroundIndex.get(key) + 1);
BiomeCodes base = new BiomeCodes(
new Field.Shared(BiomeCodes.BASE_CODE, baseSurface.lookup(index -> surfaceCodes[index])),
caveCode,
new Field.Const(0.0),
layout(ids, surfaceKeys, surfaceCodes, undergroundKeys, undergroundCodes),
loadKeys(ids));
BiomeDecision.Parts parts = new BiomeDecision.Parts(
dimensionDef.fluidHeight(),
bridge,
shoreHeight(region, picks.shoreHeights()),
chains,
surfaceCodes,
y,
carving.zoneKeys(y),
caveSlot,
requiredDepth,
carvingSlots,
undergroundCodes);
return new BiomeDecision(base, parts);
}
private static Field shoreHeight(IndexedField region, List<Field> heights) {
if (region.isConstant()) {
return heights.get(region.values().getFirst());
}
if (new HashSet<>(heights).size() == 1) {
return heights.getFirst();
}
return new Field.Shared("biome/shore_height", new Field.Mux(region.field(), heights));
}
private static SortedMap<String, String> loadKeys(Map<String, String> ids) {
SortedMap<String, String> loadKeys = new TreeMap<>();
for (Map.Entry<String, String> entry : ids.entrySet()) {
loadKeys.put(entry.getValue(), entry.getKey());
}
return loadKeys;
}
private static Map<String, Integer> indexOf(List<String> keys) {
Map<String, Integer> index = new HashMap<>(keys.size() * 2);
for (int position = 0; position < keys.size(); position++) {
index.put(keys.get(position), position);
}
return index;
}
private static Map<String, String> biomeIds(String namespace, List<String> surfaceKeys, List<String> undergroundKeys) {
SortedSet<String> all = new TreeSet<>(surfaceKeys);
all.addAll(undergroundKeys);
Set<String> taken = new HashSet<>();
Map<String, String> ids = new TreeMap<>();
for (String key : all) {
ids.put(key, BiomeIdSanitizer.sanitize(namespace, key, taken));
}
return ids;
}
private static double[] codes(double min, double max, int count) {
double[] codes = new double[count];
double half = (max - min) / (2.0 * Math.max(1, count));
for (int index = 0; index < count; index++) {
codes[index] = min + (2 * index + 1) * half;
}
return codes;
}
private static ClimateLayout layout(Map<String, String> ids, List<String> surfaceKeys, double[] surfaceCodes, List<String> undergroundKeys, double[] undergroundCodes) {
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>(surfaceKeys.size() + undergroundKeys.size());
double surfaceHalf = SURFACE_SPAN / Math.max(1, surfaceKeys.size());
for (int index = 0; index < surfaceKeys.size(); index++) {
entries.add(new ClimateLayout.ClimateEntry(ids.get(surfaceKeys.get(index)), ClimateLayout.ClimateAxis.TEMPERATURE,
surfaceCodes[index] - surfaceHalf, surfaceCodes[index] + surfaceHalf));
}
double undergroundHalf = (UNDERGROUND_MAX - UNDERGROUND_MIN) / (2.0 * Math.max(1, undergroundKeys.size()));
for (int index = 0; index < undergroundKeys.size(); index++) {
entries.add(new ClimateLayout.ClimateEntry(ids.get(undergroundKeys.get(index)), ClimateLayout.ClimateAxis.DEPTH,
undergroundCodes[index] - undergroundHalf, Math.min(UNDERGROUND_MAX, undergroundCodes[index] + undergroundHalf)));
}
return new ClimateLayout(entries);
}
private static Field blockY(DimensionDef dimension) {
double bottom = dimension.minY();
double top = dimension.minY() + dimension.height();
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, bottom, top, bottom, top);
}
private static final class Build {
private final BiomePools pools;
private final List<String> surfaceKeys;
private final Map<String, Integer> surfaceIndex;
private final SelectorCompiler selectors;
private final Map<String, IndexedField> hops = new HashMap<>();
private Build(BiomePools pools, List<String> surfaceKeys, Map<String, Integer> surfaceIndex, SelectorCompiler selectors) {
this.pools = pools;
this.surfaceKeys = surfaceKeys;
this.surfaceIndex = surfaceIndex;
this.selectors = selectors;
}
private IndexedField region(IrisDimension dimension) {
List<IrisRegion> regions = new ArrayList<>(pools.regions().size());
List<Integer> leaves = new ArrayList<>(pools.regions().size());
for (BiomePools.RegionPools regionPools : pools.regions()) {
leaves.add(regions.size());
regions.add(regionPools.region());
}
if (regions.size() == 1) {
return IndexedField.constant(0);
}
return selectors.zoomed(dimension.getRegionStyle(), dimension.getRegionZoom(), REGION_SALT)
.partition(BiomePools.rarityCuts(regions), leaves)
.shared("biome/region");
}
private RegionPicks regionPicks(IrisDimension dimension, IndexedField bridge, Map<String, Integer> undergroundIndex, List<ExportDiagnostic> diagnostics) {
int count = pools.regions().size();
RegionPicks picks = new RegionPicks(new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count), new ArrayList<>(count));
for (int position = 0; position < count; position++) {
BiomePools.RegionPools regionPools = pools.regions().get(position);
IrisRegion region = regionPools.region();
IndexedField landPick = pick(dimension, regionPools, InferredType.LAND, surfaceIndex::get, 0);
IndexedField seaPick = pick(dimension, regionPools, InferredType.SEA, surfaceIndex::get, 0);
IndexedField land = landPick == null ? seaPick : landPick;
IndexedField sea = seaPick == null ? landPick : seaPick;
IndexedField shore = pick(dimension, regionPools, InferredType.SHORE, surfaceIndex::get, 0);
IndexedField cave = pick(dimension, regionPools, InferredType.CAVE, undergroundIndex::get, 1);
picks.land().add(land);
picks.sea().add(sea);
picks.shore().add(shore == null ? IndexedField.select(bridge, List.of(land, sea)) : shore);
picks.caves().add(cave == null ? IndexedField.constant(BiomeDecision.SURFACE_SLOT) : cave);
picks.shoreHeights().add(shoreHeight(region, position));
if (region.getShoreMinimumWidth() > 0.0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.SHORE_WIDTH, "regions/" + region.getLoadKey(),
"Shore widening by slope is not exported; the shore band follows the shore height only"));
}
}
return picks;
}
private IndexedField pick(IrisDimension dimension, BiomePools.RegionPools regionPools, InferredType type, Function<String, Integer> index, int offset) {
List<IrisBiome> pool = regionPools.of(type);
if (pool.isEmpty()) {
return null;
}
List<Integer> leaves = new ArrayList<>(pool.size());
for (IrisBiome biome : pool) {
leaves.add(index.apply(biome.getLoadKey()) + offset);
}
if (pool.size() == 1) {
return IndexedField.constant(leaves.getFirst());
}
IrisRegion region = regionPools.region();
double zoom = switch (type) {
case LAND -> dimension.getBiomeZoom() * dimension.getLandZoom() * region.getLandBiomeZoom();
case SEA -> dimension.getBiomeZoom() * dimension.getSeaZoom() * region.getSeaBiomeZoom();
case SHORE -> dimension.getBiomeZoom() * region.getShoreBiomeZoom();
case CAVE -> dimension.getBiomeZoom() * region.getCaveBiomeZoom();
};
IrisGeneratorStyle style = switch (type) {
case LAND -> dimension.getLandBiomeStyle();
case SEA -> dimension.getSeaBiomeStyle();
case SHORE -> dimension.getShoreBiomeStyle();
case CAVE -> dimension.getCaveBiomeStyle();
};
return selectors.zoomed(style, zoom, PICK_SALT + type.ordinal()).partition(BiomePools.rarityCuts(pool), leaves);
}
private Field shoreHeight(IrisRegion region, int position) {
if (region.getShoreHeightMin() == region.getShoreHeightMax()) {
return new Field.Const(region.getShoreHeightMin());
}
return selectors.sampled(NoiseStyle.IRIS.style(), region.getShoreHeightZoom(), SHORE_SALT + position, region::getShoreHeight).curve();
}
private IndexedField implode(String name, IndexedField start) {
IndexedField current = start;
for (int hop = 0; hop < BiomePools.MAX_CHILD_HOPS; hop++) {
List<Integer> movers = new ArrayList<>();
for (int value : current.values()) {
BiomePools.ChildPlan plan = pools.plan(surfaceKeys.get(value));
if (plan != null && plan.moves()) {
movers.add(value);
}
}
if (movers.isEmpty()) {
return current;
}
Map<Integer, IndexedField> moved = new HashMap<>(movers.size() * 2);
for (int mover : movers) {
moved.put(mover, hops.computeIfAbsent(surfaceKeys.get(mover), this::hop));
}
current = IndexedField.advance(current, moved).shared(name + "/hop" + (hop + 1));
}
return current;
}
private IndexedField hop(String parent) {
IrisBiome biome = pools.biome(parent);
BiomePools.ChildPlan plan = pools.plan(parent);
List<Integer> leaves = new ArrayList<>(plan.choiceByIndex().size());
for (String choice : plan.choiceByIndex()) {
leaves.add(surfaceIndex.get(choice));
}
return selectors.scaled(biome.getChildStyle(), biome.getChildShrinkFactor(), CHILD_SALT)
.partition(BiomePools.selectionCuts(plan.choiceByIndex().size()), leaves);
}
}
private record RegionPicks(List<IndexedField> land, List<IndexedField> sea, List<IndexedField> shore, List<IndexedField> caves, List<Field> shoreHeights) {
}
}
@@ -0,0 +1,299 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.biome.IrisVanillaBiomeTags;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.validation.ContentGate;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.VanillaDataSource;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public final class BiomeDefinitionCompiler {
private static final String VANILLA_NAMESPACE = "minecraft";
private static final String BIOME_FOLDER = "/worldgen/biome/";
private static final String TAG_FOLDER = "/tags/worldgen/biome";
private static final String JSON_SUFFIX = ".json";
private static final String SPAWNS_ATTRIBUTE = "minecraft:gameplay/natural_mob_spawns";
private static final String SPAWN_PROBABILITY_ATTRIBUTE = "minecraft:gameplay/creature_world_gen_spawn_probability";
private static final String SPAWN_PROBABILITY = "creature_spawn_probability";
private static final List<String> CLIMATE_KEYS = List.of("has_precipitation", "temperature", "downfall", "temperature_modifier");
private static final List<String> CUSTOM_KEYS = List.of("has_precipitation", "temperature", "downfall", "temperature_modifier",
"effects", "attributes", "spawners", "spawn_costs", SPAWN_PROBABILITY);
private final Optional<VanillaDataSource> vanilla;
public BiomeDefinitionCompiler(Optional<VanillaDataSource> vanilla) {
this.vanilla = Objects.requireNonNull(vanilla);
}
public static BiomeDefinitionCompiler forTarget(DataVersion target) {
if (!IrisPlatforms.isBound()) {
return new BiomeDefinitionCompiler(Optional.empty());
}
IrisPlatform platform = IrisPlatforms.get();
boolean running = DataVersion.forMinecraftVersion(platform.minecraftVersion()) == target;
return new BiomeDefinitionCompiler(running ? Optional.ofNullable(platform.vanillaData()) : Optional.empty());
}
public Definitions compile(PackCompileContext context, Map<String, String> loadKeyByBiomeId) {
DataVersion target = context.capabilities().target();
ContentGate gate = vanilla.isPresent() ? ContentGate.forData(context.data()) : new ContentGate(null, Map.of(), null);
if (vanilla.isEmpty()) {
diagnose(context, "dimensions/" + context.dimension().getLoadKey(), "Vanilla biome data for Minecraft " + target.getVersion()
+ " is not available on this server; biomes are built from their Iris fields without vanilla music, ambience, spawns or structure tags");
}
Map<String, List<String>> vanillaTags = vanilla.map(BiomeDefinitionCompiler::directTags).orElse(Map.of());
SortedMap<String, SortedSet<String>> members = new TreeMap<>();
List<BiomeDef> biomes = new ArrayList<>(loadKeyByBiomeId.size());
for (Map.Entry<String, String> entry : new TreeMap<>(loadKeyByBiomeId).entrySet()) {
String id = entry.getKey();
IrisBiome biome = Objects.requireNonNull(context.data().getBiomeLoader().load(entry.getValue(), false),
"Biome '" + entry.getValue() + "' could not be loaded");
IrisBiomeCustom custom = custom(context, biome);
String visualKey = biome.getBiomeScatter().isEmpty() ? biome.getDerivativeKey() : namespaced(biome.getBiomeScatter().getFirst());
Optional<JsonObject> base = vanilla.flatMap(source -> read(source, visualKey));
if (vanilla.isPresent() && base.isEmpty()) {
diagnose(context, "biomes/" + biome.getLoadKey(), "Vanilla biome '" + visualKey + "' has no data for Minecraft " + target.getVersion()
+ "; the biome is built from its Iris fields");
}
JsonObject overlay = custom == null ? null : customJson(custom, target, gate);
JsonObject json = base.isPresent()
? merge(base.get(), overlay, custom)
: retained(overlay == null ? customJson(new IrisBiomeCustom(), target, gate) : overlay);
json.add("carvers", new JsonArray());
json.add("features", new JsonArray());
biomes.add(new BiomeDef(id.substring(id.indexOf(':') + 1), json));
for (String tag : tags(biome, custom, vanillaTags)) {
members.computeIfAbsent(tag, ignored -> new TreeSet<>()).add(id);
}
}
List<JsonResource> tags = new ArrayList<>(members.size());
for (Map.Entry<String, SortedSet<String>> tag : members.entrySet()) {
JsonObject json = new JsonObject();
json.addProperty("replace", false);
JsonArray values = new JsonArray(tag.getValue().size());
for (String member : tag.getValue()) {
values.add(member);
}
json.add("values", values);
int separator = tag.getKey().indexOf(':');
tags.add(new JsonResource("data/" + tag.getKey().substring(0, separator) + TAG_FOLDER + "/" + tag.getKey().substring(separator + 1) + JSON_SUFFIX, json));
}
return new Definitions(biomes, tags);
}
private static IrisBiomeCustom custom(PackCompileContext context, IrisBiome biome) {
boolean customs = biome.getCustomDerivitives() != null && biome.getCustomDerivitives().size() > 1;
if (customs || biome.getBiomeScatter().size() > 1) {
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.BIOME_VARIANT, "biomes/" + biome.getLoadKey(),
"Biomes with several custom derivatives or scatter biomes export as one biome using the first entry"));
}
return biome.isCustom() ? biome.getCustomDerivitives().getFirst() : null;
}
private static JsonObject customJson(IrisBiomeCustom custom, DataVersion target, ContentGate gate) {
return JsonParser.parseString(custom.generateJson(target.get(), gate)).getAsJsonObject();
}
private static JsonObject merge(JsonObject base, JsonObject overlay, IrisBiomeCustom custom) {
JsonObject merged = base.deepCopy();
if (overlay == null) {
return merged;
}
for (String key : CLIMATE_KEYS) {
if (overlay.has(key)) {
merged.add(key, overlay.get(key).deepCopy());
}
}
boolean spawnRarity = custom.getSpawnRarity() > 0;
putAll(object(merged, "effects"), overlay.getAsJsonObject("effects"));
JsonObject overlayAttributes = overlay.getAsJsonObject("attributes");
if (overlayAttributes != null) {
JsonObject attributes = object(merged, "attributes");
for (Map.Entry<String, JsonElement> attribute : overlayAttributes.entrySet()) {
switch (attribute.getKey()) {
case SPAWNS_ATTRIBUTE -> mergeSpawnAttribute(attributes, attribute.getValue());
case SPAWN_PROBABILITY_ATTRIBUTE -> {
if (spawnRarity) {
attributes.add(attribute.getKey(), attribute.getValue().deepCopy());
}
}
default -> attributes.add(attribute.getKey(), attribute.getValue().deepCopy());
}
}
}
if (overlay.has("spawners")) {
mergeSpawns(merged, overlay);
}
if (spawnRarity && overlay.has(SPAWN_PROBABILITY)) {
merged.add(SPAWN_PROBABILITY, overlay.get(SPAWN_PROBABILITY).deepCopy());
}
return merged;
}
private static void mergeSpawnAttribute(JsonObject attributes, JsonElement overlay) {
JsonElement existing = attributes.get(SPAWNS_ATTRIBUTE);
if (existing == null || !existing.isJsonObject() || !overlay.isJsonObject()) {
attributes.add(SPAWNS_ATTRIBUTE, overlay.deepCopy());
return;
}
JsonObject target = spawnHolder(existing.getAsJsonObject());
JsonObject source = spawnHolder(overlay.getAsJsonObject());
appendCategories(object(target, "spawns_by_category"), source.getAsJsonObject("spawns_by_category"));
putAll(object(target, "spawn_costs"), source.getAsJsonObject("spawn_costs"));
}
private static void mergeSpawns(JsonObject merged, JsonObject overlay) {
appendCategories(object(merged, "spawners"), overlay.getAsJsonObject("spawners"));
putAll(object(merged, "spawn_costs"), overlay.getAsJsonObject("spawn_costs"));
}
private static JsonObject spawnHolder(JsonObject attribute) {
JsonElement argument = attribute.get("argument");
return argument != null && argument.isJsonObject() ? argument.getAsJsonObject() : attribute;
}
private static void appendCategories(JsonObject target, JsonObject source) {
if (source == null) {
return;
}
for (Map.Entry<String, JsonElement> category : source.entrySet()) {
JsonElement existing = target.get(category.getKey());
JsonArray spawns = existing != null && existing.isJsonArray() ? existing.getAsJsonArray() : new JsonArray();
spawns.addAll(category.getValue().deepCopy().getAsJsonArray());
target.add(category.getKey(), spawns);
}
}
private static void putAll(JsonObject target, JsonObject source) {
if (source == null) {
return;
}
for (Map.Entry<String, JsonElement> entry : source.entrySet()) {
target.add(entry.getKey(), entry.getValue().deepCopy());
}
}
private static JsonObject object(JsonObject parent, String key) {
JsonElement existing = parent.get(key);
if (existing != null && existing.isJsonObject()) {
return existing.getAsJsonObject();
}
JsonObject created = new JsonObject();
parent.add(key, created);
return created;
}
private static JsonObject retained(JsonObject custom) {
JsonObject retained = new JsonObject();
for (String key : CUSTOM_KEYS) {
if (custom.has(key)) {
retained.add(key, custom.get(key).deepCopy());
}
}
return retained;
}
private static SortedSet<String> tags(IrisBiome biome, IrisBiomeCustom custom, Map<String, List<String>> vanillaTags) {
SortedSet<String> tags = new TreeSet<>();
if (custom != null && custom.getTags() != null) {
for (String tag : custom.getTags()) {
String normalized = tagId(tag);
if (normalized != null) {
tags.add(normalized);
}
}
}
String structureKey = biome.getVanillaDerivativeKey();
tags.addAll(vanillaTags.isEmpty() ? IrisVanillaBiomeTags.tagsFor(structureKey) : vanillaTags.getOrDefault(structureKey, List.of()));
return tags;
}
private static Map<String, List<String>> directTags(VanillaDataSource source) {
String root = "data/" + VANILLA_NAMESPACE + TAG_FOLDER;
Map<String, List<String>> tags = new HashMap<>();
for (String path : source.list(root)) {
if (!path.endsWith(JSON_SUFFIX)) {
continue;
}
String tag = VANILLA_NAMESPACE + ":" + path.substring(root.length() + 1, path.length() - JSON_SUFFIX.length());
Optional<JsonObject> json = read(source, path);
JsonArray values = json.isEmpty() ? null : json.get().getAsJsonArray("values");
if (values == null) {
continue;
}
for (JsonElement value : values) {
String member = value.isJsonObject() ? value.getAsJsonObject().get("id").getAsString() : value.getAsString();
if (!member.startsWith("#")) {
tags.computeIfAbsent(namespaced(member), ignored -> new ArrayList<>()).add(tag);
}
}
}
return tags;
}
private static Optional<JsonObject> read(VanillaDataSource source, String biomeKeyOrPath) {
String path = biomeKeyOrPath.startsWith("data/") ? biomeKeyOrPath : biomePath(biomeKeyOrPath);
if (path == null) {
return Optional.empty();
}
return source.read(path).map(bytes -> JsonParser.parseString(new String(bytes, StandardCharsets.UTF_8)).getAsJsonObject());
}
private static String biomePath(String biomeKey) {
if (biomeKey == null || !VanillaResourceIdentifier.validIdentifier(biomeKey)) {
return null;
}
int separator = biomeKey.indexOf(':');
return "data/" + biomeKey.substring(0, separator) + BIOME_FOLDER + biomeKey.substring(separator + 1) + JSON_SUFFIX;
}
private static String namespaced(String key) {
String trimmed = key.trim().toLowerCase(Locale.ROOT);
return trimmed.indexOf(':') < 0 ? VANILLA_NAMESPACE + ":" + trimmed : trimmed;
}
private static String tagId(String tag) {
if (tag == null || tag.isBlank()) {
return null;
}
String trimmed = tag.trim();
String normalized = namespaced(trimmed.startsWith("#") ? trimmed.substring(1) : trimmed);
return VanillaResourceIdentifier.validIdentifier(normalized) ? normalized : null;
}
private static void diagnose(PackCompileContext context, String resource, String message) {
context.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.VANILLA_BIOME_DATA, resource, message));
}
public record Definitions(List<BiomeDef> biomes, List<JsonResource> tags) {
public Definitions {
biomes = List.copyOf(biomes);
tags = List.copyOf(tags);
}
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import java.util.Objects;
import java.util.Set;
public final class BiomeIdSanitizer {
private static final int FIRST_SUFFIX = 2;
private BiomeIdSanitizer() {
}
public static String sanitize(String namespace, String loadKey, Set<String> taken) {
VanillaResourceIdentifier.requireNamespace(namespace, "biome namespace");
Objects.requireNonNull(taken);
String path = VanillaResourceIdentifier.sanitizePath(loadKey);
String id = namespace + ":" + path;
int suffix = FIRST_SUFFIX;
while (taken.contains(id)) {
id = namespace + ":" + path + "_" + suffix;
suffix++;
}
taken.add(id);
return id;
}
}
@@ -0,0 +1,192 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRaritySelection;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.Rarity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
final class BiomePools {
static final int MAX_CHILD_HOPS = 4;
static final List<InferredType> SURFACE_TYPES = List.of(InferredType.LAND, InferredType.SEA, InferredType.SHORE);
private final List<RegionPools> regions;
private final SortedMap<String, IrisBiome> biomes;
private final SortedSet<String> surface;
private final SortedSet<String> cave;
private final Map<String, ChildPlan> plans;
private BiomePools(List<RegionPools> regions, SortedMap<String, IrisBiome> biomes, SortedSet<String> surface, SortedSet<String> cave, Map<String, ChildPlan> plans) {
this.regions = regions;
this.biomes = biomes;
this.surface = surface;
this.cave = cave;
this.plans = plans;
}
static BiomePools load(IrisDimension dimension, IrisData data) {
List<RegionPools> regions = new ArrayList<>();
SortedMap<String, IrisBiome> biomes = new TreeMap<>();
for (IrisRegion region : data.getRegionLoader().loadAll(dimension.getRegions())) {
if (region.isCompatExcluded()) {
continue;
}
RegionPools pools = new RegionPools(
region,
pool(data, region.getLandBiomes(), biomes),
pool(data, region.getSeaBiomes(), biomes),
pool(data, region.getShoreBiomes(), biomes),
pool(data, region.getCaveBiomes(), biomes));
if (pools.land().isEmpty() && pools.sea().isEmpty()) {
throw new IllegalStateException("Region '" + region.getLoadKey() + "' has no land or sea biome to export");
}
regions.add(pools);
}
if (regions.isEmpty()) {
throw new IllegalStateException("Dimension '" + dimension.getLoadKey() + "' has no exportable region");
}
SortedSet<String> cave = new TreeSet<>();
SortedSet<String> frontier = new TreeSet<>();
for (RegionPools pools : regions) {
for (InferredType type : SURFACE_TYPES) {
for (IrisBiome biome : pools.of(type)) {
frontier.add(biome.getLoadKey());
}
}
for (IrisBiome biome : pools.cave()) {
cave.add(biome.getLoadKey());
}
}
Map<String, ChildPlan> plans = new HashMap<>();
SortedSet<String> surface = new TreeSet<>(frontier);
for (int hop = 0; hop < MAX_CHILD_HOPS && !frontier.isEmpty(); hop++) {
SortedSet<String> next = new TreeSet<>();
for (String key : frontier) {
ChildPlan plan = plans.computeIfAbsent(key, ignored -> ChildPlan.of(biomes.get(key), data, biomes));
for (String choice : plan.choices()) {
if (surface.add(choice)) {
next.add(choice);
}
}
}
frontier = next;
}
return new BiomePools(List.copyOf(regions), biomes, surface, cave, plans);
}
static List<Double> rarityCuts(List<? extends Rarity> options) {
double total = 0.0;
for (Rarity option : options) {
total += 1.0 / Rarity.get(option);
}
List<Double> cuts = new ArrayList<>(Math.max(0, options.size() - 1));
double cumulative = 0.0;
for (int index = 0; index + 1 < options.size(); index++) {
cumulative += 1.0 / Rarity.get(options.get(index));
cuts.add(cumulative / total);
}
return cuts;
}
static List<Double> selectionCuts(int size) {
int maximum = size - 1;
List<Double> cuts = new ArrayList<>(Math.max(0, maximum));
for (int index = 0; index < maximum; index++) {
cuts.add((index + 0.5) / maximum);
}
return cuts;
}
List<RegionPools> regions() {
return regions;
}
IrisBiome biome(String loadKey) {
return Objects.requireNonNull(biomes.get(loadKey), "Biome '" + loadKey + "' was not loaded");
}
SortedSet<String> surface() {
return surface;
}
SortedSet<String> cave() {
return cave;
}
ChildPlan plan(String loadKey) {
return plans.get(loadKey);
}
private static List<IrisBiome> pool(IrisData data, KList<String> keys, SortedMap<String, IrisBiome> biomes) {
List<IrisBiome> pool = new ArrayList<>();
for (IrisBiome biome : data.getBiomeLoader().loadAll(keys)) {
if (biome.isCompatExcluded()) {
continue;
}
biomes.putIfAbsent(biome.getLoadKey(), biome);
pool.add(biome);
}
return List.copyOf(pool);
}
record RegionPools(IrisRegion region, List<IrisBiome> land, List<IrisBiome> sea, List<IrisBiome> shore, List<IrisBiome> cave) {
List<IrisBiome> of(InferredType type) {
return switch (type) {
case LAND -> land;
case SEA -> sea;
case SHORE -> shore;
case CAVE -> cave;
};
}
}
record ChildPlan(String parent, List<String> choiceByIndex) {
static ChildPlan of(IrisBiome parent, IrisData data, SortedMap<String, IrisBiome> biomes) {
if (parent.getChildren().isEmpty()) {
return new ChildPlan(parent.getLoadKey(), List.of(parent.getLoadKey()));
}
KList<IrisBiome> options = new KList<>();
for (IrisBiome child : parent.getRealChildren(() -> data)) {
if (child != null) {
biomes.putIfAbsent(child.getLoadKey(), child);
options.add(child);
}
}
options.add(parent);
IrisRaritySelection<IrisBiome> selection = IrisRaritySelection.create(options);
long size = selection.size();
List<String> choices = new ArrayList<>((int) size);
for (long index = 0; index < size; index++) {
choices.add(selection.get(index).getLoadKey());
}
return new ChildPlan(parent.getLoadKey(), List.copyOf(choices));
}
boolean moves() {
for (String choice : choiceByIndex) {
if (!choice.equals(parent)) {
return true;
}
}
return false;
}
SortedSet<String> choices() {
return new TreeSet<>(choiceByIndex);
}
}
}
@@ -0,0 +1,21 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.compile.CompileStage;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
public final class BiomeStage implements CompileStage {
@Override
public String name() {
return "biomes";
}
@Override
public void apply(PackCompileContext context) {
BiomeDecision decision = new BiomeDecisionCompiler(context.styles()).compile(context);
context.setBiomeDecision(decision);
BiomeDefinitionCompiler.Definitions definitions = BiomeDefinitionCompiler.forTarget(context.capabilities().target())
.compile(context, decision.base().loadKeyByBiomeId());
context.setBiomes(definitions.biomes());
context.jsonResources().addAll(definitions.tags());
}
}
@@ -0,0 +1,239 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.pack.value.IrisRaritySelection;
import art.arcane.volmlib.util.math.Rarity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.ToIntFunction;
final class CarvingZones {
static final int SHALLOW_ZONE = 0;
static final int CAVE_ZONE = 1;
static final int FIRST_CARVING_ZONE = 2;
private static final int MAX_CHILD_DEPTH = 32;
private static final double MINIMUM_CHILD_SHRINK_FACTOR = 0.0001;
private static final long CARVING_SALT = 0x0CA4_7E00L;
private final List<Integer> cuts;
private final List<Integer> zoneByInterval;
private final List<Machine> machines;
private final SortedSet<String> biomes;
private CarvingZones(List<Integer> cuts, List<Integer> zoneByInterval, List<Machine> machines, SortedSet<String> biomes) {
this.cuts = cuts;
this.zoneByInterval = zoneByInterval;
this.machines = machines;
this.biomes = biomes;
}
static CarvingZones plan(IrisDimension dimension, IrisData data, int deepTop) {
List<IrisDimensionCarvingEntry> entries = new ArrayList<>();
if (dimension.getCarving() != null) {
for (IrisDimensionCarvingEntry entry : dimension.getCarving()) {
if (entry != null && entry.isEnabled() && entry.getRealBiome(data) != null) {
entries.add(entry);
}
}
}
SortedSet<Integer> cutSet = new TreeSet<>();
cutSet.add(deepTop + 1);
for (IrisDimensionCarvingEntry entry : entries) {
IrisRange range = entry.getWorldYRange();
if (range != null && range.getMin() <= range.getMax()) {
cutSet.add((int) Math.ceil(range.getMin()));
cutSet.add((int) Math.floor(range.getMax()) + 1);
}
}
List<Integer> cuts = List.copyOf(cutSet);
Map<String, IrisDimensionCarvingEntry> byId = dimension.getCarvingEntryIndex();
Map<ZoneKey, Integer> zones = new LinkedHashMap<>();
List<Machine> machines = new ArrayList<>();
SortedSet<String> biomes = new TreeSet<>();
List<Integer> zoneByInterval = new ArrayList<>(cuts.size() + 1);
for (int interval = 0; interval <= cuts.size(); interval++) {
int y = interval == 0 ? cuts.getFirst() - 1 : cuts.get(interval - 1);
if (y > deepTop) {
zoneByInterval.add(SHALLOW_ZONE);
continue;
}
IrisDimensionCarvingEntry root = null;
List<Integer> inRange = new ArrayList<>();
for (int position = 0; position < entries.size(); position++) {
IrisDimensionCarvingEntry entry = entries.get(position);
if (contains(entry, y)) {
root = entry;
inRange.add(position);
}
}
if (root == null) {
zoneByInterval.add(CAVE_ZONE);
continue;
}
ZoneKey key = new ZoneKey(entries.indexOf(root), List.copyOf(inRange));
Integer zone = zones.get(key);
if (zone == null) {
Machine machine = Machine.build(root, y, byId, data);
zone = FIRST_CARVING_ZONE + machines.size();
zones.put(key, zone);
machines.add(machine);
for (State state : machine.states()) {
biomes.add(state.entry().getRealBiome(data).getLoadKey());
}
}
zoneByInterval.add(zone);
}
return new CarvingZones(cuts, List.copyOf(zoneByInterval), List.copyOf(machines), biomes);
}
SortedSet<String> biomes() {
return biomes;
}
IndexedField zoneKeys(Field y) {
List<Double> thresholds = new ArrayList<>(cuts.size());
for (int cut : cuts) {
thresholds.add((double) cut);
}
List<Integer> leaves = new ArrayList<>(zoneByInterval.size());
for (int zone : zoneByInterval) {
leaves.add(zone * 4);
}
return IndexedField.partition(y, thresholds, leaves);
}
List<IndexedField> slots(SelectorCompiler selectors, IrisData data, ToIntFunction<String> undergroundSlot) {
List<IndexedField> slots = new ArrayList<>(machines.size());
for (int zone = 0; zone < machines.size(); zone++) {
slots.add(machines.get(zone).slot("biome/carving/" + zone, selectors, data, undergroundSlot));
}
return slots;
}
private static boolean contains(IrisDimensionCarvingEntry entry, int y) {
IrisRange range = entry.getWorldYRange();
return range == null || range.contains(y);
}
private static int clampDepth(int depth) {
return depth <= 0 ? 0 : Math.min(depth, MAX_CHILD_DEPTH);
}
private record ZoneKey(int root, List<Integer> inRange) {
}
private record State(IrisDimensionCarvingEntry entry, int depth) {
}
private record Move(IrisDimensionCarvingEntry entry, List<Integer> nextByIndex) {
}
private record CarvingOption(IrisDimensionCarvingEntry entry, int rarity) implements Rarity {
@Override
public int getRarity() {
return rarity;
}
}
private record Machine(List<State> states, Map<Integer, Move> moves, int hops) {
static Machine build(IrisDimensionCarvingEntry root, int y, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
List<State> states = new ArrayList<>();
Map<State, Integer> ids = new HashMap<>();
Map<Integer, Move> moves = new HashMap<>();
int hops = clampDepth(root.getChildRecursionDepth());
State start = new State(root, terminal(root, byId, data) ? 0 : hops);
states.add(start);
ids.put(start, 0);
for (int cursor = 0; cursor < states.size(); cursor++) {
State state = states.get(cursor);
if (state.depth() <= 0) {
continue;
}
IrisRaritySelection<CarvingOption> selection = selection(state.entry(), byId, data);
if (selection.size() <= 1L) {
continue;
}
List<Integer> nextByIndex = new ArrayList<>((int) selection.size());
for (long index = 0; index < selection.size(); index++) {
IrisDimensionCarvingEntry chosen = selection.get(index).entry();
State next = chosen == state.entry() || !contains(chosen, y)
? new State(state.entry(), 0)
: new State(chosen, Math.min(state.depth() - 1, clampDepth(chosen.getChildRecursionDepth())));
Integer id = ids.get(next);
if (id == null) {
id = states.size();
ids.put(next, id);
states.add(next);
}
nextByIndex.add(id);
}
moves.put(cursor, new Move(state.entry(), List.copyOf(nextByIndex)));
}
return new Machine(List.copyOf(states), Map.copyOf(moves), hops);
}
IndexedField slot(String name, SelectorCompiler selectors, IrisData data, ToIntFunction<String> undergroundSlot) {
IndexedField current = IndexedField.constant(0);
for (int hop = 0; hop < hops; hop++) {
List<Integer> movers = new ArrayList<>();
for (int state : current.values()) {
if (moves.containsKey(state)) {
movers.add(state);
}
}
if (movers.isEmpty()) {
break;
}
Map<Integer, IndexedField> moved = new HashMap<>(movers.size() * 2);
for (int state : movers) {
Move move = moves.get(state);
IrisDimensionCarvingEntry entry = move.entry();
SelectorCompiler.Selector selector = selectors.scaled(
entry.getChildStyle(),
Math.max(MINIMUM_CHILD_SHRINK_FACTOR, entry.getChildShrinkFactor()),
CARVING_SALT + Objects.toString(entry.getId(), "").trim().hashCode());
moved.put(state, selector.partition(BiomePools.selectionCuts(move.nextByIndex().size()), move.nextByIndex()));
}
current = IndexedField.advance(current, moved).shared(name + "/hop" + (hop + 1));
}
return current.remap(state -> undergroundSlot.applyAsInt(states.get(state).entry().getRealBiome(data).getLoadKey()));
}
private static boolean terminal(IrisDimensionCarvingEntry entry, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
return entry.getChildren() == null || entry.getChildren().isEmpty() || selection(entry, byId, data).size() <= 1L;
}
private static IrisRaritySelection<CarvingOption> selection(IrisDimensionCarvingEntry parent, Map<String, IrisDimensionCarvingEntry> byId, IrisData data) {
List<CarvingOption> options = new ArrayList<>();
if (parent.getChildren() != null) {
for (String childId : parent.getChildren()) {
if (childId == null || childId.isBlank()) {
continue;
}
IrisDimensionCarvingEntry child = byId.get(childId.trim());
if (child == null || !child.isEnabled()) {
continue;
}
IrisBiome childBiome = child.getRealBiome(data);
if (childBiome != null) {
options.add(new CarvingOption(child, Math.max(childBiome.getRarity(), 1)));
}
}
}
options.add(new CarvingOption(parent, Math.max(parent.getRealBiome(data).getRarity(), 1)));
return IrisRaritySelection.create(options);
}
}
}
@@ -0,0 +1,162 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.IntToDoubleFunction;
import java.util.function.IntUnaryOperator;
record IndexedField(Field field, List<Integer> values) {
private static final Field UNUSED_CASE = new Field.Const(0.0);
IndexedField {
Objects.requireNonNull(field);
values = List.copyOf(new TreeSet<>(values));
if (values.isEmpty()) {
throw new IllegalArgumentException("An indexed field needs at least one value");
}
}
static IndexedField constant(int value) {
return new IndexedField(new Field.Const(value), List.of(value));
}
static IndexedField partition(Field input, List<Double> thresholds, List<Integer> leaves) {
if (leaves.size() != thresholds.size() + 1) {
throw new IllegalArgumentException("Partition needs " + (thresholds.size() + 1) + " leaves, got " + leaves.size());
}
List<Double> kept = new ArrayList<>(thresholds.size());
List<Integer> keptLeaves = new ArrayList<>(leaves.size());
keptLeaves.add(leaves.getFirst());
for (int index = 0; index < thresholds.size(); index++) {
int next = leaves.get(index + 1);
if (next == keptLeaves.getLast()) {
continue;
}
kept.add(thresholds.get(index));
keptLeaves.add(next);
}
if (keptLeaves.size() == 1) {
return constant(keptLeaves.getFirst());
}
List<Field> constants = new ArrayList<>(keptLeaves.size());
for (int leaf : keptLeaves) {
constants.add(new Field.Const(leaf));
}
return new IndexedField(new Field.Partition(input, kept, constants), keptLeaves);
}
static IndexedField select(IndexedField index, List<IndexedField> cases) {
if (index.isConstant()) {
return cases.get(index.values().getFirst());
}
IndexedField first = cases.get(index.values().getFirst());
boolean uniform = true;
SortedSet<Integer> reachable = new TreeSet<>();
for (int value : index.values()) {
IndexedField selected = cases.get(value);
uniform &= selected.equals(first);
reachable.addAll(selected.values());
}
if (uniform) {
return first;
}
int count = index.values().getLast() + 1;
List<Field> muxCases = new ArrayList<>(count);
for (int value = 0; value < count; value++) {
muxCases.add(index.values().contains(value) ? cases.get(value).field() : UNUSED_CASE);
}
return new IndexedField(new Field.Mux(index.field(), muxCases), List.copyOf(reachable));
}
static IndexedField advance(IndexedField current, Map<Integer, IndexedField> moved) {
int last = current.values().getLast();
List<IndexedField> cases = new ArrayList<>(last + 1);
for (int value = 0; value <= last; value++) {
IndexedField next = moved.get(value);
cases.add(next == null ? constant(value) : next);
}
return select(current, cases);
}
static IndexedField sum(List<IndexedField> terms) {
SortedSet<Integer> values = new TreeSet<>(List.of(0));
int offset = 0;
Field field = null;
for (IndexedField term : terms) {
SortedSet<Integer> combined = new TreeSet<>();
for (int value : values) {
for (int addend : term.values()) {
combined.add(value + addend);
}
}
values = combined;
if (term.isConstant()) {
offset += term.values().getFirst();
} else {
field = field == null ? term.field() : new Field.Binary(Field.BinaryOp.ADD, field, term.field());
}
}
if (field == null) {
return constant(offset);
}
Field total = offset == 0 ? field : new Field.Binary(Field.BinaryOp.ADD, field, new Field.Const(offset));
return new IndexedField(total, List.copyOf(values));
}
static List<Double> midpoints(List<Integer> sorted) {
List<Double> thresholds = new ArrayList<>(sorted.size() - 1);
for (int index = 0; index + 1 < sorted.size(); index++) {
thresholds.add((sorted.get(index) + sorted.get(index + 1)) / 2.0);
}
return thresholds;
}
boolean isConstant() {
return values.size() == 1;
}
IndexedField remap(IntUnaryOperator mapping) {
List<Integer> mapped = new ArrayList<>(values.size());
for (int value : values) {
mapped.add(mapping.applyAsInt(value));
}
if (isConstant()) {
return constant(mapped.getFirst());
}
return partition(field, midpoints(values), mapped);
}
Field lookup(IntToDoubleFunction table) {
if (isConstant()) {
return new Field.Const(table.applyAsDouble(values.getFirst()));
}
List<Double> thresholds = midpoints(values);
List<Double> keptThresholds = new ArrayList<>(thresholds.size());
List<Field> leaves = new ArrayList<>(values.size());
double previous = table.applyAsDouble(values.getFirst());
leaves.add(new Field.Const(previous));
for (int index = 1; index < values.size(); index++) {
double next = table.applyAsDouble(values.get(index));
if (next == previous) {
continue;
}
keptThresholds.add(thresholds.get(index - 1));
leaves.add(new Field.Const(next));
previous = next;
}
if (leaves.size() == 1) {
return leaves.getFirst();
}
return new Field.Partition(field, keptThresholds, leaves);
}
IndexedField shared(String name) {
return isConstant() ? this : new IndexedField(new Field.Shared(name, field), values);
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldEvaluator;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SplittableRandom;
import java.util.function.DoubleBinaryOperator;
public final class RarityCalibrator {
private static final int SAMPLES = 8192;
private static final double WINDOW_FEATURES = 96.0;
private static final double MAX_WINDOW_BLOCKS = 4_000_000.0;
private static final long POSITION_SEED = 0x5EED_B10EL;
private static final long WORLD_SEED = 0x1215_B10EL;
private final NoiseRegistry registry;
private final DataVersion target;
public RarityCalibrator(NoiseRegistry registry, DataVersion target) {
this.registry = Objects.requireNonNull(registry);
this.target = Objects.requireNonNull(target);
}
public Calibration calibrate(DoubleBinaryOperator iris, Field compiled, double featureBlocks, long salt) {
Objects.requireNonNull(iris);
Objects.requireNonNull(compiled);
double window = Math.min(MAX_WINDOW_BLOCKS, Math.max(1.0, featureBlocks) * WINDOW_FEATURES);
Map<String, NoiseDef> noises = noiseDefinitions();
SplittableRandom positions = new SplittableRandom(POSITION_SEED ^ salt);
double[] irisSamples = new double[SAMPLES];
double[] compiledSamples = new double[SAMPLES];
for (int index = 0; index < SAMPLES; index++) {
int x = (int) Math.floor((positions.nextDouble() - 0.5) * window);
int z = (int) Math.floor((positions.nextDouble() - 0.5) * window);
irisSamples[index] = iris.applyAsDouble(x, z);
compiledSamples[index] = compiled instanceof Field.Const constant
? constant.value()
: FieldEvaluator.sample(compiled, noises, WORLD_SEED, target, x, 0, z);
}
Arrays.sort(irisSamples);
Arrays.sort(compiledSamples);
return new Calibration(irisSamples, compiledSamples);
}
private Map<String, NoiseDef> noiseDefinitions() {
List<NoiseDef> definitions = registry.definitions();
Map<String, NoiseDef> noises = new HashMap<>(definitions.size() * 2);
for (NoiseDef definition : definitions) {
noises.put(registry.namespace() + ":" + definition.id(), definition);
}
return noises;
}
public static final class Calibration {
private final double[] iris;
private final double[] compiled;
private Calibration(double[] iris, double[] compiled) {
this.iris = iris;
this.compiled = compiled;
}
public double cumulative(double irisValue) {
int low = 0;
int high = iris.length;
while (low < high) {
int middle = (low + high) >>> 1;
if (iris[middle] < irisValue) {
low = middle + 1;
} else {
high = middle;
}
}
return low / (double) iris.length;
}
public double threshold(double probability) {
return quantile(compiled, probability);
}
public double irisQuantile(double probability) {
return quantile(iris, probability);
}
public List<Double> thresholds(List<Double> irisCuts) {
List<Double> thresholds = new ArrayList<>(irisCuts.size());
for (double cut : irisCuts) {
thresholds.add(threshold(cumulative(cut)));
}
return thresholds;
}
private static double quantile(double[] sorted, double probability) {
double position = Math.clamp(probability, 0.0, 1.0) * sorted.length - 0.5;
if (position <= 0.0) {
return sorted[0];
}
if (position >= sorted.length - 1) {
return sorted[sorted.length - 1];
}
int lower = (int) Math.floor(position);
double fraction = position - lower;
return sorted[lower] + fraction * (sorted[lower + 1] - sorted[lower]);
}
}
}
@@ -0,0 +1,184 @@
package art.arcane.iris.pack.datapack.export.compile.biome;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.DoubleBinaryOperator;
public final class SelectorCompiler {
private static final double COHERENT_BLOCKS = 64.0;
private static final long CALIBRATION_SEED = 0x1215_5E1EL;
private static final int CURVE_KNOTS = 9;
private static final String SHARED_PREFIX = "biome/selector/";
private final StyleCompiler styles;
private final IrisData data;
private final NoiseRegistry registry;
private final ExportCapabilities capabilities;
private final RarityCalibrator calibrator;
private final Map<SelectorKey, Selector> selectors = new HashMap<>();
private int shared;
public SelectorCompiler(StyleCompiler styles, Sources sources) {
this.styles = Objects.requireNonNull(styles);
Objects.requireNonNull(sources);
data = sources.data();
registry = sources.registry();
capabilities = sources.capabilities();
calibrator = new RarityCalibrator(registry, capabilities.target());
}
public Selector zoomed(IrisGeneratorStyle style, double zoom, long salt) {
return selectors.computeIfAbsent(new SelectorKey(style, false, zoom, salt), this::create);
}
public Selector scaled(IrisGeneratorStyle style, double scale, long salt) {
return selectors.computeIfAbsent(new SelectorKey(style, true, scale, salt), this::create);
}
public Selector sampled(IrisGeneratorStyle style, double zoom, long salt, DoubleBinaryOperator iris) {
IrisGeneratorStyle exported = exportable(style);
exported.setZoom(exported.getZoom() * zoom);
return compile(exported, salt, iris, COHERENT_BLOCKS * exported.getZoom());
}
private Selector create(SelectorKey key) {
IrisGeneratorStyle exported = exportable(key.style());
RNG seed = new RNG(CALIBRATION_SEED).nextParallelRNG(key.salt());
if (key.scaled()) {
CNG iris = exported.createScaledGenerator(seed, data, key.factor());
IrisGeneratorStyle compiled = scaledCopy(exported, key.factor());
return compile(compiled, key.salt(), iris::noiseFast2D, COHERENT_BLOCKS * compiled.getZoom());
}
CNG iris = exported.create(seed, data);
double zoom = key.factor();
IrisGeneratorStyle compiled = copy(exported);
compiled.setZoom(exported.getZoom() * zoom);
return compile(compiled, key.salt(), (x, z) -> iris.noise(x / zoom, z / zoom), COHERENT_BLOCKS * compiled.getZoom());
}
private Selector compile(IrisGeneratorStyle style, long salt, DoubleBinaryOperator iris, double featureBlocks) {
Field field = styles.compile(style, salt, registry, capabilities);
FieldValidator.requireTwoDimensional(field, "biome selector");
RarityCalibrator.Calibration calibration = calibrator.calibrate(iris, field, featureBlocks, salt);
Field selector = field instanceof Field.Const ? field : new Field.Shared(SHARED_PREFIX + shared++, field);
return new Selector(selector, calibration);
}
private static IrisGeneratorStyle exportable(IrisGeneratorStyle style) {
IrisGeneratorStyle copy = copy(style);
for (IrisGeneratorStyle current = copy; current != null; current = current.getFracture()) {
current.setImageMap(null);
current.setCacheSize(0);
}
return copy;
}
private static IrisGeneratorStyle scaledCopy(IrisGeneratorStyle style, double scale) {
IrisGeneratorStyle copy = copy(style);
copy.setZoom(style.getZoom() / scale);
IrisGeneratorStyle fracture = copy.getFracture();
if (fracture != null) {
fracture.setZoom(fracture.getZoom() * scale);
fracture.setMultiplier(fracture.getMultiplier() * scale);
}
return copy;
}
private static IrisGeneratorStyle copy(IrisGeneratorStyle style) {
IrisGeneratorStyle copy = new IrisGeneratorStyle(style.getStyle());
copy.setCellularFrequency(style.getCellularFrequency());
copy.setCellularZoom(style.getCellularZoom());
copy.setZoom(style.getZoom());
copy.setExpression(style.getExpression());
copy.setImageMap(style.getImageMap());
copy.setMultiplier(style.getMultiplier());
copy.setExponent(style.getExponent());
copy.setCacheSize(style.getCacheSize());
copy.setFracture(style.getFracture() == null ? null : copy(style.getFracture()));
return copy;
}
public record Sources(IrisData data, NoiseRegistry registry, ExportCapabilities capabilities) {
public Sources {
Objects.requireNonNull(data);
Objects.requireNonNull(registry);
Objects.requireNonNull(capabilities);
}
}
public record Selector(Field field, RarityCalibrator.Calibration calibration) {
public Selector {
Objects.requireNonNull(field);
Objects.requireNonNull(calibration);
}
IndexedField partition(List<Double> irisCuts, List<Integer> leaves) {
if (leaves.size() != irisCuts.size() + 1) {
throw new IllegalArgumentException("Selector partition needs " + (irisCuts.size() + 1) + " leaves, got " + leaves.size());
}
List<Double> upper = new ArrayList<>(leaves.size());
List<Integer> kept = new ArrayList<>(leaves.size());
double lower = 0.0;
for (int index = 0; index < leaves.size(); index++) {
double bound = index == irisCuts.size() ? 1.0 : Math.max(lower, calibration.cumulative(irisCuts.get(index)));
if (bound > lower) {
upper.add(bound);
kept.add(leaves.get(index));
}
lower = bound;
}
if (kept.isEmpty()) {
return IndexedField.constant(leaves.getLast());
}
List<Double> thresholds = new ArrayList<>(kept.size() - 1);
for (int index = 0; index + 1 < kept.size(); index++) {
thresholds.add(calibration.threshold(upper.get(index)));
}
return IndexedField.partition(field, thresholds, kept);
}
Field curve() {
List<Field.Knot> knots = new ArrayList<>(CURVE_KNOTS);
double previousLocation = Double.NEGATIVE_INFINITY;
List<double[]> points = new ArrayList<>(CURVE_KNOTS);
for (int index = 0; index < CURVE_KNOTS; index++) {
double probability = (index + 0.5) / CURVE_KNOTS;
double location = calibration.threshold(probability);
if (location <= previousLocation) {
continue;
}
points.add(new double[]{location, calibration.irisQuantile(probability)});
previousLocation = location;
}
if (points.size() == 1) {
return new Field.Const(points.getFirst()[1]);
}
for (int index = 0; index < points.size(); index++) {
double derivative = 0.0;
if (index > 0 && index + 1 < points.size()) {
double[] before = points.get(index - 1);
double[] after = points.get(index + 1);
derivative = (after[1] - before[1]) / (after[0] - before[0]);
}
knots.add(new Field.Knot(points.get(index)[0], new Field.Const(points.get(index)[1]), derivative));
}
return new Field.Curve(field, knots);
}
}
private record SelectorKey(IrisGeneratorStyle style, boolean scaled, double factor, long salt) {
}
}
@@ -0,0 +1,119 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.pack.datapack.export.ir.AquiferDef;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
public final class AquiferCompiler {
private static final int LAVA_LIMIT_Y = -10;
private static final int LAVA_CENTER_MARGIN = 12;
private static final int FLUID_BAND_HEIGHT = 40;
private static final int FLUID_BAND_CENTER = 20;
private static final double SPREAD_LEVEL_SCALE = 10.0;
private static final double LEVEL_ROUNDING_MARGIN = 0.5;
private static final Field DRY = new Field.Const(-1.0);
private static final Field PARTIAL = new Field.Const(0.6);
private static final Field LOW_BARRIER = new Field.Const(-1.0);
private static final Field NO_EXCLUSION = new Field.Const(-1.0);
private static final Field LAVA = new Field.Const(1.0);
private static final Field NO_LAVA = new Field.Const(0.0);
private static final Field OUTSIDE_BAND = new Field.Const(0.0);
private static final Field INSIDE_BAND = new Field.Const(1.0);
private final CaveInputs inputs;
private final Field blockY;
private final int lowFluidBand;
private final int highFluidBand;
public AquiferCompiler(CaveInputs inputs) {
this.inputs = Objects.requireNonNull(inputs);
this.blockY = inputs.blockY();
this.lowFluidBand = Math.floorDiv(inputs.minY(), FLUID_BAND_HEIGHT) - 1;
this.highFluidBand = Math.floorDiv(inputs.maxY(), FLUID_BAND_HEIGHT) + 1;
}
public AquiferDef compile(CaveSelection selection) {
AquiferDef dry = AquiferDef.dry(inputs.aquiferSurface());
boolean lavaWanted = any(selection, IrisCaveProfile::isAllowLava);
boolean lavaExported = lavaWanted && inputs.lavaY() <= LAVA_LIMIT_Y;
if (lavaWanted && !lavaExported) {
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.LAVA_HEIGHT, inputs.resource(),
"Cave lava height Y " + inputs.lavaY() + " is above Y " + LAVA_LIMIT_Y + "; caves export without lava"));
}
boolean wet = any(selection, IrisCaveProfile::isAllowFluid);
if (!lavaExported && !wet) {
return dry;
}
Field spread = lavaExported ? lavaSpread(wet, dry.spread()) : dry.spread();
Field lava = lavaExported ? lavaBands() : NO_LAVA;
return new AquiferDef(LOW_BARRIER, floodedness(selection, lavaExported), spread, lava, NO_EXCLUSION, inputs.aquiferSurface());
}
private Field floodedness(CaveSelection selection, boolean lavaExported) {
Field flood = selection.profiles().isEmpty() ? DRY : selection.select(source -> flood(source, lavaExported), DRY);
for (CaveSource band : selection.bands()) {
Field banded = flood(band, lavaExported);
if (banded.equals(flood)) {
continue;
}
Field inside = new Field.Partition(blockY, List.of((double) band.minY(), band.maxY() + 1.0), List.of(OUTSIDE_BAND, INSIDE_BAND, OUTSIDE_BAND));
flood = new Field.Mux(inside, List.of(flood, banded));
}
return flood;
}
private Field flood(CaveSource source, boolean lavaExported) {
IrisCaveProfile profile = source.profile();
int top = Integer.MIN_VALUE;
if (lavaExported && profile.isAllowLava()) {
top = inputs.lavaY() + LAVA_CENTER_MARGIN;
}
if (profile.isAllowFluid()) {
top = Math.max(top, inputs.fluidHeight());
}
if (top < inputs.minY()) {
return DRY;
}
if (top >= inputs.maxY()) {
return PARTIAL;
}
return new Field.RangeIf(blockY, inputs.minY(), top + 1.0, PARTIAL, DRY);
}
private Field lavaSpread(boolean wet, Field vanillaSpread) {
Field levels = new Field.Coord(Axis.Y, Field.Tiling.CLAMP, lowFluidBand, highFluidBand, spreadAt(lowFluidBand), spreadAt(highFluidBand));
return wet ? new Field.RangeIf(fluidBand(), lowFluidBand, lavaBand() + 1.0, levels, vanillaSpread) : levels;
}
private Field lavaBands() {
return new Field.RangeIf(fluidBand(), lowFluidBand, lavaBand() + 1.0, LAVA, NO_LAVA);
}
private Field fluidBand() {
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, lowFluidBand, highFluidBand, lowFluidBand, highFluidBand);
}
private int lavaBand() {
return Math.floorDiv(inputs.lavaY() + LAVA_CENTER_MARGIN, FLUID_BAND_HEIGHT);
}
private double spreadAt(int band) {
return (inputs.lavaY() - FLUID_BAND_CENTER + LEVEL_ROUNDING_MARGIN) / SPREAD_LEVEL_SCALE - band * (FLUID_BAND_HEIGHT / SPREAD_LEVEL_SCALE);
}
private static boolean any(CaveSelection selection, Predicate<IrisCaveProfile> property) {
for (CaveSource source : selection.sources()) {
if (property.test(source.profile())) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class CaveFieldCompiler {
static final int CELL_XZ = 4;
static final int CELL_Y = 4;
static final double WALL_BAND = 0.03125;
private static final double MIN_GAIN = 1.0 / 64.0;
private static final double MAX_GAIN = 64.0;
private static final Field UNCOVERED = new Field.Const(CaveProfileCompiler.SOLID);
private final CaveProfileCompiler profiles;
private final CaveInputs inputs;
public CaveFieldCompiler(StyleCompiler styles, CaveInputs inputs) {
this.profiles = new CaveProfileCompiler(styles, inputs);
this.inputs = inputs;
}
public Optional<Field> compile(CaveSelection selection) {
return union(selection).map(union -> new Field.Grid(union, CELL_XZ, CELL_Y));
}
private Optional<Field> union(CaveSelection selection) {
List<Field> parts = new ArrayList<>(selection.bands().size() + 1);
if (!selection.profiles().isEmpty()) {
parts.add(selection.select(this::normalized, UNCOVERED));
}
for (CaveSource band : selection.bands()) {
parts.add(normalized(band));
}
if (parts.isEmpty()) {
return Optional.empty();
}
Field union = parts.getFirst();
for (int index = 1; index < parts.size(); index++) {
union = new Field.Binary(Field.BinaryOp.MIN, union, parts.get(index));
}
return Optional.of(union);
}
private Field normalized(CaveSource source) {
return profiles.compile(source, gain(source));
}
private double gain(CaveSource source) {
Optional<CaveInterior> interior = profiles.interior(source);
if (interior.isEmpty()) {
return 1.0;
}
double band = WallShellCalibrator.band(interior.get(), inputs.noises().byReference(), inputs.capabilities().target());
return band > 0.0 ? Math.clamp(WALL_BAND / band, MIN_GAIN, MAX_GAIN) : 1.0;
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.noise.NoiseRegistry;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldValidator;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import java.util.List;
import java.util.Objects;
public record CaveInputs(
String resource,
int minY,
int maxY,
int fluidHeight,
int lavaY,
Field surface,
Field aquiferSurface,
NoiseRegistry noises,
ExportCapabilities capabilities,
List<ExportDiagnostic> diagnostics
) {
public CaveInputs {
Objects.requireNonNull(resource);
Objects.requireNonNull(surface);
Objects.requireNonNull(aquiferSurface);
Objects.requireNonNull(noises);
Objects.requireNonNull(capabilities);
Objects.requireNonNull(diagnostics);
if (maxY <= minY) {
throw new IllegalArgumentException("Cave bounds need maxY above minY, got " + minY + ".." + maxY);
}
FieldValidator.requireTwoDimensional(surface, "cave surface");
FieldValidator.requireTwoDimensional(aquiferSurface, "aquifer surface");
}
public static CaveInputs of(PackCompileContext context) {
IrisDimension dimension = context.dimension();
int minY = dimension.getMinHeight();
return new CaveInputs(
"dimensions/" + dimension.getLoadKey(),
minY,
dimension.getMaxHeight() - 1,
context.dimensionDef().fluidHeight(),
minY + dimension.getCaveLavaHeight(),
context.height(),
context.surfaceEstimate(),
context.noises(),
context.capabilities(),
context.diagnostics());
}
public Field blockY() {
return new Field.Coord(Axis.Y, Field.Tiling.CLAMP, minY, maxY + 1, minY, maxY + 1);
}
}
@@ -0,0 +1,14 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.Objects;
record CaveInterior(Field field, int minY, int maxY) {
CaveInterior {
Objects.requireNonNull(field);
if (maxY < minY) {
throw new IllegalArgumentException("Cave interior needs maxY at or above minY, got " + minY + ".." + maxY);
}
}
}
@@ -0,0 +1,294 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.pack.datapack.export.compile.noise.StyleCompiler;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.value.IrisRange;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public final class CaveProfileCompiler {
static final double SOLID = 1.0;
private static final long BASE_SALT = 934_447L;
private static final long DETAIL_SALT = 612_991L;
private static final long THRESHOLD_SALT = 489_112L;
private static final long SURFACE_BREAK_SALT = 341_219L;
private static final long MODULE_SALT = 1_000_003L;
private static final long MODULE_SALT_STEP = 65_537L;
private static final double LOW_WEIGHT = 0.1;
private static final int CEILING_FADE_DEPTH = 12;
private static final double CLOSURE_EPSILON = 1.0e-6;
private static final double SEALED_MARGIN = 1.0;
private static final Set<NoiseStyle> CELLULAR_STYLES = cellularStyles();
private final StyleCompiler styles;
private final CaveInputs inputs;
private final Field blockY;
public CaveProfileCompiler(StyleCompiler styles, CaveInputs inputs) {
this.styles = Objects.requireNonNull(styles);
this.inputs = Objects.requireNonNull(inputs);
this.blockY = inputs.blockY();
}
public Field compile(CaveSource source, double gain) {
if (!(gain > 0.0) || Double.isInfinite(gain)) {
throw new IllegalArgumentException("Cave field gain must be finite and positive, got " + gain);
}
IrisCaveProfile profile = source.profile();
IrisRange range = profile.getVerticalRange();
int floor = floor(inputs, source);
int ceiling = Math.min(source.maxY(), Math.min(inputs.maxY(), inputs.minY() + (int) Math.ceil(range.getMax())));
if (ceiling < floor) {
return new Field.Const(SOLID);
}
double normalization = normalization(profile) / gain;
List<Field> terms = densityTerms(source, normalization, floor, ceiling);
edgeFade(profile, floor, ceiling, gain).ifPresent(terms::add);
terms.add(surface(source, closure(profile, normalization), gain));
return bounded(sum(terms), floor, ceiling);
}
Optional<CaveInterior> interior(CaveSource source) {
IrisCaveProfile profile = source.profile();
int floor = floor(inputs, source);
int ceiling = Math.min(source.maxY(), Math.min(inputs.maxY(), inputs.minY() + (int) Math.ceil(profile.getVerticalRange().getMax())));
if (ceiling < floor) {
return Optional.empty();
}
List<Field> terms = densityTerms(source, normalization(profile), floor, ceiling);
terms.add(negativeThreshold(profile, 1.0));
return Optional.of(new CaveInterior(sum(terms), floor, ceiling));
}
static int floor(CaveInputs inputs, CaveSource source) {
return Math.max(source.minY(), inputs.minY() + Math.max(0, (int) Math.floor(source.profile().getVerticalRange().getMin())));
}
static boolean cellular(IrisGeneratorStyle style) {
if (style.getExpression() != null || style.getImageMap() != null) {
return false;
}
if (style.getCellularFrequency() > 0.0 || CELLULAR_STYLES.contains(style.getStyle())) {
return true;
}
return style.getFracture() != null && cellular(style.getFracture());
}
private List<Field> densityTerms(CaveSource source, double normalization, int floor, int ceiling) {
IrisCaveProfile profile = source.profile();
List<Field> terms = new ArrayList<>();
addTerm(terms, source, new Term(profile.getBaseDensityStyle(), BASE_SALT, profile.getBaseWeight(), 0.0, false), normalization);
addTerm(terms, source, new Term(profile.getDetailDensityStyle(), DETAIL_SALT, profile.getDetailWeight(), 0.0, false), normalization);
List<IrisCaveFieldModule> modules = profile.getModules();
for (int index = 0; index < modules.size(); index++) {
IrisCaveFieldModule module = modules.get(index);
IrisRange range = module.getVerticalRange();
int low = inputs.minY() + (int) Math.floor(range.getMin());
int high = inputs.minY() + (int) Math.ceil(range.getMax());
if (module.getWeight() == 0.0 || high < floor || low > ceiling) {
continue;
}
Field term = term(source, new Term(module.getStyle(), MODULE_SALT + index * MODULE_SALT_STEP, module.getWeight(), module.getThreshold(), module.isInvert()), normalization);
terms.add(low <= floor && high >= ceiling ? term : new Field.Gate(window(low, high), term));
}
return terms;
}
private void addTerm(List<Field> terms, CaveSource source, Term term, double normalization) {
if (term.weight() != 0.0) {
terms.add(term(source, term, normalization));
}
}
private Field term(CaveSource source, Term term, double normalization) {
double sign = term.invert() ? -1.0 : 1.0;
double scale = sign * term.weight() / normalization;
Field unit = volume(source, term.style(), term.salt(), Math.abs(term.weight()) < LOW_WEIGHT);
return affine(unit, 2.0 * scale, -scale * (1.0 + term.threshold()));
}
private Field volume(CaveSource source, IrisGeneratorStyle style, long salt, boolean lowWeight) {
boolean cellularStyle = cellular(style);
if (cellularStyle && lowWeight) {
return VolumeLift.lift(styles.compile(substitute(style), salt, inputs.noises(), inputs.capabilities()));
}
Field compiled = styles.compile(style, salt, inputs.noises(), inputs.capabilities());
if (compiled.axes().contains(Axis.Y)) {
return compiled;
}
if (!cellularStyle) {
return VolumeLift.lift(compiled);
}
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.NOISE_STYLE, source.resource(),
"Cellular cave style " + style.getStyle() + " exports as smooth noise"));
return VolumeLift.lift(styles.compile(substitute(style), salt, inputs.noises(), inputs.capabilities()));
}
private Field planar(IrisGeneratorStyle style, long salt) {
Field compiled = styles.compile(style, salt, inputs.noises(), inputs.capabilities());
return compiled.axes().contains(Axis.Y) ? new Field.Slice(compiled, Axis.Y, 0) : compiled;
}
private Field surface(CaveSource source, double closure, double gain) {
IrisCaveProfile profile = source.profile();
Field negativeThreshold = negativeThreshold(profile, gain);
Field depthBelowClearance = new Field.Binary(Field.BinaryOp.SUB,
new Field.Binary(Field.BinaryOp.ADD, inputs.surface(), new Field.Const(-Math.max(0, profile.getSurfaceClearance()))),
blockY);
Field sealed = new Field.Curve(depthBelowClearance, List.of(
new Field.Knot(-1.0, new Field.Const(SEALED_MARGIN * gain - closure), 0.0),
new Field.Knot(0.0, new Field.Const(-closure), 0.0),
new Field.Knot(CEILING_FADE_DEPTH, negativeThreshold, 0.0)));
if (!profile.isAllowSurfaceBreak()) {
return sealed;
}
Field breaking = new Field.Binary(Field.BinaryOp.ADD, breakSteps(profile, closure, gain), negativeThreshold);
Field breakNoise = planar(profile.getSurfaceBreakStyle(), SURFACE_BREAK_SALT);
Field cutoff = new Field.Const((profile.getSurfaceBreakNoiseThreshold() + 1.0) / 2.0);
return new Field.Binary(Field.BinaryOp.ADD,
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, cutoff, breakNoise), sealed),
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, breakNoise, cutoff), breaking));
}
private Field breakSteps(IrisCaveProfile profile, double closure, double gain) {
int depth = Math.max(0, profile.getSurfaceBreakDepth());
double boost = Math.max(0.0, profile.getSurfaceBreakThresholdBoost()) * gain;
Field belowSurface = new Field.Binary(Field.BinaryOp.SUB, inputs.surface(), blockY);
List<Field.Knot> knots = new ArrayList<>();
knots.add(new Field.Knot(-1.0, new Field.Const((thresholdCeiling(profile) + SEALED_MARGIN) * gain - closure + boost), 0.0));
knots.add(new Field.Knot(0.0, new Field.Const(-boost), 0.0));
if (boost > 0.0) {
if (depth > 0) {
knots.add(new Field.Knot(depth, new Field.Const(-boost), 0.0));
}
knots.add(new Field.Knot(depth + 1.0, new Field.Const(0.0), 0.0));
}
return new Field.Curve(belowSurface, knots);
}
private Field negativeThreshold(IrisCaveProfile profile, double gain) {
IrisStyledRange range = profile.getDensityThreshold();
double bias = profile.getThresholdBias();
if (range.getMin() == range.getMax()) {
return new Field.Const(gain * (bias - range.getMin()));
}
if (range.getStyle().isFlat()) {
return new Field.Const(gain * (bias - (range.getMin() + 0.5 * (range.getMax() - range.getMin()))));
}
return affine(planar(range.getStyle(), THRESHOLD_SALT), gain * (range.getMin() - range.getMax()), gain * (bias - range.getMin()));
}
private Optional<Field> edgeFade(IrisCaveProfile profile, int floor, int ceiling, double gain) {
int fade = Math.max(0, profile.getVerticalEdgeFade());
double strength = Math.max(0.0, profile.getVerticalEdgeFadeStrength()) * gain;
if (fade <= 0 || strength <= 0.0 || ceiling <= floor) {
return Optional.empty();
}
List<Field.Knot> knots = new ArrayList<>();
knots.add(new Field.Knot(floor, new Field.Const(strength), 0.0));
if (floor + fade <= ceiling - fade) {
knots.add(new Field.Knot(floor + fade, new Field.Const(0.0), 0.0));
if (ceiling - fade > floor + fade) {
knots.add(new Field.Knot(ceiling - fade, new Field.Const(0.0), 0.0));
}
} else {
double middle = floor + (ceiling - floor) / 2.0;
double progress = Math.min(middle - floor, ceiling - middle) / fade;
knots.add(new Field.Knot(middle, new Field.Const((1.0 - smoothstep(progress)) * strength), 0.0));
}
knots.add(new Field.Knot(ceiling, new Field.Const(strength), 0.0));
return Optional.of(new Field.Curve(blockY, knots));
}
private Field bounded(Field core, int floor, int ceiling) {
if (floor <= inputs.minY() && ceiling >= inputs.maxY()) {
return core;
}
Field shifted = new Field.Binary(Field.BinaryOp.ADD, core, new Field.Const(-SOLID));
return new Field.Binary(Field.BinaryOp.ADD, new Field.Gate(window(floor, ceiling), shifted), new Field.Const(SOLID));
}
private Field window(int low, int high) {
return new Field.RangeIf(blockY, low, high + 1.0, new Field.Const(1.0), new Field.Const(0.0));
}
private static Field affine(Field input, double scale, double offset) {
if (input instanceof Field.Const constant) {
return new Field.Const(scale * constant.value() + offset);
}
if (scale == 0.0) {
return new Field.Const(offset);
}
return new Field.Curve(input, List.of(
new Field.Knot(0.0, new Field.Const(offset), scale),
new Field.Knot(1.0, new Field.Const(scale + offset), scale)));
}
private static Field sum(List<Field> terms) {
Field total = terms.getFirst();
for (int index = 1; index < terms.size(); index++) {
total = new Field.Binary(Field.BinaryOp.ADD, total, terms.get(index));
}
return total;
}
private static double normalization(IrisCaveProfile profile) {
double weight = Math.abs(profile.getBaseWeight()) + Math.abs(profile.getDetailWeight());
for (IrisCaveFieldModule module : profile.getModules()) {
weight += Math.abs(module.getWeight());
}
return weight <= 0.0 ? 1.0 : weight;
}
private static double closure(IrisCaveProfile profile, double normalization) {
double minimum = -Math.abs(profile.getBaseWeight()) - Math.abs(profile.getDetailWeight());
for (IrisCaveFieldModule module : profile.getModules()) {
double threshold = module.getThreshold();
double low = module.isInvert() ? threshold - 1.0 : -1.0 - threshold;
double high = module.isInvert() ? threshold + 1.0 : 1.0 - threshold;
minimum += Math.min(low * module.getWeight(), high * module.getWeight());
}
return minimum / normalization - CLOSURE_EPSILON;
}
private static double thresholdCeiling(IrisCaveProfile profile) {
IrisStyledRange range = profile.getDensityThreshold();
return Math.max(range.getMin(), range.getMax()) - profile.getThresholdBias();
}
private static double smoothstep(double progress) {
double clamped = Math.max(0.0, Math.min(1.0, progress));
return clamped * clamped * (3.0 - 2.0 * clamped);
}
private static IrisGeneratorStyle substitute(IrisGeneratorStyle style) {
return new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(style.getZoom());
}
private static Set<NoiseStyle> cellularStyles() {
Set<NoiseStyle> cellular = EnumSet.noneOf(NoiseStyle.class);
for (NoiseStyle style : NoiseStyle.values()) {
String name = style.name();
if (name.contains("CELL") || name.contains("VASCULAR") || name.contains("GLOB") || name.contains("HEX")) {
cellular.add(style);
}
}
return cellular;
}
private record Term(IrisGeneratorStyle style, long salt, double weight, double threshold, boolean invert) {
}
}
@@ -0,0 +1,185 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.compile.biome.BiomeCodes;
import art.arcane.iris.pack.datapack.export.ir.ClimateLayout;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
public record CaveSelection(Field index, List<CaveSource> profiles, boolean uncovered, List<CaveSource> bands) {
private static final int UNCOVERED = -1;
private static final Field NO_INDEX = new Field.Const(0.0);
private static final Comparator<ClimateLayout.ClimateEntry> CODE_ORDER = Comparator
.comparingDouble(ClimateLayout.ClimateEntry::min)
.thenComparingDouble(ClimateLayout.ClimateEntry::max)
.thenComparing(ClimateLayout.ClimateEntry::biomeId);
public CaveSelection {
Objects.requireNonNull(index);
profiles = List.copyOf(profiles);
bands = List.copyOf(bands);
}
public static CaveSelection resolve(CaveInputs inputs, IrisDimension dimension, IrisData data, BiomeCodes codes) {
if (!dimension.isCarvingEnabled() || dimension.getDisabledComponents().contains(ReservedFlag.CARVED)) {
return new CaveSelection(NO_INDEX, List.of(), false, List.of());
}
Map<IrisCaveProfile, Integer> caseByProfile = new LinkedHashMap<>();
List<CaveSource> sources = new ArrayList<>();
IrisCaveProfile dimensionProfile = dimension.getCaveProfile();
int fallback = enabled(dimensionProfile)
? caseOf(source(inputs, inputs.resource(), dimensionProfile), caseByProfile, sources)
: UNCOVERED;
List<Double> thresholds = new ArrayList<>();
List<Integer> leaves = new ArrayList<>();
leaves.add(fallback);
List<ClimateLayout.ClimateEntry> entries = caveEntries(codes, regionCaveBiomes(dimension, data));
for (int position = 0; position < entries.size(); position++) {
ClimateLayout.ClimateEntry entry = entries.get(position);
Optional<CaveSource> source = entrySource(inputs, dimension, data, codes.loadKeyByBiomeId().get(entry.biomeId()));
int selected = source.isPresent() ? caseOf(source.get(), caseByProfile, sources) : fallback;
append(thresholds, leaves, entry.min(), selected);
double nextMin = position + 1 < entries.size() ? entries.get(position + 1).min() : Double.POSITIVE_INFINITY;
if (entry.max() < nextMin) {
append(thresholds, leaves, entry.max(), fallback);
}
}
boolean uncovered = leaves.contains(UNCOVERED);
int uncoveredCase = sources.size();
List<Field> constants = new ArrayList<>(leaves.size());
for (int leaf : leaves) {
constants.add(new Field.Const(leaf == UNCOVERED ? uncoveredCase : leaf));
}
Field index = thresholds.isEmpty() ? constants.getFirst() : new Field.Partition(codes.caveCode(), thresholds, constants);
return new CaveSelection(index, sources, uncovered, bands(inputs, dimension, data));
}
public boolean isEmpty() {
return profiles.isEmpty() && bands.isEmpty();
}
public List<CaveSource> sources() {
List<CaveSource> all = new ArrayList<>(profiles.size() + bands.size());
all.addAll(profiles);
all.addAll(bands);
return all;
}
public Field select(Function<CaveSource, Field> compiler, Field uncoveredCase) {
List<Field> cases = new ArrayList<>(profiles.size() + 1);
for (CaveSource source : profiles) {
cases.add(compiler.apply(source));
}
if (uncovered || cases.isEmpty()) {
cases.add(uncoveredCase);
}
return cases.size() == 1 ? cases.getFirst() : new Field.Mux(index, cases);
}
private static void append(List<Double> thresholds, List<Integer> leaves, double start, int selected) {
if (leaves.getLast() == selected) {
return;
}
thresholds.add(start);
leaves.add(selected);
}
private static List<ClimateLayout.ClimateEntry> caveEntries(BiomeCodes codes, Set<String> caveBiomes) {
List<ClimateLayout.ClimateEntry> entries = new ArrayList<>();
for (ClimateLayout.ClimateEntry entry : codes.layout().entries()) {
if (entry.axis() == ClimateLayout.ClimateAxis.DEPTH && caveBiomes.contains(codes.loadKeyByBiomeId().get(entry.biomeId()))) {
entries.add(entry);
}
}
entries.sort(CODE_ORDER);
return entries;
}
private static Set<String> regionCaveBiomes(IrisDimension dimension, IrisData data) {
Set<String> caveBiomes = new HashSet<>();
for (String regionKey : dimension.getRegions()) {
IrisRegion region = regionKey == null ? null : data.getRegionLoader().load(regionKey, false);
if (region != null && !region.isCompatExcluded()) {
caveBiomes.addAll(region.getCaveBiomes());
}
}
return caveBiomes;
}
private static Optional<CaveSource> entrySource(CaveInputs inputs, IrisDimension dimension, IrisData data, String loadKey) {
IrisBiome biome = loadKey == null ? null : data.getBiomeLoader().load(loadKey, false);
if (biome == null || biome.isCompatExcluded()) {
return Optional.empty();
}
if (enabled(biome.getCaveProfile())) {
return Optional.of(source(inputs, "biomes/" + biome.getLoadKey(), biome.getCaveProfile()));
}
for (String regionKey : dimension.getRegions()) {
IrisRegion region = regionKey == null ? null : data.getRegionLoader().load(regionKey, false);
if (region == null || region.isCompatExcluded() || !region.getCaveBiomes().contains(loadKey) || !enabled(region.getCaveProfile())) {
continue;
}
return Optional.of(source(inputs, "regions/" + region.getLoadKey(), region.getCaveProfile()));
}
return Optional.empty();
}
private static List<CaveSource> bands(CaveInputs inputs, IrisDimension dimension, IrisData data) {
List<CaveSource> bands = new ArrayList<>();
List<IrisDimensionCarvingEntry> entries = dimension.getCarving();
if (entries == null) {
return bands;
}
for (IrisDimensionCarvingEntry entry : entries) {
if (entry == null || !entry.isEnabled()) {
continue;
}
IrisBiome biome = entry.getRealBiome(data);
if (biome == null || !enabled(biome.getCaveProfile())) {
continue;
}
IrisRange range = entry.getWorldYRange();
int low = range == null ? inputs.minY() : Math.max(inputs.minY(), (int) Math.ceil(range.getMin()));
int high = range == null ? inputs.maxY() : Math.min(inputs.maxY(), (int) Math.floor(range.getMax()));
if (high >= low) {
bands.add(new CaveSource("biomes/" + biome.getLoadKey(), biome.getCaveProfile(), low, high));
}
}
return bands;
}
private static int caseOf(CaveSource source, Map<IrisCaveProfile, Integer> caseByProfile, List<CaveSource> sources) {
Integer existing = caseByProfile.get(source.profile());
if (existing != null) {
return existing;
}
caseByProfile.put(source.profile(), sources.size());
sources.add(source);
return sources.size() - 1;
}
private static CaveSource source(CaveInputs inputs, String resource, IrisCaveProfile profile) {
return new CaveSource(resource, profile, inputs.minY(), inputs.maxY());
}
private static boolean enabled(IrisCaveProfile profile) {
return profile != null && profile.isEnabled();
}
}
@@ -0,0 +1,15 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import java.util.Objects;
public record CaveSource(String resource, IrisCaveProfile profile, int minY, int maxY) {
public CaveSource {
Objects.requireNonNull(resource);
Objects.requireNonNull(profile);
if (maxY < minY) {
throw new IllegalArgumentException("Cave source '" + resource + "' needs maxY at or above minY, got " + minY + ".." + maxY);
}
}
}
@@ -0,0 +1,61 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.compile.CompileStage;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import art.arcane.iris.pack.datapack.export.ir.Field;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class CaveStage implements CompileStage {
private static final String CARVERS = "carvers";
@Override
public String name() {
return "caves";
}
@Override
public void apply(PackCompileContext context) {
CaveInputs inputs = CaveInputs.of(context);
CaveSelection selection = CaveSelection.resolve(inputs, context.dimension(), context.data(), context.biomeCodes());
CaveFieldCompiler compiler = new CaveFieldCompiler(context.styles(), inputs);
Optional<Field> caves = compiler.compile(selection);
if (caves.isPresent()) {
Field.Shared field = new Field.Shared(DeepCaveCompiler.FIELD, caves.get());
Optional<DeepCaveCompiler.DeepCaves> deep = new DeepCaveCompiler(inputs).compile(field, selection);
context.setCaveField(field, CaveFieldCompiler.WALL_BAND);
context.setCaves(deep.map(DeepCaveCompiler.DeepCaves::router).orElse(field));
deep.ifPresent(found -> context.setDeepCaves(found.opening()));
}
context.setAquifer(new AquiferCompiler(inputs).compile(selection));
List<BiomeDef> biomes = context.biomes();
if (biomes.stream().anyMatch(CaveStage::carves)) {
context.setBiomes(withoutCarvers(biomes));
}
}
private static List<BiomeDef> withoutCarvers(List<BiomeDef> biomes) {
List<BiomeDef> cleaned = new ArrayList<>(biomes.size());
for (BiomeDef biome : biomes) {
if (!carves(biome)) {
cleaned.add(biome);
continue;
}
JsonObject json = biome.json().deepCopy();
json.add(CARVERS, new JsonArray());
cleaned.add(new BiomeDef(biome.id(), json));
}
return cleaned;
}
private static boolean carves(BiomeDef biome) {
JsonElement carvers = biome.json().get(CARVERS);
return carvers != null && !(carvers.isJsonArray() && carvers.getAsJsonArray().isEmpty());
}
}
@@ -0,0 +1,78 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.lower.FieldDomains;
import java.util.Objects;
import java.util.Optional;
public final class DeepCaveCompiler {
static final int LAVA_LINE = -54;
static final String FIELD = "cave/field";
static final String OPENING = "cave/deep_opening";
private static final double OPEN_MARGIN = Math.nextUp(Float.MIN_VALUE);
private static final Field OPEN = new Field.Const(1.0);
private static final Field INSIDE = new Field.Const(0.0);
private static final Field CLOSED = new Field.Const(CaveProfileCompiler.SOLID);
private static final Field UNMASKED = new Field.Const(-FieldDomains.CONSTANT_LIMIT);
private final CaveInputs inputs;
private final Field blockY;
public DeepCaveCompiler(CaveInputs inputs) {
this.inputs = Objects.requireNonNull(inputs);
this.blockY = inputs.blockY();
}
public Optional<DeepCaves> compile(Field.Shared field, CaveSelection selection) {
Optional<Zone> found = zone(selection);
if (found.isEmpty()) {
return Optional.empty();
}
Zone zone = found.get();
inputs.diagnostics().add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.DEEP_CAVE_LAYERS, inputs.resource(),
"Caves between Y " + zone.low() + " and Y " + zone.high() + " open in the material pass, so they get wall palettes but no floor or ceiling layers"));
Field opened = new Field.Binary(Field.BinaryOp.MAX, field, inside(zone, INSIDE, CLOSED));
Field.Shared opening = new Field.Shared(OPENING,
new Field.Gate(new Field.Binary(Field.BinaryOp.SUB, new Field.Const(OPEN_MARGIN), opened), OPEN));
Field mask = new Field.Binary(Field.BinaryOp.ADD, opening, inside(zone, INSIDE, UNMASKED));
return Optional.of(new DeepCaves(new Field.Binary(Field.BinaryOp.MAX, field, mask), opening));
}
private Optional<Zone> zone(CaveSelection selection) {
int high = Math.min(line() - 1, inputs.maxY());
int low = inputs.minY();
int deepest = Integer.MAX_VALUE;
for (CaveSource source : selection.sources()) {
deepest = Math.min(deepest, CaveProfileCompiler.floor(inputs, source));
if (source.profile().isAllowLava()) {
low = Math.max(low, inputs.lavaY() + 1);
}
}
if (high < low || high < deepest) {
return Optional.empty();
}
return Optional.of(new Zone(low, high));
}
private int line() {
return Math.min(LAVA_LINE, DimensionDef.seaLevel(inputs.fluidHeight()));
}
private Field inside(Zone zone, Field inRange, Field outOfRange) {
return new Field.RangeIf(blockY, zone.low(), zone.high() + 1.0, inRange, outOfRange);
}
public record DeepCaves(Field router, Field.Shared opening) {
public DeepCaves {
Objects.requireNonNull(router);
Objects.requireNonNull(opening);
}
}
private record Zone(int low, int high) {
}
}
@@ -0,0 +1,67 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.ArrayList;
import java.util.List;
final class VolumeLift {
private static final String SHARED_SUFFIX = "/volume";
private VolumeLift() {
}
static Field lift(Field field) {
return switch (field) {
case Field.Const constant -> constant;
case Field.Coord coord -> coord;
case Field.Ref ref -> ref;
case Field.TopSurface topSurface -> topSurface;
case Field.Slice slice -> slice;
case Field.Noise noise -> noise(noise);
case Field.Binary binary -> new Field.Binary(binary.op(), lift(binary.left()), lift(binary.right()));
case Field.Unary unary -> new Field.Unary(unary.op(), lift(unary.input()));
case Field.Clamp clamp -> new Field.Clamp(lift(clamp.input()), clamp.min(), clamp.max());
case Field.Lerp lerp -> new Field.Lerp(lift(lerp.alpha()), lift(lerp.first()), lift(lerp.second()));
case Field.Round round -> new Field.Round(round.mode(), lift(round.input()), lift(round.multiple()));
case Field.Curve curve -> curve(curve);
case Field.Partition partition -> new Field.Partition(lift(partition.input()), partition.thresholds(), partition.leaves());
case Field.Mux mux -> new Field.Mux(lift(mux.index()), liftAll(mux.cases()));
case Field.Gate gate -> new Field.Gate(lift(gate.condition()), lift(gate.expr()));
case Field.RangeIf rangeIf -> new Field.RangeIf(lift(rangeIf.input()), rangeIf.min(), rangeIf.max(), lift(rangeIf.inRange()), lift(rangeIf.outOfRange()));
case Field.Grid grid -> new Field.Grid(lift(grid.expr()), grid.cellXZ(), grid.cellY());
case Field.Shared shared -> shared(shared);
};
}
private static Field noise(Field.Noise noise) {
double yScale = noise.yScale() == 0.0 ? noise.xzScale() : noise.yScale();
return new Field.Noise(noise.noiseId(), noise.xzScale(), yScale, liftShift(noise.shiftX()), liftShift(noise.shiftY()), liftShift(noise.shiftZ()));
}
private static Field liftShift(Field shift) {
return shift == null ? null : lift(shift);
}
private static Field.Curve curve(Field.Curve curve) {
List<Field.Knot> knots = new ArrayList<>(curve.knots().size());
for (Field.Knot knot : curve.knots()) {
Field value = knot.value() instanceof Field.Curve nested ? curve(nested) : knot.value();
knots.add(new Field.Knot(knot.location(), value, knot.derivative()));
}
return new Field.Curve(lift(curve.input()), knots);
}
private static List<Field> liftAll(List<Field> fields) {
List<Field> lifted = new ArrayList<>(fields.size());
for (Field field : fields) {
lifted.add(lift(field));
}
return lifted;
}
private static Field shared(Field.Shared shared) {
Field body = lift(shared.expr());
return body.equals(shared.expr()) ? shared : new Field.Shared(shared.name() + SHARED_SUFFIX, body);
}
}
@@ -0,0 +1,114 @@
package art.arcane.iris.pack.datapack.export.compile.cave;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.FieldEvaluator;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import java.util.Arrays;
import java.util.Map;
import java.util.SplittableRandom;
final class WallShellCalibrator {
private static final long SEED = 0x5EED_3A11L;
private static final int PATCHES = 256;
private static final int PATCH_CELLS = 3;
private static final int SPREAD_CELLS = 1024;
private final Map<String, NoiseDef> noises;
private final DataVersion target;
private final int cellXZ;
private final int cellY;
private double[] solid = new double[4096];
private int solidCount;
private long walls;
private WallShellCalibrator(Map<String, NoiseDef> noises, DataVersion target, int cellXZ, int cellY) {
this.noises = noises;
this.target = target;
this.cellXZ = cellXZ;
this.cellY = cellY;
}
static double band(CaveInterior interior, Map<String, NoiseDef> noises, DataVersion target) {
WallShellCalibrator calibrator = new WallShellCalibrator(noises, target, CaveFieldCompiler.CELL_XZ, CaveFieldCompiler.CELL_Y);
SplittableRandom random = new SplittableRandom(SEED);
for (int patch = 0; patch < PATCHES; patch++) {
calibrator.sample(interior, random);
}
return calibrator.band();
}
private void sample(CaveInterior interior, SplittableRandom random) {
int x0 = cellXZ * (random.nextInt(2 * SPREAD_CELLS) - SPREAD_CELLS);
int z0 = cellXZ * (random.nextInt(2 * SPREAD_CELLS) - SPREAD_CELLS);
int lowCell = Math.floorDiv(interior.minY(), cellY);
int highCell = Math.max(lowCell, Math.floorDiv(interior.maxY(), cellY) - PATCH_CELLS);
int y0 = cellY * (lowCell + random.nextInt(highCell - lowCell + 1));
int corners = PATCH_CELLS + 1;
double[][][] lattice = new double[corners][corners][corners];
for (int i = 0; i < corners; i++) {
for (int j = 0; j < corners; j++) {
for (int k = 0; k < corners; k++) {
lattice[i][j][k] = FieldEvaluator.sample(interior.field(), noises, SEED, target, x0 + i * cellXZ, y0 + j * cellY, z0 + k * cellXZ);
}
}
}
int width = PATCH_CELLS * cellXZ;
int height = PATCH_CELLS * cellY;
double[][][] voxels = new double[width][height][width];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
for (int z = 0; z < width; z++) {
voxels[x][y][z] = interpolate(lattice, x, y, z);
}
}
}
for (int x = 1; x < width - 1; x++) {
for (int y = 0; y < height; y++) {
for (int z = 1; z < width - 1; z++) {
double value = voxels[x][y][z];
if (value < 0.0) {
continue;
}
add(value);
if (voxels[x - 1][y][z] < 0.0 || voxels[x + 1][y][z] < 0.0 || voxels[x][y][z - 1] < 0.0 || voxels[x][y][z + 1] < 0.0) {
walls++;
}
}
}
}
}
private double interpolate(double[][][] lattice, int x, int y, int z) {
int i = x / cellXZ;
int j = y / cellY;
int k = z / cellXZ;
double fx = (x - i * cellXZ) / (double) cellXZ;
double fy = (y - j * cellY) / (double) cellY;
double fz = (z - k * cellXZ) / (double) cellXZ;
double bottom = lerp(fz, lerp(fx, lattice[i][j][k], lattice[i + 1][j][k]), lerp(fx, lattice[i][j][k + 1], lattice[i + 1][j][k + 1]));
double top = lerp(fz, lerp(fx, lattice[i][j + 1][k], lattice[i + 1][j + 1][k]), lerp(fx, lattice[i][j + 1][k + 1], lattice[i + 1][j + 1][k + 1]));
return lerp(fy, bottom, top);
}
private void add(double value) {
if (solidCount == solid.length) {
solid = Arrays.copyOf(solid, solid.length * 2);
}
solid[solidCount++] = value;
}
private double band() {
if (walls == 0L) {
return 0.0;
}
double[] sorted = Arrays.copyOf(solid, solidCount);
Arrays.sort(sorted);
int rank = (int) Math.min(solidCount - 1L, walls);
return sorted[rank];
}
private static double lerp(double delta, double start, double end) {
return start + delta * (end - start);
}
}
@@ -0,0 +1,69 @@
package art.arcane.iris.pack.datapack.export.compile.decoration;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public final class BiomeFeatureLists {
private static final GenerationStep[] STEPS = GenerationStep.values();
private final Map<GenerationStep, Set<String>> order = new EnumMap<>(GenerationStep.class);
private final Map<String, Map<GenerationStep, Set<String>>> members = new HashMap<>();
public void add(String biomeId, GenerationStep step, String placedFeatureId) {
VanillaResourceIdentifier.requireIdentifier(biomeId, "feature list biome id");
Objects.requireNonNull(step);
VanillaResourceIdentifier.requireIdentifier(placedFeatureId, "placed feature id");
order.computeIfAbsent(step, ignored -> new LinkedHashSet<>()).add(placedFeatureId);
members.computeIfAbsent(biomeId, ignored -> new EnumMap<>(GenerationStep.class))
.computeIfAbsent(step, ignored -> new HashSet<>())
.add(placedFeatureId);
}
public List<BiomeDef> apply(List<BiomeDef> biomes, String namespace) {
VanillaResourceIdentifier.requireNamespace(namespace, "feature list namespace");
Set<String> defined = new HashSet<>(biomes.size());
List<BiomeDef> result = new ArrayList<>(biomes.size());
for (BiomeDef biome : biomes) {
String id = namespace + ":" + biome.id();
defined.add(id);
JsonObject json = biome.json().deepCopy();
json.add("features", features(members.getOrDefault(id, Map.of())));
result.add(new BiomeDef(biome.id(), json));
}
for (String biomeId : members.keySet()) {
if (!defined.contains(biomeId)) {
throw new IllegalArgumentException("Feature lists reference undefined biome '" + biomeId + "'");
}
}
return result;
}
private JsonArray features(Map<GenerationStep, Set<String>> biomeSteps) {
JsonArray steps = new JsonArray(STEPS.length);
for (GenerationStep step : STEPS) {
JsonArray features = new JsonArray();
Set<String> biomeFeatures = biomeSteps.get(step);
if (biomeFeatures != null) {
for (String feature : order.get(step)) {
if (biomeFeatures.contains(feature)) {
features.add(feature);
}
}
}
steps.add(features);
}
return steps;
}
}
@@ -0,0 +1,26 @@
package art.arcane.iris.pack.datapack.export.compile.decoration;
import art.arcane.iris.pack.datapack.export.compile.BlockStateResolver;
import art.arcane.iris.pack.datapack.export.compile.CompileStage;
import art.arcane.iris.pack.datapack.export.compile.ExportedBiomes;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.WorldgenSyntax;
import art.arcane.iris.pack.datapack.export.compile.surface.RepaintFeatureCompiler;
public final class DecorationStage implements CompileStage {
@Override
public String name() {
return "decoration";
}
@Override
public void apply(PackCompileContext context) {
ExportedBiomes biomes = ExportedBiomes.load(context);
BlockStateResolver blocks = BlockStateResolver.forContext(context);
FeatureCatalog catalog = new FeatureCatalog(context.namespace(), WorldgenSyntax.forTarget(context.capabilities().target()));
new RepaintFeatureCompiler(context, biomes, blocks, catalog).compile();
new DecoratorCompiler(context, biomes, blocks, catalog).compile();
new DepositCompiler(context, biomes, blocks, catalog).compile();
context.jsonResources().addAll(catalog.resources());
}
}
@@ -0,0 +1,331 @@
package art.arcane.iris.pack.datapack.export.compile.decoration;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.decoration.IrisDecorationPart;
import art.arcane.iris.generation.decoration.IrisDecorator;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.IrisSlopeClip;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.BlockStateResolver;
import art.arcane.iris.pack.datapack.export.compile.ExportedBiomes;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.iris.pack.datapack.export.compile.WorldgenSyntax;
import art.arcane.iris.pack.datapack.export.compile.surface.RepaintFeatureCompiler;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
public final class DecoratorCompiler {
private static final int COLUMNS = 256;
private static final double CAVE_AIR_SHARE = 0.08;
private static final int CAVE_SCAN_STEPS = 12;
private static final int MAX_ATTEMPTS = 256;
private static final double SLOPE_CEILING = 10.0;
private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air");
private static final String STYLE_MESSAGE = "Decorators gated or varied by a noise style place at the style's average density without its pattern";
private static final String STACK_MESSAGE = "Decorator stacks scaled to cave height use their fixed stack range";
private static final String CAVE_MESSAGE = "Cave ceiling decorators place at an estimated density because features cannot enumerate every cave ceiling";
private final PackCompileContext context;
private final ExportedBiomes biomes;
private final BlockStateResolver blocks;
private final FeatureCatalog catalog;
private final WorldgenSyntax syntax;
public DecoratorCompiler(PackCompileContext context, ExportedBiomes biomes, BlockStateResolver blocks, FeatureCatalog catalog) {
this.context = context;
this.biomes = biomes;
this.blocks = blocks;
this.catalog = catalog;
this.syntax = catalog.syntax();
}
public void compile() {
for (Map.Entry<String, IrisBiome> entry : biomes.byId().entrySet()) {
compileBiome(entry.getKey(), entry.getValue());
}
}
static double[] columnShares(double[] chances) {
double[] shares = new double[chances.length];
for (int index = 0; index < chances.length; index++) {
double[] others = new double[chances.length];
others[0] = 1.0;
int size = 1;
for (int other = 0; other < chances.length; other++) {
if (other == index) {
continue;
}
double chance = chances[other];
for (int count = size; count >= 0; count--) {
double stays = others[count] * (1.0 - chance);
double grows = count > 0 ? others[count - 1] * chance : 0.0;
others[count] = stays + grows;
}
size++;
}
double expectation = 0.0;
for (int count = 0; count < size; count++) {
expectation += others[count] / (count + 1);
}
shares[index] = chances[index] * expectation;
}
return shares;
}
static double[] sequentialChances(double[] shares) {
double[] chances = new double[shares.length];
double claimed = 0.0;
for (int index = 0; index < shares.length; index++) {
double free = 1.0 - claimed;
chances[index] = free <= 0.0 ? 0.0 : Math.min(1.0, shares[index] / free);
claimed += shares[index];
}
return chances;
}
private void compileBiome(String biomeId, IrisBiome biome) {
if (biome.getDecorators() == null || biome.getDecorators().isEmpty()) {
return;
}
String resource = biomes.resource(biomeId);
Map<IrisDecorationPart, List<Candidate>> buckets = new EnumMap<>(IrisDecorationPart.class);
for (IrisDecorator decorator : biome.getDecorators()) {
if (decorator == null) {
continue;
}
Optional<Candidate> candidate = candidate(decorator, resource);
IrisDecorationPart part = decorator.getPartOf() == null ? IrisDecorationPart.NONE : decorator.getPartOf();
candidate.ifPresent(value -> buckets.computeIfAbsent(part, ignored -> new ArrayList<>()).add(value));
}
for (Map.Entry<IrisDecorationPart, List<Candidate>> bucket : buckets.entrySet()) {
List<Candidate> candidates = bucket.getValue();
double[] chances = new double[candidates.size()];
for (int index = 0; index < chances.length; index++) {
chances[index] = Math.max(0.0, Math.min(1.0, candidates.get(index).decorator().getChance()));
}
double[] shares = columnShares(chances);
for (int index = 0; index < shares.length; index++) {
shares[index] *= 1.0 - candidates.get(index).airShare();
}
double[] sequential = sequentialChances(shares);
for (int index = 0; index < candidates.size(); index++) {
Candidate candidate = candidates.get(index);
if (candidate.entries().isEmpty() || shares[index] <= 0.0) {
continue;
}
if (bucket.getKey() == IrisDecorationPart.CEILING) {
report(resource, ExportLimit.CAVE_DECORATOR, CAVE_MESSAGE);
context.features().add(biomeId, GenerationStep.UNDERGROUND_DECORATION, ceiling(candidate, shares[index]));
} else {
context.features().add(biomeId, GenerationStep.VEGETAL_DECORATION, column(bucket.getKey(), candidate, sequential[index]));
}
}
}
}
private Optional<Candidate> candidate(IrisDecorator decorator, String resource) {
List<WeightedState> entries = new ArrayList<>();
int airWeight = 0;
int total = 0;
for (IrisBlockData block : decorator.getPalette()) {
Optional<VanillaBlockState> state = blocks.resolve(block, resource);
if (state.isEmpty()) {
continue;
}
int weight = Math.max(1, block.getWeight());
total += weight;
if (AIR.contains(state.get().name())) {
airWeight += weight;
} else {
entries.add(new WeightedState(state.get(), weight));
}
}
if (total == 0) {
return Optional.empty();
}
if (!isStatic(decorator.getStyle()) || (entries.size() > 1 && !isStatic(decorator.getVariance()))) {
report(resource, ExportLimit.DECORATOR_STYLE, STYLE_MESSAGE);
}
if (decorator.isScaleStack()) {
report(resource, ExportLimit.SCALE_STACK, STACK_MESSAGE);
}
if (decorator.getSlopeCondition() != null && !decorator.getSlopeCondition().isDefault()) {
report(resource, ExportLimit.SLOPE, RepaintFeatureCompiler.SLOPE_MESSAGE);
}
List<WeightedState> top = new ArrayList<>();
for (IrisBlockData block : decorator.getTopPalette()) {
Optional<VanillaBlockState> state = blocks.resolve(block, resource);
if (state.isPresent() && !AIR.contains(state.get().name())) {
top.add(new WeightedState(state.get(), Math.max(1, block.getWeight())));
}
}
VanillaBlockState force = decorator.getForceBlock() == null ? null : blocks.resolve(decorator.getForceBlock(), resource).orElse(null);
return Optional.of(new Candidate(decorator, entries, top, (double) airWeight / total, force, names(decorator.getWhitelist(), resource),
names(decorator.getBlacklist(), resource)));
}
private String column(IrisDecorationPart part, Candidate candidate, double sequential) {
String featureId = feature(candidate, false);
JsonArray placement = new JsonArray();
String heightmap = part == IrisDecorationPart.SEA_FLOOR ? Placements.OCEAN_FLOOR : Placements.WORLD_SURFACE;
for (JsonObject modifier : Placements.everyColumn()) {
placement.add(modifier);
}
placement.add(Placements.heightmap(heightmap));
if (sequential < 1.0) {
placement.add(Placements.randomChance(sequential));
}
placement.add(Placements.biome());
placement.add(Placements.filter(Placements.allOf(predicates(part, candidate))));
return catalog.placed(name(candidate), featureId, placement);
}
private String ceiling(Candidate candidate, double share) {
String featureId = feature(candidate, true);
JsonArray placement = new JsonArray();
placement.add(Placements.count(Placements.meanCount(Math.min(MAX_ATTEMPTS, COLUMNS * share / CAVE_AIR_SHARE))));
placement.add(Placements.inSquare());
placement.add(Placements.heightRange(Placements.fullHeight()));
placement.add(Placements.environmentScanUp(CAVE_SCAN_STEPS));
placement.add(Placements.verticalOffset(-1));
placement.add(Placements.biome());
placement.add(Placements.filter(Placements.air(0, 0, 0)));
return catalog.placed(name(candidate) + "_ceiling", featureId, placement);
}
private List<JsonObject> predicates(IrisDecorationPart part, Candidate candidate) {
List<JsonObject> predicates = new ArrayList<>();
switch (part) {
case SEA_FLOOR -> predicates.add(Placements.water(0, 0, 0));
case SEA_SURFACE -> {
predicates.add(Placements.air(0, 0, 0));
predicates.add(Placements.water(0, -1, 0));
}
case SHORE_LINE -> {
predicates.add(Placements.air(0, 0, 0));
predicates.add(Placements.anyOf(List.of(Placements.water(1, -1, 0), Placements.water(-1, -1, 0), Placements.water(0, -1, 1),
Placements.water(0, -1, -1))));
}
default -> {
predicates.add(Placements.air(0, 0, 0));
predicates.add(Placements.solid(0, -1, 0));
}
}
if (!candidate.whitelist().isEmpty()) {
predicates.add(Placements.matchingBlocks(candidate.whitelist(), 0, -1, 0));
}
if (!candidate.blacklist().isEmpty()) {
predicates.add(Placements.not(Placements.matchingBlocks(candidate.blacklist(), 0, -1, 0)));
}
IrisSlopeClip slope = candidate.decorator().getSlopeCondition();
if (slope != null && !slope.isDefault()) {
if (slope.getMinimumSlope() > 0) {
predicates.add(Placements.steeperThan(rise(slope.getMinimumSlope()), -1));
}
if (slope.getMaximumSlope() < SLOPE_CEILING) {
predicates.add(Placements.not(Placements.steeperThan(rise(slope.getMaximumSlope()) + 1, -1)));
}
}
return predicates;
}
private String feature(Candidate candidate, boolean hanging) {
IrisDecorator decorator = candidate.decorator();
int stackMax = Math.max(1, Math.min(decorator.getStackMax(), Math.max(1, decorator.getAbsoluteMaxStack())));
int stackMin = Math.max(1, Math.min(decorator.getStackMin(), stackMax));
String name = name(candidate);
String main;
if (!hanging && stackMax == 1 && candidate.top().isEmpty()) {
JsonObject config = new JsonObject();
config.add("to_place", syntax.provider(candidate.entries()));
main = catalog.feature(name, "minecraft:simple_block", config);
} else {
JsonArray layers = new JsonArray(2);
if (candidate.top().isEmpty()) {
layers.add(layer(Placements.intRange(stackMin, stackMax), candidate.entries()));
} else {
layers.add(layer(Placements.intRange(stackMin - 1, stackMax - 1), candidate.entries()));
layers.add(layer(Placements.intRange(1, 1), candidate.top()));
}
JsonObject config = new JsonObject();
config.add("layers", layers);
config.addProperty("direction", hanging ? "down" : "up");
config.add("allowed_placement", Placements.air(0, 0, 0));
config.addProperty("prioritize_tip", true);
main = catalog.feature(name, "minecraft:block_column", config);
}
if (candidate.force() == null) {
return main;
}
JsonObject forceConfig = new JsonObject();
forceConfig.add("to_place", syntax.provider(List.of(new WeightedState(candidate.force(), 1))));
String force = catalog.feature("decorator/ground_" + path(candidate.force()), "minecraft:simple_block", forceConfig);
JsonArray groundPlacement = new JsonArray(1);
groundPlacement.add(Placements.verticalOffset(-1));
JsonArray features = new JsonArray(2);
features.add(catalog.inlinePlaced(force, groundPlacement));
features.add(catalog.inlinePlaced(main, new JsonArray()));
JsonObject sequence = new JsonObject();
sequence.add("features", features);
return catalog.feature(name + "_forced", "minecraft:sequence", sequence);
}
private JsonObject layer(JsonElement height, List<WeightedState> entries) {
JsonObject layer = new JsonObject();
layer.add("height", height);
layer.add("provider", syntax.provider(entries));
return layer;
}
private List<String> names(List<IrisBlockData> list, String resource) {
TreeSet<String> names = new TreeSet<>();
if (list == null) {
return List.of();
}
for (IrisBlockData block : list) {
blocks.resolve(block, resource).ifPresent(state -> names.add(state.name()));
}
return List.copyOf(names);
}
private void report(String resource, ExportLimit limit, String message) {
ExportDiagnostic diagnostic = new ExportDiagnostic(ExportDiagnostic.Severity.INFO, limit, resource, message);
if (!context.diagnostics().contains(diagnostic)) {
context.diagnostics().add(diagnostic);
}
}
private static String name(Candidate candidate) {
return "decorator/" + path(candidate.entries().getFirst().state());
}
private static String path(VanillaBlockState state) {
return state.name().substring(state.name().indexOf(':') + 1);
}
private static boolean isStatic(IrisGeneratorStyle style) {
return style == null || style.getStyle() == null || style.getStyle() == NoiseStyle.STATIC;
}
private static int rise(double slope) {
return Math.max(1, (int) Math.round(slope));
}
private record Candidate(IrisDecorator decorator, List<WeightedState> entries, List<WeightedState> top, double airShare,
VanillaBlockState force, List<String> whitelist, List<String> blacklist) {
}
}
@@ -0,0 +1,261 @@
package art.arcane.iris.pack.datapack.export.compile.decoration;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisBiomePaletteLayer;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
import art.arcane.iris.generation.decoration.IrisDepositPlacementScope;
import art.arcane.iris.generation.decoration.IrisDepositShape;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.BlockStateResolver;
import art.arcane.iris.pack.datapack.export.compile.ExportedBiomes;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.WorldgenSyntax;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.TreeSet;
public final class DepositCompiler {
private static final int MAX_ORE_SIZE = 64;
private static final int MAX_COUNT = 256;
private static final int MIN_ANCHOR = -2032;
private static final int MAX_ANCHOR = 2031;
private static final String HOST_TAG = "deposit_host";
private static final List<String> HOST_TAG_BASE = List.of("#minecraft:base_stone_overworld", "#minecraft:dirt", "#minecraft:sand");
private final PackCompileContext context;
private final ExportedBiomes biomes;
private final BlockStateResolver blocks;
private final FeatureCatalog catalog;
private final WorldgenSyntax syntax;
private String hostTag;
public DepositCompiler(PackCompileContext context, ExportedBiomes biomes, BlockStateResolver blocks, FeatureCatalog catalog) {
this.context = context;
this.biomes = biomes;
this.blocks = blocks;
this.catalog = catalog;
this.syntax = catalog.syntax();
}
public void compile() {
List<String> everyBiome = biomes.inCodeOrder(biomes.byId().keySet());
String dimensionResource = "dimensions/" + context.dimension().getLoadKey();
for (IrisDepositGenerator deposit : list(context.dimension().getDeposits())) {
place(deposit, everyBiome, dimensionResource);
}
TreeMap<String, List<String>> regionBiomes = new TreeMap<>();
TreeMap<String, IrisRegion> regions = new TreeMap<>();
for (String biomeId : everyBiome) {
IrisRegion region = biomes.region(biomeId);
if (region != null) {
regions.put(region.getLoadKey(), region);
regionBiomes.computeIfAbsent(region.getLoadKey(), ignored -> new ArrayList<>()).add(biomeId);
}
}
for (Map.Entry<String, IrisRegion> region : regions.entrySet()) {
for (IrisDepositGenerator deposit : list(region.getValue().getDeposits())) {
place(deposit, regionBiomes.get(region.getKey()), "regions/" + region.getKey());
}
}
for (Map.Entry<String, IrisBiome> biome : biomes.byId().entrySet()) {
for (IrisDepositGenerator deposit : list(biome.getValue().getDeposits())) {
place(deposit, List.of(biome.getKey()), biomes.resource(biome.getKey()));
}
}
}
private void place(IrisDepositGenerator deposit, List<String> candidates, String resource) {
List<String> biomeIds = filtered(deposit, candidates);
if (biomeIds.isEmpty() || deposit.getMaxPerChunk() <= 0 || deposit.getSpawnChance() <= 0 || deposit.getPerClumpSpawnChance() <= 0) {
return;
}
List<VanillaBlockState> states = new ArrayList<>();
for (IrisBlockData block : list(deposit.getPalette())) {
blocks.resolve(block, resource).ifPresent(state -> {
if (!states.contains(state)) {
states.add(state);
}
});
}
if (states.isEmpty()) {
return;
}
String feature = feature(deposit, states, resource);
String placed = catalog.placed("deposit/" + path(states.getFirst()), feature, placement(deposit, biomeIds, resource));
for (String biomeId : biomeIds) {
context.features().add(biomeId, GenerationStep.UNDERGROUND_ORES, placed);
}
}
private String feature(IrisDepositGenerator deposit, List<VanillaBlockState> states, String resource) {
String type = deposit.getShape() == IrisDepositShape.VANILLA_SCATTERED ? "minecraft:scattered_ore" : "minecraft:ore";
int size = (int) Math.round((Math.max(0, deposit.getMinSize()) + Math.max(0, deposit.getMaxSize())) / 2.0);
if (size > MAX_ORE_SIZE) {
report(resource, ExportLimit.DEPOSIT_SIZE, ExportDiagnostic.Severity.WARNING,
"Deposit clumps of " + size + " blocks export as Minecraft ore veins of " + MAX_ORE_SIZE + " blocks");
size = MAX_ORE_SIZE;
}
List<JsonObject> hosts = hosts(deposit, resource);
List<String> ores = new ArrayList<>(states.size());
for (VanillaBlockState state : states) {
JsonArray targets = new JsonArray(hosts.size());
for (JsonObject host : hosts) {
JsonObject target = new JsonObject();
target.add("target", host.deepCopy());
target.add("state", syntax.blockState(state));
targets.add(target);
}
JsonObject config = new JsonObject();
config.addProperty("size", Math.max(0, size));
config.addProperty("discard_chance_on_air_exposure", (float) Math.max(0.0, Math.min(1.0, deposit.getDiscardChanceOnAirExposure())));
config.add("targets", targets);
ores.add(catalog.feature("deposit/" + path(state), type, config));
}
if (ores.size() == 1) {
return ores.getFirst();
}
JsonArray features = new JsonArray(ores.size());
for (String ore : ores) {
features.add(catalog.inlinePlaced(ore, new JsonArray()));
}
JsonObject selector = new JsonObject();
selector.add("features", features);
return catalog.feature("deposit/" + path(states.getFirst()) + "_mix", "minecraft:simple_random_selector", selector);
}
private JsonArray placement(IrisDepositGenerator deposit, List<String> biomeIds, String resource) {
int minCount = Math.max(0, Math.min(deposit.getMinPerChunk(), deposit.getMaxPerChunk()));
int maxCount = Math.max(minCount, deposit.getMaxPerChunk());
if (maxCount > MAX_COUNT) {
report(resource, ExportLimit.DEPOSIT_SIZE, ExportDiagnostic.Severity.WARNING,
"Deposits place at most " + MAX_COUNT + " clumps per chunk");
maxCount = MAX_COUNT;
minCount = Math.min(minCount, maxCount);
}
double spawnChance = Math.min(1.0, deposit.getSpawnChance());
double clumpChance = Math.min(1.0, deposit.getPerClumpSpawnChance());
JsonArray placement = new JsonArray();
if (spawnChance < 1.0) {
placement.add(Placements.randomChance(spawnChance));
}
placement.add(Placements.count(Placements.intRange(minCount, maxCount)));
placement.add(Placements.inSquare());
boolean surfaceScope = deposit.getBiomeScope() == IrisDepositBiomeScope.SURFACE;
if (surfaceScope) {
placement.add(Placements.heightmap(Placements.OCEAN_FLOOR));
placement.add(Placements.filter(Placements.matchingBiomes(biomeIds)));
}
placement.add(Placements.heightRange(height(deposit)));
IrisDepositPlacementScope scope = deposit.getPlacementScope() == null ? IrisDepositPlacementScope.TERRAIN : deposit.getPlacementScope();
if (scope == IrisDepositPlacementScope.TERRAIN) {
placement.add(Placements.surfaceRelative(Placements.OCEAN_FLOOR, -(Math.max(0, deposit.getSurfaceClearance()) + 1)));
} else if (scope == IrisDepositPlacementScope.ABOVE_TERRAIN) {
report(resource, ExportLimit.DEPOSIT_SCOPE, ExportDiagnostic.Severity.INFO,
"Deposits limited to above-terrain placement also generate inside overhangs and floating terrain within their height band");
}
if (clumpChance < 1.0) {
placement.add(Placements.randomChance(clumpChance));
}
if (!surfaceScope) {
placement.add(Placements.biome());
}
return placement;
}
private JsonObject height(IrisDepositGenerator deposit) {
int min = clampAnchor(Math.min(deposit.getMinHeight(), deposit.getMaxHeight()));
int max = clampAnchor(Math.max(deposit.getMinHeight(), deposit.getMaxHeight()));
return deposit.getHeightDistribution() == IrisDepositHeightDistribution.TRIANGLE
? Placements.trapezoidHeight(min, max)
: Placements.uniformHeight(min, max);
}
private List<JsonObject> hosts(IrisDepositGenerator deposit, String resource) {
List<JsonObject> hosts = new ArrayList<>();
TreeSet<String> names = new TreeSet<>();
for (String raw : list(deposit.getReplaceableBlocks())) {
blocks.resolve(raw, resource).ifPresent(state -> names.add(state.name()));
}
for (String name : names) {
JsonObject host = new JsonObject();
host.addProperty("predicate_type", "minecraft:block_match");
host.addProperty("block", name);
hosts.add(host);
}
if (!hosts.isEmpty()) {
return hosts;
}
JsonObject host = new JsonObject();
host.addProperty("predicate_type", "minecraft:tag_match");
host.addProperty("tag", hostTag());
return List.of(host);
}
private String hostTag() {
if (hostTag != null) {
return hostTag;
}
TreeSet<String> layerBlocks = new TreeSet<>();
for (Map.Entry<String, IrisBiome> biome : biomes.byId().entrySet()) {
for (IrisBiomePaletteLayer layer : list(biome.getValue().getLayers())) {
for (IrisBlockData block : list(layer.getPalette())) {
Optional<VanillaBlockState> state = blocks.resolve(block, biomes.resource(biome.getKey()));
state.ifPresent(value -> layerBlocks.add(value.name()));
}
}
}
List<String> values = new ArrayList<>(HOST_TAG_BASE);
values.add(context.dimensionDef().defaultBlock());
for (String block : layerBlocks) {
if (!values.contains(block) && !block.equals("minecraft:bedrock")) {
values.add(block);
}
}
hostTag = catalog.blockTag(HOST_TAG, values);
return hostTag;
}
private List<String> filtered(IrisDepositGenerator deposit, List<String> candidates) {
List<String> included = list(deposit.getIncludedBiomes());
List<String> excluded = list(deposit.getExcludedBiomes());
List<String> biomeIds = new ArrayList<>(candidates.size());
for (String biomeId : candidates) {
String loadKey = biomes.loadKey(biomeId);
if ((included.isEmpty() || included.contains(loadKey)) && !excluded.contains(loadKey)) {
biomeIds.add(biomeId);
}
}
return biomeIds;
}
private void report(String resource, ExportLimit limit, ExportDiagnostic.Severity severity, String message) {
ExportDiagnostic diagnostic = new ExportDiagnostic(severity, limit, resource, message);
if (!context.diagnostics().contains(diagnostic)) {
context.diagnostics().add(diagnostic);
}
}
private static <T> List<T> list(List<T> values) {
return values == null ? List.of() : values;
}
private static int clampAnchor(int value) {
return Math.max(MIN_ANCHOR, Math.min(MAX_ANCHOR, value));
}
private static String path(VanillaBlockState state) {
return state.name().substring(state.name().indexOf(':') + 1);
}
}
@@ -0,0 +1,114 @@
package art.arcane.iris.pack.datapack.export.compile.decoration;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.WorldgenSyntax;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
public final class FeatureCatalog {
private static final String PLACED_FOLDER = "worldgen/placed_feature";
private static final String BLOCK_TAG_FOLDER = "tags/block";
private final String namespace;
private final WorldgenSyntax syntax;
private final Map<String, String> idsByBody = new HashMap<>();
private final SortedMap<String, JsonElement> files = new TreeMap<>();
public FeatureCatalog(String namespace, WorldgenSyntax syntax) {
this.namespace = VanillaResourceIdentifier.requireNamespace(namespace, "feature namespace");
this.syntax = syntax;
}
public WorldgenSyntax syntax() {
return syntax;
}
public String namespace() {
return namespace;
}
public String feature(String name, String type, JsonObject config) {
return register(syntax.featureFolder(), name, syntax.feature(type, config));
}
public String placed(String name, String featureId, JsonArray placement) {
JsonObject placed = new JsonObject();
placed.addProperty("feature", featureId);
placed.add("placement", placement);
return register(PLACED_FOLDER, name, placed);
}
public JsonObject inlinePlaced(String featureId, JsonArray placement) {
JsonObject placed = new JsonObject();
placed.addProperty("feature", featureId);
placed.add("placement", placement);
return placed;
}
public String blockTag(String name, List<String> values) {
JsonArray entries = new JsonArray(values.size());
for (String value : values) {
entries.add(value);
}
JsonObject tag = new JsonObject();
tag.add("values", entries);
return register(BLOCK_TAG_FOLDER, name, tag);
}
public List<JsonResource> resources() {
List<JsonResource> resources = new ArrayList<>(files.size());
for (Map.Entry<String, JsonElement> file : files.entrySet()) {
resources.add(new JsonResource(file.getKey(), file.getValue()));
}
return resources;
}
private String register(String folder, String name, JsonElement body) {
String key = folder + "|" + canonical(body);
String existing = idsByBody.get(key);
if (existing != null) {
return existing;
}
String base = VanillaResourceIdentifier.sanitizePath(name);
String path = base;
int suffix = 2;
while (files.containsKey(file(folder, path))) {
path = base + "_" + suffix++;
}
files.put(file(folder, path), body.deepCopy());
String id = namespace + ":" + path;
idsByBody.put(key, id);
return id;
}
private String file(String folder, String path) {
return "data/" + namespace + "/" + folder + "/" + path + ".json";
}
private static String canonical(JsonElement element) {
if (element.isJsonObject()) {
StringBuilder builder = new StringBuilder("{");
for (Map.Entry<String, JsonElement> member : new TreeMap<>(element.getAsJsonObject().asMap()).entrySet()) {
builder.append('"').append(member.getKey()).append("\":").append(canonical(member.getValue())).append(',');
}
return builder.append('}').toString();
}
if (element.isJsonArray()) {
StringBuilder builder = new StringBuilder("[");
for (JsonElement item : element.getAsJsonArray()) {
builder.append(canonical(item)).append(',');
}
return builder.append(']').toString();
}
return element.toString();
}
}
@@ -0,0 +1,15 @@
package art.arcane.iris.pack.datapack.export.compile.decoration;
public enum GenerationStep {
RAW_GENERATION,
LAKES,
LOCAL_MODIFICATIONS,
UNDERGROUND_STRUCTURES,
SURFACE_STRUCTURES,
STRONGHOLDS,
UNDERGROUND_ORES,
UNDERGROUND_DECORATION,
FLUID_SPRINGS,
VEGETAL_DECORATION,
TOP_LAYER_MODIFICATION
}
@@ -0,0 +1,283 @@
package art.arcane.iris.pack.datapack.export.compile.decoration;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.Collection;
import java.util.List;
public final class Placements {
public static final String WORLD_SURFACE = "WORLD_SURFACE_WG";
public static final String OCEAN_FLOOR = "OCEAN_FLOOR_WG";
private static final int CHUNK_EDGE = 15;
private static final int WEIGHT_SCALE = 1000;
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private Placements() {
}
public static List<JsonObject> everyColumn() {
JsonObject cuboid = typed("minecraft:cuboid");
cuboid.addProperty("xz_size", CHUNK_EDGE);
cuboid.addProperty("y_size", 1);
return List.of(cuboid, filter(insideWorldBounds(0, -1, 0)));
}
public static JsonObject heightmap(String heightmap) {
JsonObject json = typed("minecraft:heightmap");
json.addProperty("heightmap", heightmap);
return json;
}
public static JsonObject verticalOffset(int y) {
JsonObject json = typed("minecraft:offset");
json.addProperty("x", 0);
json.addProperty("y", y);
json.addProperty("z", 0);
return json;
}
public static JsonObject randomChance(double chance) {
JsonObject json = typed("minecraft:random_chance");
json.addProperty("chance", (float) chance);
return json;
}
public static JsonObject biome() {
return typed("minecraft:biome");
}
public static JsonObject inSquare() {
return typed("minecraft:in_square");
}
public static JsonObject count(JsonElement count) {
JsonObject json = typed("minecraft:count");
json.add("count", count);
return json;
}
public static JsonObject heightRange(JsonObject height) {
JsonObject json = typed("minecraft:height_range");
json.add("height", height);
return json;
}
public static JsonObject uniformHeight(int minAboveBottom, int maxAboveBottom) {
JsonObject json = typed("minecraft:uniform");
json.add("min_inclusive", aboveBottom(minAboveBottom));
json.add("max_inclusive", aboveBottom(maxAboveBottom));
return json;
}
public static JsonObject trapezoidHeight(int minAboveBottom, int maxAboveBottom) {
JsonObject json = typed("minecraft:trapezoid");
json.add("min_inclusive", aboveBottom(minAboveBottom));
json.add("max_inclusive", aboveBottom(maxAboveBottom));
json.addProperty("plateau", 0);
return json;
}
public static JsonObject fullHeight() {
JsonObject bottom = new JsonObject();
bottom.addProperty("above_bottom", 0);
JsonObject top = new JsonObject();
top.addProperty("below_top", 0);
JsonObject json = typed("minecraft:uniform");
json.add("min_inclusive", bottom);
json.add("max_inclusive", top);
return json;
}
public static JsonObject surfaceRelative(String heightmap, int maxInclusive) {
JsonObject json = typed("minecraft:surface_relative_threshold_filter");
json.addProperty("heightmap", heightmap);
json.addProperty("max_inclusive", maxInclusive);
return json;
}
public static JsonObject environmentScanUp(int maxSteps) {
JsonObject json = typed("minecraft:environment_scan");
json.addProperty("direction_of_search", "up");
json.addProperty("max_steps", maxSteps);
json.add("target_condition", solid(0, 0, 0));
json.add("allowed_search_condition", air(0, 0, 0));
return json;
}
public static JsonObject filter(JsonObject predicate) {
JsonObject json = typed("minecraft:block_predicate_filter");
json.add("predicate", predicate);
return json;
}
public static JsonObject uniform(int min, int max) {
JsonObject json = typed("minecraft:uniform");
json.addProperty("min_inclusive", min);
json.addProperty("max_inclusive", max);
return json;
}
public static JsonElement intRange(int min, int max) {
return min == max ? new JsonPrimitive(min) : uniform(min, max);
}
public static JsonElement meanCount(double mean) {
double clamped = Math.max(0.0, mean);
int lower = (int) Math.floor(clamped);
int upperWeight = (int) Math.round((clamped - lower) * WEIGHT_SCALE);
if (upperWeight == 0) {
return new JsonPrimitive(lower);
}
if (upperWeight == WEIGHT_SCALE) {
return new JsonPrimitive(lower + 1);
}
JsonArray distribution = new JsonArray(2);
distribution.add(weighted(lower, WEIGHT_SCALE - upperWeight));
distribution.add(weighted(lower + 1, upperWeight));
JsonObject json = typed("minecraft:weighted_list");
json.add("distribution", distribution);
return json;
}
public static JsonObject solid(int x, int y, int z) {
return offset(typed("minecraft:solid"), x, y, z);
}
public static JsonObject air(int x, int y, int z) {
JsonObject json = typed("minecraft:matching_block_tag");
json.addProperty("tag", "minecraft:air");
return offset(json, x, y, z);
}
public static JsonObject water(int x, int y, int z) {
JsonArray fluids = new JsonArray(2);
fluids.add("minecraft:water");
fluids.add("minecraft:flowing_water");
JsonObject json = typed("minecraft:matching_fluids");
json.add("fluids", fluids);
return offset(json, x, y, z);
}
public static JsonObject matchingBlocks(Collection<String> blocks, int x, int y, int z) {
JsonArray ids = new JsonArray(blocks.size());
for (String block : blocks) {
ids.add(block);
}
JsonObject json = typed("minecraft:matching_blocks");
json.add("blocks", ids);
return offset(json, x, y, z);
}
public static JsonObject matchingBiomes(Collection<String> biomeIds) {
JsonArray ids = new JsonArray(biomeIds.size());
for (String id : biomeIds) {
ids.add(id);
}
JsonObject json = typed("minecraft:matching_biomes");
json.add("biomes", ids);
return json;
}
public static JsonObject wouldSurvive(JsonObject state) {
JsonObject json = typed("minecraft:would_survive");
json.add("state", state);
return json;
}
public static JsonObject insideWorldBounds(int x, int y, int z) {
return offset(typed("minecraft:inside_world_bounds"), x, y, z);
}
public static JsonObject not(JsonObject predicate) {
JsonObject json = typed("minecraft:not");
json.add("predicate", predicate);
return json;
}
public static JsonObject allOf(List<JsonObject> predicates) {
return predicates.size() == 1 ? predicates.getFirst() : combined("minecraft:all_of", predicates);
}
public static JsonObject anyOf(List<JsonObject> predicates) {
return predicates.size() == 1 ? predicates.getFirst() : combined("minecraft:any_of", predicates);
}
public static JsonObject steeperThan(int rise, int groundOffset) {
JsonObject[] directions = new JsonObject[CARDINALS.length * 2];
for (int index = 0; index < CARDINALS.length; index++) {
int[] cardinal = CARDINALS[index];
directions[index * 2] = solid(cardinal[0] * 3, groundOffset + rise, cardinal[1] * 3);
directions[index * 2 + 1] = not(solid(cardinal[0] * 3, groundOffset - rise + 1, cardinal[1] * 3));
}
return anyOf(List.of(directions));
}
public static JsonObject dropAtLeast(int drop) {
JsonObject[] directions = new JsonObject[CARDINALS.length];
for (int index = 0; index < CARDINALS.length; index++) {
directions[index] = not(solid(CARDINALS[index][0], 1 - drop, CARDINALS[index][1]));
}
return anyOf(List.of(directions));
}
public static JsonObject exposed() {
JsonObject[] directions = new JsonObject[CARDINALS.length];
for (int index = 0; index < CARDINALS.length; index++) {
directions[index] = not(solid(CARDINALS[index][0], 0, CARDINALS[index][1]));
}
return allOf(List.of(solid(0, 0, 0), anyOf(List.of(directions))));
}
public static JsonObject stepUpBeside() {
JsonObject[] directions = new JsonObject[CARDINALS.length];
for (int index = 0; index < CARDINALS.length; index++) {
int[] cardinal = CARDINALS[index];
directions[index] = allOf(List.of(solid(cardinal[0], 0, cardinal[1]), not(solid(cardinal[0], 1, cardinal[1]))));
}
return anyOf(List.of(directions));
}
public static JsonObject typed(String type) {
JsonObject json = new JsonObject();
json.addProperty("type", type);
return json;
}
private static JsonObject combined(String type, List<JsonObject> predicates) {
JsonArray list = new JsonArray(predicates.size());
for (JsonObject predicate : predicates) {
list.add(predicate);
}
JsonObject json = typed(type);
json.add("predicates", list);
return json;
}
private static JsonObject aboveBottom(int value) {
JsonObject json = new JsonObject();
json.addProperty("above_bottom", value);
return json;
}
private static JsonObject offset(JsonObject predicate, int x, int y, int z) {
if (x == 0 && y == 0 && z == 0) {
return predicate;
}
JsonArray offset = new JsonArray(3);
offset.add(x);
offset.add(y);
offset.add(z);
predicate.add("offset", offset);
return predicate;
}
private static JsonObject weighted(int data, int weight) {
JsonObject json = new JsonObject();
json.addProperty("data", data);
json.addProperty("weight", weight);
return json;
}
}
@@ -0,0 +1,105 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.DoubleUnaryOperator;
public final class CellHashNoise {
public static final String NOISE_ID = "cell_hash";
public static final double LATTICE_SPACING = 2.625D;
public static final int CDF_INTERVALS = 168;
private static final String REFERENCE_NOISE = "iris_export:cell_hash";
private static final long REFERENCE_SEED = 0L;
private static final double REFERENCE_PLANE = 7.375D;
private static final int REFERENCE_SEEDS = 8;
private static final long REFERENCE_SEED_STRIDE = 7_919L;
private static final int REFERENCE_ROWS = 16;
private static final int REFERENCE_COLUMNS = 32;
private static final int PLANE_SLOTS = 1 << 17;
private static final double PLANE_STEP = 4.0D;
private static final double PLANE_OFFSET = 0.375D;
private static final int VOLUME_PLANE_SLOTS = 1 << 22;
private static final double VOLUME_PLANE_RESOLUTION = 512.0D;
private static final long FNV_OFFSET = 0xcbf29ce484222325L;
private static final long FNV_PRIME = 0x100000001b3L;
private static final Map<DataVersion, double[]> QUANTILES = new ConcurrentHashMap<>();
private CellHashNoise() {
}
public static String register(NoiseRegistry registry) {
return registry.register(new NoiseDef(NOISE_ID, 0, List.of(1.0D)));
}
public static double plane(String key) {
return PLANE_OFFSET + PLANE_STEP * Math.floorMod(stableHash(key), PLANE_SLOTS);
}
public static double volumePlane(String key) {
return Math.floorMod(stableHash(key), VOLUME_PLANE_SLOTS) / VOLUME_PLANE_RESOLUTION;
}
public static Field sample(String noiseId, Field shiftX, Field shiftY, Field shiftZ) {
return new Field.Noise(noiseId, 0.0D, 0.0D, shiftX, shiftY, shiftZ);
}
public static Field uniform(Field hash, DataVersion target) {
double[] quantiles = quantiles(target);
double[] values = new double[quantiles.length];
for (int index = 0; index < values.length; index++) {
values[index] = (double) index / CDF_INTERVALS;
}
return CurveKnots.monotone(hash, quantiles, values);
}
public static Field periodic(Field hash, DataVersion target, DoubleUnaryOperator shape) {
double[] quantiles = quantiles(target);
double[] values = new double[quantiles.length];
for (int index = 0; index < values.length; index++) {
values[index] = shape.applyAsDouble((double) index / CDF_INTERVALS);
}
return CurveKnots.smooth(hash, quantiles, values);
}
public static Field white(String key, NoiseRegistry registry, DataVersion target) {
Field hash = new Field.Noise(register(registry), LATTICE_SPACING, 0.0D, null, new Field.Const(plane(key)), null);
return uniform(hash, target);
}
static long stableHash(String value) {
long hash = FNV_OFFSET;
for (byte part : value.getBytes(StandardCharsets.UTF_8)) {
hash ^= part & 0xFFL;
hash *= FNV_PRIME;
}
hash = (hash ^ hash >>> 30) * 0xbf58476d1ce4e5b9L;
hash = (hash ^ hash >>> 27) * 0x94d049bb133111ebL;
return hash ^ hash >>> 31;
}
private static double[] quantiles(DataVersion target) {
return QUANTILES.computeIfAbsent(target, CellHashNoise::bake).clone();
}
private static double[] bake(DataVersion target) {
double[] samples = new double[REFERENCE_SEEDS * REFERENCE_ROWS * REFERENCE_COLUMNS];
int index = 0;
for (int seed = 0; seed < REFERENCE_SEEDS; seed++) {
VanillaNoiseModel model = VanillaNoiseModel.create(REFERENCE_NOISE, 0, List.of(1.0D), REFERENCE_SEED + REFERENCE_SEED_STRIDE * seed, target);
for (int row = 0; row < REFERENCE_ROWS; row++) {
for (int column = 0; column < REFERENCE_COLUMNS; column++) {
samples[index++] = model.sample((column - REFERENCE_COLUMNS / 2) * LATTICE_SPACING, REFERENCE_PLANE, (row - REFERENCE_ROWS / 2) * LATTICE_SPACING);
}
}
}
Arrays.sort(samples);
return IrisSampleFit.quantiles(samples, CDF_INTERVALS);
}
}
@@ -0,0 +1,319 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
public final class CellularLibrary {
public static final double JITTER_RADIUS = 0.45D;
public static final int JITTER_TURNS = 7;
public static final int VOLUME_CELL_XZ = 4;
public static final int VOLUME_CELL_Y = 8;
private static final DataVersion TARGET = DataVersion.V26_3;
private static final double COORDINATE_STEP = 64.0D;
private static final double HORIZONTAL_EXTENT = 30_000_000.0D;
private static final double VERTICAL_EXTENT = 4096.0D;
private static final double MIN_CELL_BLOCKS = COORDINATE_STEP / 1.0e6D;
private static final double MAX_CELL_BLOCKS = 1.0e6D;
private static final double RANGE_LIMIT = 1.0e6D;
private static final int POLAR_TURNS = 4;
private static final String NAMESPACE_PREFIX = "cell/";
private static final String COORDINATE_PREFIX = "cell/coord/";
private static final String QUADRANT_SEGMENT = "/quadrant";
private CellularLibrary() {
}
public static CellularOutputs build(String instanceId, double cellSizeBlocks, Field warpX, Field warpZ, CellularMode mode, NoiseRegistry registry) {
VanillaResourceIdentifier.requirePath(instanceId, "cellular instance id");
Objects.requireNonNull(mode, "mode");
Objects.requireNonNull(registry, "registry");
requireCellSize(cellSizeBlocks);
Construction construction = new Construction(NAMESPACE_PREFIX + instanceId + modeSegment(mode) + "/", instanceId, cellSizeBlocks, CellHashNoise.register(registry), warpX, warpZ);
return switch (mode) {
case GRID_3X3 -> construction.grid();
case QUADRANT_2X2 -> construction.quadrant();
case GRID_3X3X3 -> construction.volume();
};
}
static String modeSegment(CellularMode mode) {
return mode == CellularMode.QUADRANT_2X2 ? QUADRANT_SEGMENT : "";
}
static Field cellCoordinate(String name, Axis axis, double cellSizeBlocks, Field warpBlocks) {
requireCellSize(cellSizeBlocks);
Field base = blockCoordinate(axis);
Field scaled = cellSizeBlocks == COORDINATE_STEP ? base : multiply(base, COORDINATE_STEP / cellSizeBlocks);
Field warped = warpBlocks == null ? scaled : add(scaled, multiply(warpBlocks, 1.0D / cellSizeBlocks));
return new Field.Shared(name, warped);
}
private static Field blockCoordinate(Axis axis) {
double extent = axis == Axis.Y ? VERTICAL_EXTENT : HORIZONTAL_EXTENT;
double value = extent / COORDINATE_STEP;
Field positive = new Field.Coord(axis, Field.Tiling.CLAMP, 0.0D, extent, 0.0D, value);
Field negative = new Field.Coord(axis, Field.Tiling.CLAMP, 0.0D, -extent, 0.0D, -value);
return new Field.Shared(COORDINATE_PREFIX + axis.name().toLowerCase(Locale.ROOT), add(positive, negative));
}
private static void requireCellSize(double cellSizeBlocks) {
if (!(cellSizeBlocks >= MIN_CELL_BLOCKS && cellSizeBlocks <= MAX_CELL_BLOCKS)) {
throw new IllegalArgumentException("Cell size must be within [" + MIN_CELL_BLOCKS + ", " + MAX_CELL_BLOCKS + "] blocks, got " + cellSizeBlocks);
}
}
private static Field add(Field left, Field right) {
return new Field.Binary(Field.BinaryOp.ADD, left, right);
}
private static Field subtract(Field left, Field right) {
return new Field.Binary(Field.BinaryOp.SUB, left, right);
}
private static Field multiply(Field left, double factor) {
return new Field.Binary(Field.BinaryOp.MUL, left, new Field.Const(factor));
}
private static Field unary(Field.UnaryOp op, Field input) {
return new Field.Unary(op, input);
}
private static String tag(int offset) {
return switch (offset) {
case -1 -> "m1";
case 0 -> "0";
case 1 -> "p1";
default -> throw new IllegalArgumentException("Cell offset must be -1, 0 or 1, got " + offset);
};
}
private static double planarJitterX(int offset, double fraction) {
return offset + JITTER_RADIUS * Math.cos(2.0D * Math.PI * JITTER_TURNS * fraction);
}
private static double planarJitterZ(int offset, double fraction) {
return offset + JITTER_RADIUS * Math.sin(2.0D * Math.PI * JITTER_TURNS * fraction);
}
private static double polar(double fraction) {
double phase = POLAR_TURNS * fraction - Math.floor(POLAR_TURNS * fraction);
return 1.0D - 2.0D * Math.abs(2.0D * phase - 1.0D);
}
private static double volumeJitterX(int offset, double fraction) {
double height = polar(fraction);
return offset + JITTER_RADIUS * Math.sqrt(Math.max(0.0D, 1.0D - height * height)) * Math.cos(2.0D * Math.PI * JITTER_TURNS * fraction);
}
private static double volumeJitterY(int offset, double fraction) {
return offset + JITTER_RADIUS * polar(fraction);
}
private static double volumeJitterZ(int offset, double fraction) {
double height = polar(fraction);
return offset + JITTER_RADIUS * Math.sqrt(Math.max(0.0D, 1.0D - height * height)) * Math.sin(2.0D * Math.PI * JITTER_TURNS * fraction);
}
private static final class Construction {
private final String prefix;
private final double cellSizeBlocks;
private final String hashId;
private final Field plane;
private final double volumePlane;
private final Field xs;
private final Field u;
private final Field ik;
private final Field zs;
private final Field w;
private final Field jk;
private Construction(String prefix, String instanceId, double cellSizeBlocks, String hashId, Field warpX, Field warpZ) {
this.prefix = prefix;
this.cellSizeBlocks = cellSizeBlocks;
this.hashId = hashId;
this.plane = new Field.Const(CellHashNoise.plane(instanceId));
this.volumePlane = CellHashNoise.volumePlane(instanceId);
this.xs = cellCoordinate(prefix + "xs", Axis.X, cellSizeBlocks, warpX);
Field i = shared("i", new Field.Round(Field.RoundMode.ROUND, xs, new Field.Const(1.0D)));
this.u = shared("u", subtract(xs, i));
this.ik = shared("ik", multiply(i, CellHashNoise.LATTICE_SPACING));
this.zs = cellCoordinate(prefix + "zs", Axis.Z, cellSizeBlocks, warpZ);
Field j = shared("j", new Field.Round(Field.RoundMode.ROUND, zs, new Field.Const(1.0D)));
this.w = shared("w", subtract(zs, j));
this.jk = shared("jk", multiply(j, CellHashNoise.LATTICE_SPACING));
}
private CellularOutputs grid() {
List<Candidate> candidates = new ArrayList<>(9);
for (int di = -1; di <= 1; di++) {
for (int dj = -1; dj <= 1; dj++) {
candidates.add(planar(di, dj));
}
}
return planarOutputs("", candidates);
}
private CellularOutputs quadrant() {
Field signX = shared("s", unary(Field.UnaryOp.SIGN, u));
Field signZ = shared("t", unary(Field.UnaryOp.SIGN, w));
Field towardX = shared("s_u", subtract(signX, u));
Field towardZ = shared("t_w", subtract(signZ, w));
Field neighbourX = shared("ik_s", add(ik, new Field.Binary(Field.BinaryOp.MUL, signX, new Field.Const(CellHashNoise.LATTICE_SPACING))));
Field neighbourZ = shared("jk_t", add(jk, new Field.Binary(Field.BinaryOp.MUL, signZ, new Field.Const(CellHashNoise.LATTICE_SPACING))));
List<Candidate> candidates = List.of(
planar(0, 0),
quadrantCandidate("s0", neighbourX, jk, towardX, null),
quadrantCandidate("0t", ik, neighbourZ, null, towardZ),
quadrantCandidate("st", neighbourX, neighbourZ, towardX, towardZ));
return planarOutputs("q_", candidates);
}
private CellularOutputs volume() {
Field ys = cellCoordinate(prefix + "ys", Axis.Y, cellSizeBlocks, null);
Field l = shared("l", new Field.Round(Field.RoundMode.ROUND, ys, new Field.Const(1.0D)));
Field v = shared("v", subtract(ys, l));
Field lk = shared("lk", add(multiply(l, CellHashNoise.LATTICE_SPACING), new Field.Const(volumePlane)));
List<Candidate> candidates = new ArrayList<>(27);
for (int di = -1; di <= 1; di++) {
for (int dl = -1; dl <= 1; dl++) {
for (int dj = -1; dj <= 1; dj++) {
candidates.add(volumeCandidate(di, dl, dj, lk, v));
}
}
}
Candidate winner = tournament("v_m", candidates);
Extremes extremes = extremes("v_f", candidates);
return new CellularOutputs(
volumeGrid(shared("v_value", CellHashNoise.uniform(winner.hash(), TARGET))),
volumeGrid(extremes.nearest()),
volumeGrid(extremes.second()),
volumeGrid(winnerCoordinate("v_winner_x", xs, winner.offsetX())),
volumeGrid(winnerCoordinate("v_winner_z", zs, winner.offsetZ())));
}
private Candidate planar(int di, int dj) {
String tag = tag(di) + "_" + tag(dj);
Field hash = shared("h_" + tag, CellHashNoise.sample(hashId, neighbour(ik, "ik", di), plane, neighbour(jk, "jk", dj)));
Field offsetX = shared("vx_" + tag, subtract(CellHashNoise.periodic(hash, TARGET, fraction -> planarJitterX(di, fraction)), u));
Field offsetZ = shared("vz_" + tag, subtract(CellHashNoise.periodic(hash, TARGET, fraction -> planarJitterZ(dj, fraction)), w));
return new Candidate(shared("d_" + tag, natural(List.of(offsetX, offsetZ))), hash, offsetX, offsetZ);
}
private Candidate quadrantCandidate(String tag, Field shiftX, Field shiftZ, Field towardX, Field towardZ) {
Field hash = shared("q_h_" + tag, CellHashNoise.sample(hashId, shiftX, plane, shiftZ));
Field jitterX = CellHashNoise.periodic(hash, TARGET, fraction -> planarJitterX(0, fraction));
Field jitterZ = CellHashNoise.periodic(hash, TARGET, fraction -> planarJitterZ(0, fraction));
Field offsetX = shared("q_vx_" + tag, towardX == null ? subtract(jitterX, u) : add(jitterX, towardX));
Field offsetZ = shared("q_vz_" + tag, towardZ == null ? subtract(jitterZ, w) : add(jitterZ, towardZ));
return new Candidate(shared("q_d_" + tag, natural(List.of(offsetX, offsetZ))), hash, offsetX, offsetZ);
}
private Candidate volumeCandidate(int di, int dl, int dj, Field lk, Field v) {
String tag = tag(di) + "_" + tag(dl) + "_" + tag(dj);
Field hash = shared("v_h_" + tag, CellHashNoise.sample(hashId, neighbour(ik, "ik", di), neighbour(lk, "lk", dl), neighbour(jk, "jk", dj)));
Field offsetX = shared("v_vx_" + tag, subtract(CellHashNoise.periodic(hash, TARGET, fraction -> volumeJitterX(di, fraction)), u));
Field offsetY = shared("v_vy_" + tag, subtract(CellHashNoise.periodic(hash, TARGET, fraction -> volumeJitterY(dl, fraction)), v));
Field offsetZ = shared("v_vz_" + tag, subtract(CellHashNoise.periodic(hash, TARGET, fraction -> volumeJitterZ(dj, fraction)), w));
return new Candidate(shared("v_d_" + tag, natural(List.of(offsetX, offsetY, offsetZ))), hash, offsetX, offsetZ);
}
private CellularOutputs planarOutputs(String modePrefix, List<Candidate> candidates) {
Candidate winner = tournament(modePrefix + "m", candidates);
Extremes extremes = extremes(modePrefix + "f", candidates);
return new CellularOutputs(
shared(modePrefix + "value", CellHashNoise.uniform(winner.hash(), TARGET)),
extremes.nearest(),
extremes.second(),
winnerCoordinate(modePrefix + "winner_x", xs, winner.offsetX()),
winnerCoordinate(modePrefix + "winner_z", zs, winner.offsetZ()));
}
private Candidate tournament(String tagPrefix, List<Candidate> candidates) {
List<Candidate> round = candidates;
int level = 0;
while (round.size() > 1) {
List<Candidate> next = new ArrayList<>((round.size() + 1) / 2);
for (int index = 0; index < round.size(); index += 2) {
if (index + 1 < round.size()) {
next.add(match(tagPrefix + level + "_" + index / 2, round.get(index), round.get(index + 1)));
} else {
next.add(round.get(index));
}
}
round = next;
level++;
}
return round.getFirst();
}
private Candidate match(String tag, Candidate first, Candidate second) {
Field difference = shared(tag + "_diff", subtract(first.distance(), second.distance()));
return new Candidate(
shared(tag + "_d", new Field.Binary(Field.BinaryOp.MIN, first.distance(), second.distance())),
shared(tag + "_h", pick(difference, first.hash(), second.hash())),
shared(tag + "_vx", pick(difference, first.offsetX(), second.offsetX())),
shared(tag + "_vz", pick(difference, first.offsetZ(), second.offsetZ())));
}
private Extremes extremes(String tagPrefix, List<Candidate> candidates) {
Field first = candidates.get(0).distance();
Field second = candidates.get(1).distance();
Field nearest = shared(tagPrefix + "1_1", new Field.Binary(Field.BinaryOp.MIN, first, second));
Field runnerUp = shared(tagPrefix + "2_1", new Field.Binary(Field.BinaryOp.MAX, first, second));
for (int index = 2; index < candidates.size(); index++) {
Field distance = candidates.get(index).distance();
runnerUp = shared(tagPrefix + "2_" + index, new Field.Binary(Field.BinaryOp.MIN, runnerUp, new Field.Binary(Field.BinaryOp.MAX, nearest, distance)));
nearest = shared(tagPrefix + "1_" + index, new Field.Binary(Field.BinaryOp.MIN, nearest, distance));
}
return new Extremes(nearest, runnerUp);
}
private Field winnerCoordinate(String name, Field cellCoordinate, Field offset) {
return shared(name, multiply(add(cellCoordinate, offset), cellSizeBlocks));
}
private Field neighbour(Field base, String name, int offset) {
if (offset == 0) {
return base;
}
return shared(name + "_" + tag(offset), add(base, new Field.Const(offset * CellHashNoise.LATTICE_SPACING)));
}
private Field natural(List<Field> offsets) {
Field manhattan = null;
Field euclidean = null;
for (Field offset : offsets) {
Field absolute = unary(Field.UnaryOp.ABS, offset);
Field square = unary(Field.UnaryOp.SQUARE, offset);
manhattan = manhattan == null ? absolute : add(manhattan, absolute);
euclidean = euclidean == null ? square : add(euclidean, square);
}
return add(manhattan, euclidean);
}
private Field pick(Field difference, Field first, Field second) {
return new Field.RangeIf(difference, -RANGE_LIMIT, 0.0D, first, second);
}
private Field volumeGrid(Field field) {
return new Field.Grid(field, VOLUME_CELL_XZ, VOLUME_CELL_Y);
}
private Field shared(String name, Field expr) {
return new Field.Shared(prefix + name, expr);
}
}
private record Candidate(Field distance, Field hash, Field offsetX, Field offsetZ) {
}
private record Extremes(Field nearest, Field second) {
}
}
@@ -0,0 +1,29 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
public final class CellularLookalike {
private static final String NAMESPACE_PREFIX = "cell/";
private CellularLookalike() {
}
public static Field smoothStyle(String instanceId, NoiseStyle style, double zoom, Field warpX, Field warpZ, NoiseRegistry registry, DataVersion target) {
String prefix = prefix(instanceId);
if (!(zoom > 0.0D) || !Double.isFinite(zoom)) {
throw new IllegalArgumentException("Smooth look-alike zoom must be positive and finite, got " + zoom);
}
IrisSampleFit.Profile iris = IrisSampleFit.style(style);
IrisSampleFit.Profile vanilla = IrisSampleFit.vanilla(target);
double xzScale = LeafNoise.xzScale(vanilla, iris.correlationLength() * zoom);
Field noise = LeafNoise.sample(LeafNoise.register(registry), xzScale, prefix + "smooth", warpX, warpZ);
return new Field.Shared(prefix + "smooth", LeafNoise.toMarginal(noise, vanilla, iris));
}
private static String prefix(String instanceId) {
return NAMESPACE_PREFIX + VanillaResourceIdentifier.requirePath(instanceId, "cellular look-alike instance id") + "/";
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
public enum CellularMode {
GRID_3X3,
QUADRANT_2X2,
GRID_3X3X3
}
@@ -0,0 +1,15 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.Objects;
public record CellularOutputs(Field cellValue, Field f1, Field f2, Field winnerX, Field winnerZ) {
public CellularOutputs {
Objects.requireNonNull(cellValue, "cellValue");
Objects.requireNonNull(f1, "f1");
Objects.requireNonNull(f2, "f2");
Objects.requireNonNull(winnerX, "winnerX");
Objects.requireNonNull(winnerZ, "winnerZ");
}
}
@@ -0,0 +1,9 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
public interface CellularStyleLowering {
Field lower(IrisGeneratorStyle style, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities);
}
@@ -0,0 +1,71 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.List;
final class CellularWarp {
static final Offsets NONE = new Offsets(null, null);
private static final double IRIS_FREQUENCY = 0.01D;
private static final double STYLE_FREQUENCY = 1.0D / 64.0D;
private CellularWarp() {
}
static Offsets signature(String prefix, Signature signature, double zoom, NoiseRegistry registry, DataVersion target) {
String leafId = LeafNoise.register(registry);
IrisSampleFit.Profile vanilla = IrisSampleFit.vanilla(target);
Offsets inner = NONE;
List<Level> levels = signature.levels();
for (int index = levels.size() - 1; index >= 0; index--) {
inner = level(prefix + "warp" + index, levels.get(index), inner, zoom, leafId, vanilla);
}
return inner;
}
static Offsets fracture(String prefix, Field fractureX, Field fractureZ, double amplitude) {
return new Offsets(centered(prefix + "fracture_x", fractureX, amplitude), centered(prefix + "fracture_z", fractureZ, amplitude));
}
private static Offsets level(String name, Level level, Offsets inner, double zoom, String leafId, IrisSampleFit.Profile vanilla) {
IrisSampleFit.Profile iris = IrisSampleFit.style(level.leaf());
double frequency = level.scale() * IRIS_FREQUENCY / zoom;
double xzScale = LeafNoise.xzScale(vanilla, iris.correlationLength() * STYLE_FREQUENCY / frequency);
Field amplitude = new Field.Const(level.fracture() * zoom * iris.deviation() / vanilla.deviation());
Field x = new Field.Binary(Field.BinaryOp.MUL, LeafNoise.sample(leafId, xzScale, name + "_x", inner.x(), inner.z()), amplitude);
Field z = new Field.Binary(Field.BinaryOp.MUL, LeafNoise.sample(leafId, xzScale, name + "_z", inner.x(), inner.z()), amplitude);
return new Offsets(new Field.Shared(name + "_x", x), new Field.Shared(name + "_z", z));
}
private static Field centered(String name, Field value, double amplitude) {
Field offset = new Field.Binary(Field.BinaryOp.ADD, value, new Field.Const(-0.5D));
return new Field.Shared(name, new Field.Binary(Field.BinaryOp.MUL, offset, new Field.Const(amplitude)));
}
enum Signature {
IRIS(List.of(new Level(NoiseStyle.SIMPLEX, 0.9D, 44.0D), new Level(NoiseStyle.SIMPLEX, 0.21D, 145.0D), new Level(NoiseStyle.SIMPLEX, 0.9D, 620.0D))),
THICK(List.of(new Level(NoiseStyle.SIMPLEX, 0.5D, 44.0D), new Level(NoiseStyle.SIMPLEX, 0.11D, 145.0D), new Level(NoiseStyle.SIMPLEX, 0.4D, 620.0D))),
HALF(List.of(new Level(NoiseStyle.SIMPLEX, 0.9D, 22.0D), new Level(NoiseStyle.SIMPLEX, 0.21D, 99.0D), new Level(NoiseStyle.SIMPLEX, 0.9D, 420.0D))),
DOUBLE(List.of(new Level(NoiseStyle.SIMPLEX, 1.0D, 93.0D), new Level(NoiseStyle.SIMPLEX, 0.9D, 44.0D), new Level(NoiseStyle.SIMPLEX, 0.21D, 145.0D),
new Level(NoiseStyle.SIMPLEX, 0.9D, 620.0D))),
PERLIN(List.of(new Level(NoiseStyle.PERLIN, 1.25D, 250.0D)));
private final List<Level> levels;
Signature(List<Level> levels) {
this.levels = levels;
}
List<Level> levels() {
return levels;
}
}
record Offsets(Field x, Field z) {
}
record Level(NoiseStyle leaf, double scale, double fracture) {
}
}
@@ -0,0 +1,60 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.ArrayList;
import java.util.List;
final class CurveKnots {
private CurveKnots() {
}
static Field monotone(Field input, double[] locations, double[] values) {
double[] knots = increasing(locations);
return curve(input, knots, values, monotoneDerivatives(knots, values));
}
static Field smooth(Field input, double[] locations, double[] values) {
double[] knots = increasing(locations);
double[] derivatives = new double[knots.length];
for (int index = 1; index + 1 < knots.length; index++) {
derivatives[index] = (values[index + 1] - values[index - 1]) / (knots[index + 1] - knots[index - 1]);
}
return curve(input, knots, values, derivatives);
}
private static Field curve(Field input, double[] knots, double[] values, double[] derivatives) {
if (knots.length != values.length) {
throw new IllegalArgumentException("Curve needs one value per knot, got " + values.length + " values for " + knots.length + " knots");
}
List<Field.Knot> points = new ArrayList<>(knots.length);
for (int index = 0; index < knots.length; index++) {
points.add(new Field.Knot(knots[index], new Field.Const(values[index]), derivatives[index]));
}
return new Field.Curve(input, points);
}
private static double[] increasing(double[] locations) {
double[] knots = new double[locations.length];
float previous = Float.NEGATIVE_INFINITY;
for (int index = 0; index < locations.length; index++) {
float location = (float) locations[index];
if (location <= previous) {
location = Math.nextUp(previous);
}
knots[index] = location;
previous = location;
}
return knots;
}
private static double[] monotoneDerivatives(double[] knots, double[] values) {
double[] derivatives = new double[knots.length];
for (int index = 1; index + 1 < knots.length; index++) {
double left = (values[index] - values[index - 1]) / (knots[index] - knots[index - 1]);
double right = (values[index + 1] - values[index]) / (knots[index + 1] - knots[index]);
derivatives[index] = left * right <= 0.0D ? 0.0D : 2.0D * left * right / (left + right);
}
return derivatives;
}
}
@@ -0,0 +1,397 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
public final class DefaultCellularStyleLowering implements CellularStyleLowering {
private static final double CELL_BLOCKS = 64.0D;
private static final double SIMPLEX_CELL_BLOCKS = 100.0D;
private static final double SIMPLEX_CELL_FRACTURE = 200.0D;
private static final double HEX_BLOCKS = 32.0D;
private static final double SMOOTHING_BLOCKS = 32.0D;
private static final int SMOOTHING_CELL_Y = 8;
private static final double THIN_POWER = 1.0D / 0.65D;
private static final double IRIS_CELL_FREQUENCY = 0.01D;
private static final double CRACK_CELL_SCALE = 0.73D;
private static final double CRACK_SIGNATURE_ZOOM = 1.5625D;
private static final int SECOND_VALUE_TURNS = 5;
private static final int SECOND_VALUE_INTERVALS = 120;
private static final double RANGE_LIMIT = 1.0e6D;
private static final String NAMESPACE_PREFIX = "cell/";
private static final String FRACTURE_X = "fracture_x";
private static final String FRACTURE_Z = "fracture_z";
private final List<ExportDiagnostic> diagnostics;
private final CellularStyleLowering nestedStyles;
private final CellularMode cellMode;
public DefaultCellularStyleLowering(List<ExportDiagnostic> diagnostics, CellularStyleLowering nestedStyles, CellularMode cellMode) {
this.diagnostics = Objects.requireNonNull(diagnostics, "diagnostics");
this.nestedStyles = Objects.requireNonNull(nestedStyles, "nestedStyles");
this.cellMode = Objects.requireNonNull(cellMode, "cellMode");
if (cellMode == CellularMode.GRID_3X3X3) {
throw new IllegalArgumentException("Planar cell values lower in GRID_3X3 or QUADRANT_2X2 mode, got " + cellMode);
}
}
public static boolean supports(NoiseStyle style) {
return style != null && shape(style) != null;
}
@Override
public Field lower(IrisGeneratorStyle style, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities) {
Objects.requireNonNull(style, "style");
Objects.requireNonNull(registry, "registry");
Objects.requireNonNull(capabilities, "capabilities");
if (style.getExpression() != null || style.getImageMap() != null) {
throw new IllegalArgumentException("Cellular lowering needs a plain noise style, got an expression or image map style");
}
NoiseStyle noiseStyle = style.getStyle() == null ? NoiseStyle.FLAT : style.getStyle();
Shape shape = shape(noiseStyle);
if (shape == null) {
return smoothCore(style, noiseStyle, seedSalt, registry, capabilities);
}
if (style.getCellularFrequency() > 0.0D) {
report(noiseStyle, "Noise style " + noiseStyle.name() + " exports without its cellularFrequency cell lookup");
}
Request request = new Request(instanceId(style, seedSalt, capabilities.target()), cellMode, style, noiseStyle, shape, seedSalt, registry, capabilities);
Field value = switch (shape.family()) {
case CELL_VALUE -> cellValue(request);
case VASCULAR, GLOB, CELL_HEIGHT -> distanceStyle(request);
case SIMPLEX_CELLS -> simplexCells(request);
case SIMPLEX_VASCULAR -> simplexVascular(request);
case WHITE -> white(request);
case HEXAGON -> hexagon(request);
case PATTERN -> pattern(request);
};
return power(request, value, shape.power() * style.getExponent());
}
public Field cellCracks(IrisGenerator generator, Field value, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities) {
Objects.requireNonNull(generator, "generator");
Objects.requireNonNull(value, "value");
Objects.requireNonNull(registry, "registry");
Objects.requireNonNull(capabilities, "capabilities");
double height = generator.getCellFractureHeight();
if (height == 0.0D) {
return value;
}
double zoom = generator.getCellFractureZoom();
DataVersion target = capabilities.target();
String description = "cracks|" + seedSalt + "|" + zoom + "|" + generator.getCellFractureShuffle() + "|" + target.name();
String instanceId = "cracks/" + Long.toUnsignedString(CellHashNoise.stableHash(description), 36);
String prefix = NAMESPACE_PREFIX + instanceId + "/";
CellularWarp.Offsets chain = CellularWarp.signature(prefix, CellularWarp.Signature.IRIS, zoom, registry, target);
double shuffle = generator.getCellFractureShuffle() * zoom / CRACK_CELL_SCALE;
Field warpX = shuffleWarp(prefix + "shuffle_x", instanceId + "/shuffle_x", chain, zoom, shuffle, registry, target);
Field warpZ = shuffleWarp(prefix + "shuffle_z", instanceId + "/shuffle_z", chain, zoom, shuffle, registry, target);
double cell = zoom / (IRIS_CELL_FREQUENCY * CRACK_CELL_SCALE);
Field distance = halfGap(CellularLibrary.build(instanceId, cell, warpX, warpZ, CellularMode.GRID_3X3, registry));
Field crack = new Field.Shared(prefix + "distance", distance);
Field raised = new Field.Binary(Field.BinaryOp.MUL, value, new Field.Const(height));
return new Field.RangeIf(crack, generator.getCellPercentSize(), RANGE_LIMIT, raised, value);
}
private Field cellValue(Request request) {
CellularWarp.Offsets warp = warp(request);
double zoom = request.zoom();
Field value = CellularLibrary.build(request.instanceId(), CELL_BLOCKS * zoom, warp.x(), warp.z(), cellMode, request.registry()).cellValue();
if (!request.shape().smoothed()) {
return value;
}
return new Field.Shared(request.modePrefix() + "smoothed", new Field.Grid(value, smoothingCell(zoom), SMOOTHING_CELL_Y));
}
private Field distanceStyle(Request request) {
Family family = request.shape().family();
CellularWarp.Offsets warp = warp(request);
double cell = CELL_BLOCKS * request.zoom();
CellularOutputs outputs = CellularLibrary.build(request.instanceId(), cell, warp.x(), warp.z(), CellularMode.GRID_3X3, request.registry());
Field distance = switch (family) {
case VASCULAR -> vascular(outputs);
case GLOB -> new Field.Binary(Field.BinaryOp.DIV, outputs.f1(), outputs.f2());
case CELL_HEIGHT -> new Field.Clamp(halfGap(outputs), 0.0D, 1.0D);
default -> throw new IllegalStateException("Family " + family + " is not a distance style");
};
return new Field.Shared(request.prefix() + "distance", distance);
}
private Field simplexCells(Request request) {
if (request.style().getFracture() != null) {
return smooth(request, NoiseStyle.SIMPLEX, userWarp(request));
}
double cell = SIMPLEX_CELL_BLOCKS * request.zoom();
Field first = CellularLibrary.build(request.instanceId() + "/cells", cell, null, null, cellMode, request.registry()).cellValue();
Field second = new Field.Shared(request.modePrefix() + "cells_second", secondValue(first));
CellularWarp.Offsets fracture = CellularWarp.fracture(request.modePrefix(), first, second, SIMPLEX_CELL_FRACTURE * request.zoom());
return CellularLookalike.smoothStyle(request.modeId(), NoiseStyle.SIMPLEX, request.zoom(), fracture.x(), fracture.z(), request.registry(), request.target());
}
private Field simplexVascular(Request request) {
if (request.style().getFracture() != null) {
return smooth(request, NoiseStyle.SIMPLEX, userWarp(request));
}
double cell = SIMPLEX_CELL_BLOCKS * request.zoom();
Field first = veinValue(request, request.instanceId() + "/veins_x", cell);
Field second = veinValue(request, request.instanceId() + "/veins_z", cell);
return smooth(request, NoiseStyle.SIMPLEX, CellularWarp.fracture(request.prefix(), first, second, SIMPLEX_CELL_FRACTURE * request.zoom()));
}
private Field white(Request request) {
if (!request.shape().smoothed()) {
return new Field.Shared(request.prefix() + "white", CellHashNoise.white(request.instanceId(), request.registry(), request.target()));
}
Field white = CellHashNoise.white(request.instanceId(), request.registry(), request.target());
return new Field.Shared(request.prefix() + "smoothed", new Field.Grid(white, smoothingCell(request.zoom()), SMOOTHING_CELL_Y));
}
private Field hexagon(Request request) {
CellularWarp.Offsets warp = warp(request);
return HexLattice.centerValue(request.instanceId(), HEX_BLOCKS * request.zoom(), warp.x(), warp.z(), request.registry());
}
private Field pattern(Request request) {
report(request.noiseStyle(), "Noise style " + request.noiseStyle().name() + " exports as fitted smooth noise");
return smooth(request, request.noiseStyle(), userWarp(request));
}
private Field veinValue(Request request, String instanceId, double cell) {
CellularOutputs outputs = CellularLibrary.build(instanceId, cell, null, null, CellularMode.GRID_3X3, request.registry());
return new Field.Shared(NAMESPACE_PREFIX + instanceId + "/vascular", vascular(outputs));
}
private Field smooth(Request request, NoiseStyle style, CellularWarp.Offsets warp) {
return CellularLookalike.smoothStyle(request.instanceId(), style, request.zoom(), warp.x(), warp.z(), request.registry(), request.target());
}
private CellularWarp.Offsets warp(Request request) {
if (request.style().getFracture() != null) {
return userWarp(request);
}
CellularWarp.Signature signature = request.shape().warp();
if (signature == null) {
return CellularWarp.NONE;
}
return CellularWarp.signature(request.prefix(), signature, request.zoom(), request.registry(), request.target());
}
private CellularWarp.Offsets userWarp(Request request) {
IrisGeneratorStyle fracture = request.style().getFracture();
if (fracture == null) {
return CellularWarp.NONE;
}
IrisGeneratorStyle zoomed = zoomedCopy(fracture, fracture.getZoom() * request.zoom());
Field x = nestedStyles.lower(zoomed, salt(request.seedSalt(), FRACTURE_X), request.registry(), request.capabilities());
Field z = nestedStyles.lower(zoomed, salt(request.seedSalt(), FRACTURE_Z), request.registry(), request.capabilities());
return CellularWarp.fracture(request.modePrefix(), x, z, fracture.getMultiplier() * request.zoom());
}
private Field power(Request request, Field value, double exponent) {
if (exponent == 1.0D) {
return value;
}
Field constant = new Field.Const(exponent);
if (request.shape().family() != Family.PATTERN) {
return new Field.Shared(request.modePrefix() + "style", new Field.Binary(Field.BinaryOp.POW, new Field.Clamp(value, 0.0D, 1.0D), constant));
}
Field magnitude = new Field.Binary(Field.BinaryOp.POW, new Field.Unary(Field.UnaryOp.ABS, value), constant);
return new Field.Shared(request.modePrefix() + "style", new Field.Binary(Field.BinaryOp.MUL, new Field.Unary(Field.UnaryOp.SIGN, value), magnitude));
}
private Field shuffleWarp(String name, String instanceId, CellularWarp.Offsets chain, double zoom, double shuffle, NoiseRegistry registry, DataVersion target) {
Field value = CellularLookalike.smoothStyle(instanceId, NoiseStyle.SIMPLEX, CRACK_SIGNATURE_ZOOM * zoom, chain.x(), chain.z(), registry, target);
return new Field.Shared(name, new Field.Binary(Field.BinaryOp.MUL, value, new Field.Const(shuffle)));
}
private void report(NoiseStyle style, String message) {
ExportDiagnostic diagnostic = new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.NOISE_STYLE, "noise style " + style.name(), message);
if (!diagnostics.contains(diagnostic)) {
diagnostics.add(diagnostic);
}
}
private static Field vascular(CellularOutputs outputs) {
Field gap = new Field.Binary(Field.BinaryOp.SUB, outputs.f2(), outputs.f1());
Field value = new Field.Binary(Field.BinaryOp.ADD, new Field.Const(1.0D), new Field.Binary(Field.BinaryOp.MUL, gap, new Field.Const(-0.5D)));
return new Field.Clamp(value, 0.0D, 1.0D);
}
private static Field halfGap(CellularOutputs outputs) {
return new Field.Binary(Field.BinaryOp.MUL, new Field.Binary(Field.BinaryOp.SUB, outputs.f2(), outputs.f1()), new Field.Const(0.5D));
}
private static Field secondValue(Field cellValue) {
double[] locations = new double[SECOND_VALUE_INTERVALS + 1];
double[] values = new double[SECOND_VALUE_INTERVALS + 1];
for (int index = 0; index <= SECOND_VALUE_INTERVALS; index++) {
double fraction = (double) index / SECOND_VALUE_INTERVALS;
double phase = SECOND_VALUE_TURNS * fraction - Math.floor(SECOND_VALUE_TURNS * fraction);
locations[index] = fraction;
values[index] = 1.0D - Math.abs(2.0D * phase - 1.0D);
}
return CurveKnots.smooth(cellValue, locations, values);
}
private static int smoothingCell(double zoom) {
return Math.max(1, (int) Math.round(SMOOTHING_BLOCKS * zoom));
}
private static long salt(long seedSalt, String role) {
return CellHashNoise.stableHash(seedSalt + "|" + role);
}
private static String instanceId(IrisGeneratorStyle style, long seedSalt, DataVersion target) {
String name = (style.getStyle() == null ? NoiseStyle.FLAT : style.getStyle()).name().toLowerCase(Locale.ROOT);
long hash = CellHashNoise.stableHash(describe(style) + "|" + seedSalt + "|" + target.name());
return name + "/" + Long.toUnsignedString(hash, 36);
}
private static String describe(IrisGeneratorStyle style) {
if (style == null) {
return "-";
}
String name = style.getStyle() == null ? NoiseStyle.FLAT.name() : style.getStyle().name();
return name + "(" + style.getZoom() + "," + style.getExponent() + "," + style.getMultiplier() + "," + style.getCellularFrequency()
+ "," + style.getCellularZoom() + "," + style.getExpression() + "," + style.getImageMap() + "," + describe(style.getFracture()) + ")";
}
private Field smoothCore(IrisGeneratorStyle style, NoiseStyle noiseStyle, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities) {
IrisGeneratorStyle core = withoutCellularParts(style);
if (describe(core).equals(describe(style))) {
throw new IllegalArgumentException("Noise style " + noiseStyle.name() + " is not a cellular-family style");
}
report(noiseStyle, "Noise style " + noiseStyle.name() + " exports without its cellular fracture and cell lookup");
return nestedStyles.lower(core, seedSalt, registry, capabilities);
}
private static IrisGeneratorStyle withoutCellularParts(IrisGeneratorStyle style) {
IrisGeneratorStyle fracture = style.getFracture();
IrisGeneratorStyle smoothFracture = fracture == null || supports(fracture.getStyle()) ? null : withoutCellularParts(fracture);
return zoomedCopy(style, style.getZoom()).setCellularFrequency(0.0D).setFracture(smoothFracture);
}
private static IrisGeneratorStyle zoomedCopy(IrisGeneratorStyle style, double zoom) {
return new IrisGeneratorStyle(style.getStyle())
.setCellularFrequency(style.getCellularFrequency())
.setCellularZoom(style.getCellularZoom())
.setZoom(zoom)
.setExpression(style.getExpression())
.setImageMap(style.getImageMap())
.setMultiplier(style.getMultiplier())
.setFracture(style.getFracture())
.setExponent(style.getExponent())
.setCacheSize(style.getCacheSize());
}
private static Shape shape(NoiseStyle style) {
return switch (style) {
case CELLULAR -> plain(Family.CELL_VALUE);
case CELLULAR_IRIS -> warped(Family.CELL_VALUE, CellularWarp.Signature.IRIS);
case CELLULAR_IRIS_THICK -> warped(Family.CELL_VALUE, CellularWarp.Signature.THICK);
case CELLULAR_IRIS_DOUBLE -> warped(Family.CELL_VALUE, CellularWarp.Signature.DOUBLE);
case CELLULAR_IRIS_HALF -> warped(Family.CELL_VALUE, CellularWarp.Signature.HALF);
case NOWHERE_CELLULAR -> warped(Family.CELL_VALUE, CellularWarp.Signature.PERLIN);
case CELLULAR_BILINEAR, CELLULAR_BICUBIC, CELLULAR_HERMITE,
CELLULAR_STARCAST_3, CELLULAR_STARCAST_6, CELLULAR_STARCAST_9, CELLULAR_STARCAST_12,
CELLULAR_BILINEAR_STARCAST_3, CELLULAR_BILINEAR_STARCAST_6, CELLULAR_BILINEAR_STARCAST_9, CELLULAR_BILINEAR_STARCAST_12,
CELLULAR_HERMITE_STARCAST_3, CELLULAR_HERMITE_STARCAST_6, CELLULAR_HERMITE_STARCAST_9, CELLULAR_HERMITE_STARCAST_12 -> smoothed(Family.CELL_VALUE);
case VASCULAR -> plain(Family.VASCULAR);
case VASCULAR_THIN -> new Shape(Family.VASCULAR, null, false, THIN_POWER);
case VASCULAR_IRIS -> warped(Family.VASCULAR, CellularWarp.Signature.IRIS);
case VASCULAR_IRIS_THICK -> warped(Family.VASCULAR, CellularWarp.Signature.THICK);
case VASCULAR_IRIS_DOUBLE -> warped(Family.VASCULAR, CellularWarp.Signature.DOUBLE);
case VASCULAR_IRIS_HALF -> warped(Family.VASCULAR, CellularWarp.Signature.HALF);
case NOWHERE_VASCULAR -> warped(Family.VASCULAR, CellularWarp.Signature.PERLIN);
case GLOB -> plain(Family.GLOB);
case GLOB_IRIS -> warped(Family.GLOB, CellularWarp.Signature.IRIS);
case GLOB_IRIS_THICK -> warped(Family.GLOB, CellularWarp.Signature.THICK);
case GLOB_IRIS_DOUBLE -> warped(Family.GLOB, CellularWarp.Signature.DOUBLE);
case GLOB_IRIS_HALF -> warped(Family.GLOB, CellularWarp.Signature.HALF);
case NOWHERE_GLOB -> warped(Family.GLOB, CellularWarp.Signature.PERLIN);
case CELLULAR_HEIGHT -> plain(Family.CELL_HEIGHT);
case CELLULAR_HEIGHT_IRIS -> warped(Family.CELL_HEIGHT, CellularWarp.Signature.IRIS);
case CELLULAR_HEIGHT_IRIS_THICK -> warped(Family.CELL_HEIGHT, CellularWarp.Signature.THICK);
case CELLULAR_HEIGHT_IRIS_DOUBLE -> warped(Family.CELL_HEIGHT, CellularWarp.Signature.DOUBLE);
case CELLULAR_HEIGHT_IRIS_HALF -> warped(Family.CELL_HEIGHT, CellularWarp.Signature.HALF);
case SIMPLEX_CELLS -> plain(Family.SIMPLEX_CELLS);
case SIMPLEX_VASCULAR -> plain(Family.SIMPLEX_VASCULAR);
case STATIC -> plain(Family.WHITE);
case STATIC_BILINEAR, STATIC_BICUBIC, STATIC_HERMITE -> smoothed(Family.WHITE);
case HEXAGON, FRACTAL_HEXAGON -> plain(Family.HEXAGON);
case HEXAGON_IRIS -> warped(Family.HEXAGON, CellularWarp.Signature.IRIS);
case HEXAGON_IRIS_THICK -> warped(Family.HEXAGON, CellularWarp.Signature.THICK);
case HEXAGON_IRIS_DOUBLE -> warped(Family.HEXAGON, CellularWarp.Signature.DOUBLE);
case HEXAGON_IRIS_HALF -> warped(Family.HEXAGON, CellularWarp.Signature.HALF);
case NOWHERE_HEXAGON -> warped(Family.HEXAGON, CellularWarp.Signature.PERLIN);
case HEX_JAMES, HEX_SIMPLEX, HEX_RANDOM_SIZE, SIERPINSKI_TRIANGLE,
NOWHERE_HEX_JAMES, NOWHERE_HEX_SIMPLEX, NOWHERE_HEX_RANDOM_SIZE, NOWHERE_SIERPINSKI_TRIANGLE,
FRACTAL_HEX_JAMES, FRACTAL_HEX_SIMPLEX, FRACTAL_HEX_RANDOM_SIZE, FRACTAL_SIERPINSKI_TRIANGLE,
HEX_JAMES_IRIS, HEX_JAMES_IRIS_DOUBLE, HEX_JAMES_IRIS_THICK, HEX_JAMES_IRIS_HALF,
HEX_SIMPLEX_IRIS, HEX_SIMPLEX_IRIS_DOUBLE, HEX_SIMPLEX_IRIS_THICK, HEX_SIMPLEX_IRIS_HALF,
HEX_RANDOM_SIZE_IRIS, HEX_RANDOM_SIZE_IRIS_DOUBLE, HEX_RANDOM_SIZE_IRIS_THICK, HEX_RANDOM_SIZE_IRIS_HALF,
GYROID, QUASICRYSTAL, TRUCHET, CRATER, VORTEX, DUNE, STRATA, WOOD, GABOR, MARBLE, SCALES, CHLADNI,
KALEIDOSCOPE, MENGER_SPONGE, CIRCUIT -> plain(Family.PATTERN);
default -> null;
};
}
private static Shape plain(Family family) {
return new Shape(family, null, false, 1.0D);
}
private static Shape warped(Family family, CellularWarp.Signature warp) {
return new Shape(family, warp, false, 1.0D);
}
private static Shape smoothed(Family family) {
return new Shape(family, null, true, 1.0D);
}
private enum Family {
CELL_VALUE,
VASCULAR,
GLOB,
CELL_HEIGHT,
SIMPLEX_CELLS,
SIMPLEX_VASCULAR,
WHITE,
HEXAGON,
PATTERN
}
private record Shape(Family family, CellularWarp.Signature warp, boolean smoothed, double power) {
}
private record Request(String instanceId, CellularMode cellMode, IrisGeneratorStyle style, NoiseStyle noiseStyle, Shape shape, long seedSalt,
NoiseRegistry registry, ExportCapabilities capabilities) {
private double zoom() {
return style.getZoom();
}
private DataVersion target() {
return capabilities.target();
}
private String prefix() {
return NAMESPACE_PREFIX + instanceId + "/";
}
private String modeId() {
return instanceId + CellularLibrary.modeSegment(cellMode);
}
private String modePrefix() {
return NAMESPACE_PREFIX + modeId() + "/";
}
}
}
@@ -0,0 +1,75 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.Axis;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import java.util.Objects;
public final class HexLattice {
public static final double COLUMN_SPACING = Math.sqrt(3.0D);
public static final double ROW_SPACING = 3.0D;
public static final double VALUE_FREQUENCY = 0.18D;
private static final DataVersion TARGET = DataVersion.V26_3;
private static final double RANGE_LIMIT = 1.0e6D;
private static final String NAMESPACE_PREFIX = "cell/";
private HexLattice() {
}
public static Centers centers(String instanceId, double hexSizeBlocks, Field warpX, Field warpZ) {
String prefix = prefix(instanceId);
Field x = CellularLibrary.cellCoordinate(prefix + "hex_x", Axis.X, hexSizeBlocks, warpX);
Field z = CellularLibrary.cellCoordinate(prefix + "hex_z", Axis.Z, hexSizeBlocks, warpZ);
Field firstX = new Field.Shared(prefix + "hex_c1x", snap(x, COLUMN_SPACING, 0.0D));
Field firstZ = new Field.Shared(prefix + "hex_c1z", snap(z, ROW_SPACING, 0.0D));
Field secondX = new Field.Shared(prefix + "hex_c2x", snap(x, COLUMN_SPACING, COLUMN_SPACING / 2.0D));
Field secondZ = new Field.Shared(prefix + "hex_c2z", snap(z, ROW_SPACING, ROW_SPACING / 2.0D));
Field difference = new Field.Shared(prefix + "hex_diff", new Field.Binary(Field.BinaryOp.SUB,
squaredDistance(x, z, firstX, firstZ), squaredDistance(x, z, secondX, secondZ)));
return new Centers(
new Field.Shared(prefix + "hex_cx", new Field.RangeIf(difference, -RANGE_LIMIT, 0.0D, firstX, secondX)),
new Field.Shared(prefix + "hex_cz", new Field.RangeIf(difference, -RANGE_LIMIT, 0.0D, firstZ, secondZ)));
}
public static Field centerValue(String instanceId, double hexSizeBlocks, Field warpX, Field warpZ, NoiseRegistry registry) {
Objects.requireNonNull(registry, "registry");
Centers centers = centers(instanceId, hexSizeBlocks, warpX, warpZ);
IrisSampleFit.Profile vanilla = IrisSampleFit.vanilla(TARGET);
IrisSampleFit.Profile iris = IrisSampleFit.simplexSlice();
double factor = VALUE_FREQUENCY * vanilla.correlationLength() / iris.correlationLength();
String prefix = prefix(instanceId);
Field noise = new Field.Noise(LeafNoise.register(registry), 0.0D, 0.0D,
new Field.Binary(Field.BinaryOp.MUL, centers.x(), new Field.Const(factor)),
new Field.Const(CellHashNoise.plane(prefix + "hex_value")),
new Field.Binary(Field.BinaryOp.MUL, centers.z(), new Field.Const(factor)));
return new Field.Shared(prefix + "hex_value", LeafNoise.toMarginal(noise, vanilla, iris));
}
private static String prefix(String instanceId) {
return NAMESPACE_PREFIX + VanillaResourceIdentifier.requirePath(instanceId, "hex lattice instance id") + "/";
}
private static Field snap(Field coordinate, double spacing, double offset) {
if (offset == 0.0D) {
return new Field.Round(Field.RoundMode.ROUND, coordinate, new Field.Const(spacing));
}
Field shifted = new Field.Binary(Field.BinaryOp.ADD, coordinate, new Field.Const(-offset));
Field snapped = new Field.Round(Field.RoundMode.ROUND, shifted, new Field.Const(spacing));
return new Field.Binary(Field.BinaryOp.ADD, snapped, new Field.Const(offset));
}
private static Field squaredDistance(Field x, Field z, Field centerX, Field centerZ) {
Field deltaX = new Field.Binary(Field.BinaryOp.SUB, x, centerX);
Field deltaZ = new Field.Binary(Field.BinaryOp.SUB, z, centerZ);
return new Field.Binary(Field.BinaryOp.ADD, new Field.Unary(Field.UnaryOp.SQUARE, deltaX), new Field.Unary(Field.UnaryOp.SQUARE, deltaZ));
}
public record Centers(Field x, Field z) {
public Centers {
Objects.requireNonNull(x, "x");
Objects.requireNonNull(z, "z");
}
}
}
@@ -0,0 +1,173 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.noise.FastNoiseDouble;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.DoubleBinaryOperator;
final class IrisSampleFit {
private static final long FIT_SEED = 1_337L;
private static final String FIT_NOISE = "iris_export:cell_leaf";
private static final double VANILLA_PLANE = 4.375D;
private static final int ROWS = 32;
private static final int STYLE_SEEDS = 4;
private static final int VANILLA_SEEDS = 8;
private static final long SEED_STRIDE = 7_919L;
private static final int ROW_LENGTH = 1024;
private static final int LAGS = 256;
private static final int MARGINAL_AXIS = 64;
private static final double STYLE_STEP = 1.0D;
private static final double STYLE_ROW_SPACING = 211.0D;
private static final double STYLE_MARGINAL_SPACING = 37.0D;
private static final double UNIT_STEP = 0.025D;
private static final double UNIT_ROW_SPACING = 7.31D;
private static final double UNIT_MARGINAL_SPACING = 1.37D;
private static final double CORRELATION_THRESHOLD = 0.5D;
private static final double ROW_SHEAR = 1.618033988749895D;
private static final Map<String, Profile> PROFILES = new ConcurrentHashMap<>();
private IrisSampleFit() {
}
static Profile style(NoiseStyle style) {
return PROFILES.computeIfAbsent("style/" + style.name(), ignored -> styleProfile(style));
}
static Profile simplexSlice() {
return PROFILES.computeIfAbsent("simplex_slice", ignored -> simplexSliceProfile());
}
static Profile vanilla(DataVersion target) {
return PROFILES.computeIfAbsent("vanilla/" + target.name(), ignored -> vanillaProfile(target));
}
static double[] quantiles(double[] sorted, int intervals) {
double[] quantiles = new double[intervals + 1];
for (int index = 0; index <= intervals; index++) {
quantiles[index] = quantile(sorted, (double) index / intervals);
}
return quantiles;
}
private static Profile styleProfile(NoiseStyle style) {
List<DoubleBinaryOperator> samplers = new ArrayList<>(STYLE_SEEDS);
for (int index = 0; index < STYLE_SEEDS; index++) {
CNG generator = style.create(new RNG(seed(index)));
samplers.add(generator::noise);
}
return measure(samplers, STYLE_STEP, STYLE_ROW_SPACING, STYLE_MARGINAL_SPACING);
}
private static Profile simplexSliceProfile() {
List<DoubleBinaryOperator> samplers = new ArrayList<>(STYLE_SEEDS);
for (int index = 0; index < STYLE_SEEDS; index++) {
FastNoiseDouble simplex = new FastNoiseDouble(new RNG(seed(index)).lmax());
simplex.setFrequency(1.0D);
samplers.add((x, z) -> 0.5D + 0.5D * simplex.GetSimplex(x, 0.0D, z));
}
return measure(samplers, UNIT_STEP, UNIT_ROW_SPACING, UNIT_MARGINAL_SPACING);
}
private static Profile vanillaProfile(DataVersion target) {
List<DoubleBinaryOperator> samplers = new ArrayList<>(VANILLA_SEEDS);
for (int index = 0; index < VANILLA_SEEDS; index++) {
VanillaNoiseModel model = VanillaNoiseModel.create(FIT_NOISE, 0, List.of(1.0D), seed(index), target);
samplers.add((x, z) -> model.sample(x, VANILLA_PLANE, z));
}
return measure(samplers, UNIT_STEP, UNIT_ROW_SPACING, UNIT_MARGINAL_SPACING);
}
private static long seed(int index) {
return FIT_SEED + SEED_STRIDE * index;
}
private static Profile measure(List<DoubleBinaryOperator> samplers, double step, double rowSpacing, double marginalSpacing) {
double[] marginal = new double[MARGINAL_AXIS * MARGINAL_AXIS];
for (int row = 0; row < MARGINAL_AXIS; row++) {
DoubleBinaryOperator sampler = samplers.get(row % samplers.size());
for (int column = 0; column < MARGINAL_AXIS; column++) {
marginal[row * MARGINAL_AXIS + column] = sampler.applyAsDouble(column * marginalSpacing, row * marginalSpacing);
}
}
Arrays.sort(marginal);
return new Profile(correlationLength(samplers, step, rowSpacing), deviation(marginal), marginal);
}
private static double correlationLength(List<DoubleBinaryOperator> samplers, double step, double rowSpacing) {
double[][] rows = new double[ROWS][ROW_LENGTH + LAGS];
double sum = 0.0D;
for (int row = 0; row < ROWS; row++) {
DoubleBinaryOperator sampler = samplers.get(row % samplers.size());
for (int column = 0; column < ROW_LENGTH + LAGS; column++) {
rows[row][column] = sampler.applyAsDouble(row * rowSpacing * ROW_SHEAR + column * step, row * rowSpacing);
}
for (int column = 0; column < ROW_LENGTH; column++) {
sum += rows[row][column];
}
}
double mean = sum / (ROWS * ROW_LENGTH);
double previous = 1.0D;
double variance = covariance(rows, mean, 0);
if (variance <= 0.0D) {
return LAGS * step;
}
for (int lag = 1; lag <= LAGS; lag++) {
double correlation = covariance(rows, mean, lag) / variance;
if (correlation < CORRELATION_THRESHOLD) {
double fraction = (previous - CORRELATION_THRESHOLD) / (previous - correlation);
return (lag - 1 + fraction) * step;
}
previous = correlation;
}
return LAGS * step;
}
private static double covariance(double[][] rows, double mean, int lag) {
double sum = 0.0D;
for (double[] row : rows) {
for (int column = 0; column < ROW_LENGTH; column++) {
sum += (row[column] - mean) * (row[column + lag] - mean);
}
}
return sum / (ROWS * ROW_LENGTH);
}
private static double deviation(double[] samples) {
double sum = 0.0D;
for (double sample : samples) {
sum += sample;
}
double mean = sum / samples.length;
double squares = 0.0D;
for (double sample : samples) {
squares += (sample - mean) * (sample - mean);
}
return Math.sqrt(squares / samples.length);
}
private static double quantile(double[] sorted, double fraction) {
double position = fraction * (sorted.length - 1);
int lower = (int) Math.floor(position);
int upper = Math.min(sorted.length - 1, lower + 1);
double weight = position - lower;
return sorted[lower] + (sorted[upper] - sorted[lower]) * weight;
}
record Profile(double correlationLength, double deviation, double[] sorted) {
double quantile(double fraction) {
return IrisSampleFit.quantile(sorted, fraction);
}
double[] quantiles(int intervals) {
return IrisSampleFit.quantiles(sorted, intervals);
}
}
}
@@ -0,0 +1,37 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import java.util.List;
final class LeafNoise {
static final String NOISE_ID = "cell_leaf";
private static final int MARGINAL_INTERVALS = 32;
private LeafNoise() {
}
static String register(NoiseRegistry registry) {
return registry.register(new NoiseDef(NOISE_ID, 0, List.of(1.0D)));
}
static Field sample(String noiseId, double xzScale, String planeKey, Field shiftXBlocks, Field shiftZBlocks) {
return new Field.Noise(noiseId, xzScale, 0.0D, scaled(shiftXBlocks, xzScale), new Field.Const(CellHashNoise.plane(planeKey)), scaled(shiftZBlocks, xzScale));
}
static double xzScale(IrisSampleFit.Profile vanilla, double irisCorrelationBlocks) {
return vanilla.correlationLength() / irisCorrelationBlocks;
}
static Field toMarginal(Field noise, IrisSampleFit.Profile vanilla, IrisSampleFit.Profile iris) {
return CurveKnots.monotone(noise, vanilla.quantiles(MARGINAL_INTERVALS), iris.quantiles(MARGINAL_INTERVALS));
}
private static Field scaled(Field shiftBlocks, double xzScale) {
if (shiftBlocks == null) {
return null;
}
return new Field.Binary(Field.BinaryOp.MUL, shiftBlocks, new Field.Const(xzScale));
}
}
@@ -0,0 +1,224 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.noise.NoiseType;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.SplittableRandom;
import java.util.function.DoubleBinaryOperator;
public final class LeafNoiseFit {
static final long FIT_SEED = 0x5EED_1EAFL;
private static final String FIT_NOISE_ID = "iris_fit:leaf";
private static final int MARGINAL_SAMPLES = 65_536;
private static final int PROFILE_POINTS = 4096;
private static final int INSTANCES = 16;
private static final long INSTANCE_STRIDE = 0x9E37_79B9L;
private static final double IRIS_SPAN = 1 << 20;
private static final double VANILLA_SPAN = 1 << 15;
private static final double SEARCH_RATIO = 3.0;
private static final int SEARCH_ITERATIONS = 40;
private static final double[] IRIS_LAGS = RankCorrelation.geometricLags(1.0, 4096.0, 72, true);
private static final double[] VANILLA_LAGS = RankCorrelation.geometricLags(0.002, 64.0, 160, false);
private static final double[] TAIL_AMPLITUDES = {0.0, 0.25, 0.5, 0.75, 1.0};
private static final int TOPOLOGY_SIZE = 256;
private static final double TOPOLOGY_PIXELS_PER_HALF = 4.0;
private static final double TOPOLOGY_WEIGHT = 0.05;
private static final double TOPOLOGY_ORIGIN = 4096.0;
private static final double LOWER_TAIL = 0.15;
private static final double UPPER_TAIL = 0.85;
private LeafNoiseFit() {
}
public static FittedLeaf fit(NoiseType type, int octaves, DataVersion target) {
Objects.requireNonNull(type, "type");
Objects.requireNonNull(target, "target");
if (NoiseLeaves.isCellular(type) || type == NoiseType.FLAT) {
throw new IllegalArgumentException("Noise type " + type + " is not a smooth leaf");
}
int effectiveOctaves = NoiseLeaves.effectiveOctaves(type, octaves);
double coordinateScale = type.getCoordinateScale();
CNG generator = new CNG(new RNG(FIT_SEED), type, 1.0, effectiveOctaves);
DoubleBinaryOperator iris = (x, z) -> generator.noise(x * coordinateScale, z * coordinateScale);
double[] irisMarginal = marginal(iris, IRIS_SPAN, FIT_SEED, MARGINAL_SAMPLES);
double[] irisProfile = RankCorrelation.profile(iris, IRIS_LAGS, PROFILE_POINTS, IRIS_SPAN, true, FIT_SEED + 1L);
double irisHalf = RankCorrelation.halfCorrelationLag(IRIS_LAGS, irisProfile);
int[] window = RankCorrelation.window(irisProfile);
double spacing = irisHalf / TOPOLOGY_PIXELS_PER_HALF;
int irisFragments = fragments((x, z) -> iris.applyAsDouble(TOPOLOGY_ORIGIN + x * spacing, TOPOLOGY_ORIGIN + z * spacing));
Candidate best = null;
for (List<Double> profile : profiles(type, effectiveOctaves)) {
Candidate candidate = candidate(profile, target, irisProfile, irisHalf, window, spacing, irisFragments);
if (best == null || candidate.error() < best.error()) {
best = candidate;
}
}
Objects.requireNonNull(best, "candidate");
List<Field.Knot> distribution = ValueDistributionFit.fit(best.marginal(), irisMarginal);
NoiseDef noise = new NoiseDef(leafId(type, effectiveOctaves), 0, best.amplitudes());
return new FittedLeaf(noise, best.frequency() / coordinateScale, distribution);
}
static String leafId(NoiseType type, int octaves) {
return type.name().toLowerCase(Locale.ROOT) + "_" + octaves;
}
private static List<List<Double>> profiles(NoiseType type, int octaves) {
List<Double> base = NoiseLeaves.isOctaveSum(type) ? Collections.nCopies(octaves, 1.0) : List.of(1.0);
List<List<Double>> profiles = new ArrayList<>(TAIL_AMPLITUDES.length);
for (double tail : TAIL_AMPLITUDES) {
List<Double> profile = new ArrayList<>(base);
if (tail > 0.0) {
profile.add(tail);
}
profiles.add(List.copyOf(profile));
}
return profiles;
}
private static Candidate candidate(List<Double> amplitudes, DataVersion target, double[] irisProfile, double irisHalf, int[] window,
double spacing, int irisFragments) {
Ensemble ensemble = ensemble(amplitudes, target);
double vanillaHalf = RankCorrelation.halfCorrelationLag(VANILLA_LAGS, ensemble.profile());
double guess = Math.log(vanillaHalf / irisHalf);
double logFrequency = RankCorrelation.goldenMinimum(
candidate -> error(irisProfile, window, ensemble.profile(), Math.exp(candidate)),
guess - Math.log(SEARCH_RATIO), guess + Math.log(SEARCH_RATIO), SEARCH_ITERATIONS);
double frequency = Math.exp(logFrequency);
double topology = Math.log(Math.max(1.0, fragments(ensemble.instances(), spacing * frequency)) / Math.max(1, irisFragments));
double score = error(irisProfile, window, ensemble.profile(), frequency) + TOPOLOGY_WEIGHT * topology * topology;
return new Candidate(amplitudes, frequency, score, ensemble.marginal());
}
private static Ensemble ensemble(List<Double> amplitudes, DataVersion target) {
double[] marginal = new double[0];
double[] profile = new double[VANILLA_LAGS.length];
List<DoubleBinaryOperator> instances = new ArrayList<>(INSTANCES);
for (int instance = 0; instance < INSTANCES; instance++) {
VanillaNoiseModel model = VanillaNoiseModel.create(FIT_NOISE_ID, 0, amplitudes, FIT_SEED + instance * INSTANCE_STRIDE, target);
DoubleBinaryOperator vanilla = (x, z) -> model.sample(x, 0.0, z);
instances.add(vanilla);
marginal = concat(marginal, marginal(vanilla, VANILLA_SPAN, FIT_SEED + 2L + instance, MARGINAL_SAMPLES / INSTANCES));
double[] sampled = RankCorrelation.profile(vanilla, VANILLA_LAGS, PROFILE_POINTS / INSTANCES, VANILLA_SPAN, false, FIT_SEED + 3L + instance);
for (int index = 0; index < profile.length; index++) {
profile[index] += sampled[index] / INSTANCES;
}
}
return new Ensemble(List.copyOf(instances), marginal, profile);
}
private static double fragments(List<DoubleBinaryOperator> instances, double pixel) {
double total = 0.0;
for (DoubleBinaryOperator vanilla : instances) {
total += fragments((x, z) -> vanilla.applyAsDouble(TOPOLOGY_ORIGIN + x * pixel, TOPOLOGY_ORIGIN + z * pixel));
}
return total / instances.size();
}
private static double error(double[] irisProfile, int[] window, double[] vanillaProfile, double frequency) {
double sum = 0.0;
for (int index : window) {
double expected = RankCorrelation.structure(irisProfile[index]);
double actual = RankCorrelation.structure(VANILLA_LAGS, vanillaProfile, IRIS_LAGS[index] * frequency);
sum += RankCorrelation.structureError(expected, actual);
}
return sum / window.length;
}
private static double[] marginal(DoubleBinaryOperator field, double span, long seed, int count) {
SplittableRandom random = new SplittableRandom(seed);
double[] samples = new double[count];
for (int index = 0; index < samples.length; index++) {
double x = (random.nextDouble() * 2.0 - 1.0) * span;
double z = (random.nextDouble() * 2.0 - 1.0) * span;
samples[index] = field.applyAsDouble(x, z);
}
return samples;
}
private static double[] concat(double[] first, double[] second) {
double[] joined = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, joined, first.length, second.length);
return joined;
}
private static int fragments(DoubleBinaryOperator field) {
double[] values = new double[TOPOLOGY_SIZE * TOPOLOGY_SIZE];
for (int z = 0; z < TOPOLOGY_SIZE; z++) {
for (int x = 0; x < TOPOLOGY_SIZE; x++) {
values[z * TOPOLOGY_SIZE + x] = field.applyAsDouble(x, z);
}
}
double[] sorted = values.clone();
Arrays.sort(sorted);
double lower = ValueDistributionFit.quantile(sorted, LOWER_TAIL);
double upper = ValueDistributionFit.quantile(sorted, UPPER_TAIL);
return components(values, upper, true) + components(values, lower, false);
}
private static int components(double[] values, double threshold, boolean above) {
boolean[] visited = new boolean[values.length];
ArrayDeque<Integer> queue = new ArrayDeque<>();
int count = 0;
for (int start = 0; start < values.length; start++) {
if (visited[start] || !inside(values[start], threshold, above)) {
continue;
}
count++;
visited[start] = true;
queue.add(start);
while (!queue.isEmpty()) {
int cell = queue.poll();
int x = cell % TOPOLOGY_SIZE;
int z = cell / TOPOLOGY_SIZE;
visit(values, visited, queue, x > 0 ? cell - 1 : -1, threshold, above);
visit(values, visited, queue, x < TOPOLOGY_SIZE - 1 ? cell + 1 : -1, threshold, above);
visit(values, visited, queue, z > 0 ? cell - TOPOLOGY_SIZE : -1, threshold, above);
visit(values, visited, queue, z < TOPOLOGY_SIZE - 1 ? cell + TOPOLOGY_SIZE : -1, threshold, above);
}
}
return count;
}
private static void visit(double[] values, boolean[] visited, ArrayDeque<Integer> queue, int cell, double threshold, boolean above) {
if (cell >= 0 && !visited[cell] && inside(values[cell], threshold, above)) {
visited[cell] = true;
queue.add(cell);
}
}
private static boolean inside(double value, double threshold, boolean above) {
return above ? value > threshold : value < threshold;
}
public record FittedLeaf(NoiseDef noise, double xzScale, List<Field.Knot> distribution) {
public FittedLeaf {
Objects.requireNonNull(noise, "noise");
distribution = List.copyOf(distribution);
if (!(xzScale > 0.0) || !Double.isFinite(xzScale)) {
throw new IllegalArgumentException("Fitted leaf xz scale must be positive and finite, got " + xzScale);
}
}
public Field.Curve curve(Field input) {
return new Field.Curve(input, distribution);
}
}
private record Candidate(List<Double> amplitudes, double frequency, double error, double[] marginal) {
}
private record Ensemble(List<DoubleBinaryOperator> instances, double[] marginal, double[] profile) {
}
}
@@ -0,0 +1,101 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.volmlib.util.noise.InterpolatedNoise;
import art.arcane.volmlib.util.noise.NoiseGenerator;
import art.arcane.volmlib.util.noise.NoiseType;
import art.arcane.volmlib.util.noise.OffsetNoiseGenerator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
final class NoiseLeaves {
private static final int MAXIMUM_OCTAVES = 16;
private static final Set<NoiseType> CELLULAR_BASES = EnumSet.of(
NoiseType.WHITE, NoiseType.CELLULAR, NoiseType.GLOB, NoiseType.CELLULAR_HEIGHT, NoiseType.VASCULAR,
NoiseType.HEXAGON, NoiseType.HEX_JAMES, NoiseType.HEX_SIMPLEX, NoiseType.HEX_RANDOM_SIZE);
private static final Set<NoiseType> OCTAVE_SUMS = EnumSet.of(NoiseType.SIMPLEX, NoiseType.PERLIN, NoiseType.FRACTAL_FBM_SIMPLEX);
private static final Set<NoiseType> OCTAVE_TYPES = EnumSet.of(
NoiseType.SIMPLEX, NoiseType.PERLIN, NoiseType.FRACTAL_FBM_SIMPLEX, NoiseType.FRACTAL_BILLOW_SIMPLEX,
NoiseType.FRACTAL_BILLOW_PERLIN, NoiseType.FRACTAL_RIGID_MULTI_SIMPLEX, NoiseType.FRACTAL_CUBIC);
private NoiseLeaves() {
}
static NoiseType typeOf(NoiseGenerator generator) {
NoiseGenerator base = unwrap(generator);
if (base instanceof InterpolatedNoise interpolated) {
String name = typeOf(interpolated.getGenerator()).name() + "_" + interpolated.getMethod().name();
return NoiseType.valueOf(name);
}
NoiseType type = Classes.TYPES.get(base.getClass());
if (type == null) {
throw new IllegalArgumentException("Noise generator " + base.getClass().getName() + " has no exportable noise type");
}
return type;
}
static boolean isCellular(NoiseType type) {
return CELLULAR_BASES.contains(baseOf(type));
}
static boolean isOctaveSum(NoiseType type) {
return OCTAVE_SUMS.contains(type);
}
static Fold fold(NoiseType type) {
return switch (type) {
case FRACTAL_BILLOW_SIMPLEX -> new Fold(FoldKind.BILLOW, NoiseType.SIMPLEX);
case FRACTAL_BILLOW_PERLIN -> new Fold(FoldKind.BILLOW, NoiseType.PERLIN);
case FRACTAL_CUBIC -> new Fold(FoldKind.BILLOW, NoiseType.CUBIC);
case FRACTAL_RIGID_MULTI_SIMPLEX -> new Fold(FoldKind.RIGID, NoiseType.SIMPLEX);
default -> new Fold(FoldKind.NONE, type);
};
}
static int effectiveOctaves(NoiseType type, int octaves) {
return OCTAVE_TYPES.contains(type) ? Math.clamp(octaves, 1, MAXIMUM_OCTAVES) : 1;
}
static NoiseGenerator unwrap(NoiseGenerator generator) {
NoiseGenerator current = generator;
while (current instanceof OffsetNoiseGenerator offset) {
current = offset.getBase();
}
return current;
}
private static NoiseType baseOf(NoiseType type) {
for (NoiseType candidate : CELLULAR_BASES) {
if (type == candidate || type.name().startsWith(candidate.name() + "_")) {
return candidate;
}
}
return type;
}
enum FoldKind {
NONE,
BILLOW,
RIGID
}
record Fold(FoldKind kind, NoiseType base) {
}
private static final class Classes {
private static final Map<Class<?>, NoiseType> TYPES = scan();
private static Map<Class<?>, NoiseType> scan() {
Map<Class<?>, NoiseType> types = new HashMap<>();
for (NoiseType type : NoiseType.values()) {
NoiseGenerator base = unwrap(type.create(0L));
if (!(base instanceof InterpolatedNoise)) {
types.putIfAbsent(base.getClass(), type);
}
}
return Map.copyOf(types);
}
}
}
@@ -0,0 +1,44 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
public final class NoiseRegistry {
private final String namespace;
private final SortedMap<String, NoiseDef> definitions = new TreeMap<>();
public NoiseRegistry(String namespace) {
this.namespace = VanillaResourceIdentifier.requireNamespace(namespace, "noise namespace");
}
public String namespace() {
return namespace;
}
public String register(NoiseDef definition) {
Objects.requireNonNull(definition);
NoiseDef existing = definitions.putIfAbsent(definition.id(), definition);
if (existing != null && !existing.equals(definition)) {
throw new IllegalArgumentException("Noise '" + namespace + ":" + definition.id() + "' is already registered with different parameters");
}
return namespace + ":" + definition.id();
}
public List<NoiseDef> definitions() {
return List.copyOf(definitions.values());
}
public SortedMap<String, NoiseDef> byReference() {
SortedMap<String, NoiseDef> references = new TreeMap<>();
for (NoiseDef definition : definitions.values()) {
references.put(namespace + ":" + definition.id(), definition);
}
return Collections.unmodifiableSortedMap(references);
}
}
@@ -0,0 +1,550 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisNoiseGenerator;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.CostClass;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.noise.NoiseType;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
public final class NoiseStyleCompiler implements StyleCompiler {
private static final long BUILD_SEED = 0x1215_57E1L;
private static final long FIT_SALT = 0x0F17_0F17L;
private static final String FIT_NAMESPACE = "iris_fit";
private static final String NOISE_PREFIX = "style/";
private static final String SHARED_PREFIX = "style/shared/";
private static final double SINE_KNOT_SPACING = 1.0 / 16.0;
private static final double SINE_DOMAIN_LIMIT = 16.0;
private final CellularStyleLowering cellular;
private final StyleCompiler selectors;
private final Map<LeafKey, LeafNoiseFit.FittedLeaf> leaves = new ConcurrentHashMap<>();
private final Map<LevelKey, WarpFit.LevelFit> levels = new ConcurrentHashMap<>();
public NoiseStyleCompiler(CellularStyleLowering cellular) {
this.cellular = Objects.requireNonNull(cellular, "cellular");
this.selectors = this;
}
private NoiseStyleCompiler(Function<StyleCompiler, CellularStyleLowering> cellular, StyleCompiler selectors) {
this.cellular = Objects.requireNonNull(cellular.apply(this), "cellular");
this.selectors = selectors == null ? this : selectors;
}
public static NoiseStyleCompiler create(List<ExportDiagnostic> diagnostics) {
Objects.requireNonNull(diagnostics, "diagnostics");
NoiseStyleCompiler selectors = new NoiseStyleCompiler(nested -> new DefaultCellularStyleLowering(diagnostics, nested::compile, CellularMode.QUADRANT_2X2), null);
return new NoiseStyleCompiler(nested -> new DefaultCellularStyleLowering(diagnostics, nested::compile, CellularMode.GRID_3X3), selectors);
}
public static CNG generator(IrisGeneratorStyle style) {
return build(prepared(Objects.requireNonNull(style, "style"), 1.0), 1);
}
@Override
public Field compile(IrisGeneratorStyle style, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities) {
Emit emit = new Emit(registry, capabilities, seedSalt);
return style(Objects.requireNonNull(style, "style"), 1, Domain.IDENTITY, emit, "s");
}
@Override
public StyleCompiler selectors() {
return selectors;
}
public Field compileLayer(IrisNoiseGenerator layer, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities) {
Emit emit = new Emit(registry, capabilities, seedSalt);
return layer(Objects.requireNonNull(layer, "layer"), Domain.IDENTITY, emit, "l" + layer.getSeed());
}
public Field compileComposite(IrisGenerator generator, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities) {
Objects.requireNonNull(generator, "generator");
Emit emit = new Emit(registry, capabilities, seedSalt);
KList<IrisNoiseGenerator> composite = generator.getComposite();
if (composite.isEmpty()) {
return new Field.Const(0.0);
}
Domain domain = new Domain(positive(generator.getZoom(), "generator zoom"), generator.getOffsetX(), generator.getOffsetZ(), null, null);
Field combined = null;
double totalOpacity = 0.0;
for (int index = 0; index < composite.size(); index++) {
IrisNoiseGenerator layer = composite.get(index);
Field value = layer(layer, domain, emit, "g" + index + "_" + layer.getSeed());
if (generator.isMultiplicitive()) {
combined = combined == null ? value : binary(Field.BinaryOp.MUL, combined, value);
} else {
totalOpacity += layer.getOpacity();
combined = combined == null ? value : binary(Field.BinaryOp.ADD, combined, value);
}
}
if (generator.isMultiplicitive()) {
return scaled(combined, generator.getOpacity());
}
if (totalOpacity == 0.0) {
throw new IllegalArgumentException("Generator '" + generator.getLoadKey() + "' composite opacity sums to zero");
}
return scaled(combined, generator.getOpacity() / totalOpacity);
}
public Field compileRange(IrisStyledRange range, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities) {
Objects.requireNonNull(range, "range");
if (range.getMin() == range.getMax()) {
return new Field.Const(range.getMin());
}
if (range.getStyle().isFlat()) {
return new Field.Const(range.getMin() + 0.5 * (range.getMax() - range.getMin()));
}
Field unit = compile(range.getStyle(), seedSalt, registry, capabilities);
return binary(Field.BinaryOp.ADD, new Field.Const(range.getMin()), scaled(unit, range.getMax() - range.getMin()));
}
public Optional<Warp> compileRootWarp(IrisGeneratorStyle style, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities) {
Emit emit = new Emit(registry, capabilities, seedSalt);
CNG root = generator(style);
if (!warped(root)) {
return Optional.empty();
}
String path = "w" + styleName(style);
Field warpX = displacement(node(root.getFracture(), Displacement.NONE, emit, path + "/x"), root.getFscale());
Field warpZ = displacement(node(root.getFracture(), Displacement.NONE, emit, path + "/z"), root.getFscale());
return Optional.of(new Warp(warpX, warpZ));
}
private Field layer(IrisNoiseGenerator layer, Domain domain, Emit emit, String path) {
if (!layer.isEnabled()) {
return new Field.Const(layer.getOffsetY());
}
Field warpX = null;
Field warpZ = null;
List<IrisNoiseGenerator> fractures = layer.getFracture();
for (int index = 0; index < fractures.size(); index++) {
IrisNoiseGenerator child = fractures.get(index);
if (!child.isEnabled()) {
continue;
}
Field center = new Field.Const(-child.getOpacity() / 2.0);
String childPath = path + "/f" + index + "_" + child.getSeed();
warpX = sum(warpX, binary(Field.BinaryOp.ADD, layer(child, domain, emit, childPath + "x"), center));
warpZ = sum(warpZ, binary(Field.BinaryOp.ADD, layer(child, domain.swapped(), emit, childPath + "z"), center));
}
Domain sampled = domain.layer(positive(layer.getZoom(), "layer zoom"), layer.getOffsetX(), layer.getOffsetZ(), warpX, warpZ);
Field value = style(layer.getStyle(), layer.getOctaves(), sampled, emit, path);
return layerPost(layer, value, emit, path);
}
private Field layerPost(IrisNoiseGenerator layer, Field input, Emit emit, String path) {
double opacity = layer.getOpacity();
Field value = scaled(input, opacity);
if (layer.isNegative()) {
value = binary(Field.BinaryOp.SUB, new Field.Const(opacity), value);
}
if (layer.getExponent() != 1.0) {
value = signedPower(value, layer.getExponent(), emit, path + "/e");
}
if (layer.getOffsetY() != 0.0) {
value = binary(Field.BinaryOp.ADD, value, new Field.Const(layer.getOffsetY()));
}
if (layer.isParametric()) {
Field shared = emit.shared(path + "/p", value);
Field square = new Field.Unary(Field.UnaryOp.SQUARE, shared);
Field complement = new Field.Unary(Field.UnaryOp.SQUARE, binary(Field.BinaryOp.SUB, new Field.Const(1.0), shared));
value = binary(Field.BinaryOp.DIV, square, binary(Field.BinaryOp.ADD, square, complement));
}
if (layer.isBezier()) {
Field shared = emit.shared(path + "/b", value);
Field weight = binary(Field.BinaryOp.SUB, new Field.Const(3.0), scaled(shared, 2.0));
value = binary(Field.BinaryOp.MUL, new Field.Unary(Field.UnaryOp.SQUARE, shared), weight);
}
if (layer.isSinCentered()) {
value = sineCentered(value);
}
return value;
}
private Field style(IrisGeneratorStyle style, int octaveMultiplier, Domain domain, Emit emit, String path) {
IrisGeneratorStyle prepared = prepared(Objects.requireNonNull(style, "noise style"), domain.zoom());
String stylePath = path + "/" + styleName(style);
CNG root = build(prepared, octaveMultiplier);
if (cellular(prepared, root)) {
if (domain.warpX() != null || domain.warpZ() != null) {
throw new IllegalArgumentException("Cellular noise style " + styleName(style) + " cannot be exported under a layer fracture warp");
}
return cellular.lower(prepared, emit.salt(stylePath), emit.registry(), emit.capabilities());
}
Displacement displacement = new Displacement(domain.offsetX(), domain.offsetZ(), domain.warpX(), domain.warpZ());
return node(root, displacement, emit, stylePath);
}
private Field node(CNG cng, Displacement displacement, Emit emit, String path) {
WarpFit.LevelFit fit = fitted(cng) ? levelFit(cng, emit.capabilities().target()) : null;
return fit == null ? node(cng, displacement, emit, path, 1.0, null) : node(cng, displacement, emit, path, fit.multiplier(), fit.distribution());
}
private Field node(CNG cng, Displacement displacement, Emit emit, String path, double multiplier, List<Field.Knot> distribution) {
requireSupported(cng);
NoiseType type = NoiseLeaves.typeOf(cng.getGenerator());
Field value;
if (type == NoiseType.FLAT) {
value = new Field.Const(1.0);
} else {
Displacement leafDisplacement = displacement;
if (warped(cng)) {
Field warpX = displacement(node(cng.getFracture(), displacement, emit, path + "/x"), cng.getFscale());
Field warpZ = displacement(node(cng.getFracture(), displacement.swapped(), emit, path + "/z"), cng.getFscale());
leafDisplacement = displacement.warped(warpX, warpZ);
}
value = leaf(type, cng.getOct(), cng.getEffectiveScale() * multiplier, leafDisplacement, emit, path);
if (distribution != null) {
value = new Field.Curve(value, distribution);
}
}
return post(cng, value, emit, path);
}
private Field leaf(NoiseType type, int octaves, double scale, Displacement displacement, Emit emit, String path) {
NoiseLeaves.Fold fold = NoiseLeaves.fold(type);
int effectiveOctaves = NoiseLeaves.effectiveOctaves(type, octaves);
DataVersion target = emit.capabilities().target();
if (fold.kind() == NoiseLeaves.FoldKind.NONE) {
LeafNoiseFit.FittedLeaf fitted = fitted(type, effectiveOctaves, target);
return fitted.curve(noise(fitted, fitted.xzScale() * scale, displacement, emit, path));
}
LeafNoiseFit.FittedLeaf base = fitted(fold.base(), 1, target);
List<Field.Knot> signed = signed(base.distribution());
Displacement shared = effectiveOctaves > 1 ? displacement.shared(emit, path) : displacement;
double bounding = bounding(effectiveOctaves);
Field sum = fold.kind() == NoiseLeaves.FoldKind.RIGID ? new Field.Const(bounding) : null;
double amplitude = 1.0;
for (int octave = 0; octave < effectiveOctaves; octave++) {
double octaveScale = base.xzScale() * scale * Math.scalb(1.0, octave);
Field noise = noise(base, octaveScale, shared, emit, path + "/o" + octave);
Field folded = new Field.Unary(Field.UnaryOp.ABS, new Field.Curve(noise, signed));
double weight = fold.kind() == NoiseLeaves.FoldKind.RIGID && octave == 0 ? -bounding : bounding * amplitude;
sum = sum(sum, scaled(folded, weight));
amplitude *= 0.5;
}
return sum;
}
private Field noise(LeafNoiseFit.FittedLeaf leaf, double xzScale, Displacement displacement, Emit emit, String path) {
String id = emit.noise(leaf, path);
return new Field.Noise(id, xzScale, 0.0, shift(displacement.offsetX(), displacement.warpX(), xzScale), null,
shift(displacement.offsetZ(), displacement.warpZ(), xzScale));
}
private Field post(CNG cng, Field input, Emit emit, String path) {
Field value = scaled(input, cng.getOpacity());
if (cng.getPower() != 1.0) {
value = signedPower(value, cng.getPower(), emit, path + "/power");
}
double offset = cng.getUp() - cng.getDown();
if (offset != 0.0) {
value = binary(Field.BinaryOp.ADD, value, new Field.Const(offset));
}
return scaled(value, cng.getPatch());
}
private WarpFit.LevelFit levelFit(CNG level, DataVersion target) {
LevelSpec spec = LevelSpec.of(level).canonical();
LevelKey key = new LevelKey(spec, target);
WarpFit.LevelFit cached = levels.get(key);
if (cached != null) {
return cached;
}
NoiseRegistry scratch = new NoiseRegistry(FIT_NAMESPACE);
Emit fitEmit = new Emit(scratch, ExportCapabilities.forTarget(target), FIT_SALT);
CNG iris = spec.build(LeafNoiseFit.FIT_SEED);
WarpFit.LevelFit fit = WarpFit.fit((x, z) -> iris.noise(x, z),
candidate -> node(iris, Displacement.NONE, fitEmit, "fit", candidate, null), scratch, target);
WarpFit.LevelFit existing = levels.putIfAbsent(key, fit);
return existing == null ? fit : existing;
}
private LeafNoiseFit.FittedLeaf fitted(NoiseType type, int octaves, DataVersion target) {
LeafKey key = new LeafKey(type, octaves, target);
LeafNoiseFit.FittedLeaf cached = leaves.get(key);
if (cached != null) {
return cached;
}
LeafNoiseFit.FittedLeaf fitted = LeafNoiseFit.fit(type, octaves, target);
LeafNoiseFit.FittedLeaf existing = leaves.putIfAbsent(key, fitted);
return existing == null ? fitted : existing;
}
private static Field signedPower(Field value, double power, Emit emit, String path) {
Field exponent = new Field.Const(power);
if (value.range().min() >= 0.0) {
return binary(Field.BinaryOp.POW, value, exponent);
}
Field shared = emit.shared(path, value);
Field magnitude = binary(Field.BinaryOp.POW, new Field.Unary(Field.UnaryOp.ABS, shared), exponent);
return binary(Field.BinaryOp.MUL, new Field.Unary(Field.UnaryOp.SIGN, shared), magnitude);
}
private static Field sineCentered(Field value) {
double lower = Math.max(-SINE_DOMAIN_LIMIT, Math.floor(clampFinite(value.range().min(), -1.0) / SINE_KNOT_SPACING) * SINE_KNOT_SPACING);
double upper = Math.min(SINE_DOMAIN_LIMIT, Math.ceil(clampFinite(value.range().max(), 2.0) / SINE_KNOT_SPACING) * SINE_KNOT_SPACING);
int count = (int) Math.round((upper - lower) / SINE_KNOT_SPACING) + 1;
List<Field.Knot> knots = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
double location = lower + index * SINE_KNOT_SPACING;
knots.add(new Field.Knot(location, new Field.Const(Math.sin(location * Math.PI)), Math.PI * Math.cos(location * Math.PI)));
}
return new Field.Curve(value, knots);
}
private static double clampFinite(double value, double fallback) {
return Double.isFinite(value) ? value : fallback;
}
private static List<Field.Knot> signed(List<Field.Knot> knots) {
List<Field.Knot> signed = new ArrayList<>(knots.size());
for (Field.Knot knot : knots) {
double value = ((Field.Const) knot.value()).value();
signed.add(new Field.Knot(knot.location(), new Field.Const(2.0 * value - 1.0), 2.0 * knot.derivative()));
}
return List.copyOf(signed);
}
private static double bounding(int octaves) {
double amplitude = 0.5;
double total = 1.0;
for (int octave = 1; octave < octaves; octave++) {
total += amplitude;
amplitude *= 0.5;
}
return 1.0 / total;
}
private static Field displacement(Field fracture, double scale) {
return binary(Field.BinaryOp.ADD, scaled(fracture, scale), new Field.Const(-0.5 * scale));
}
private static Field shift(double offset, Field warp, double xzScale) {
if (warp == null) {
return offset == 0.0 ? null : new Field.Const(offset * xzScale);
}
Field scaledWarp = scaled(warp, xzScale);
return offset == 0.0 ? scaledWarp : binary(Field.BinaryOp.ADD, scaledWarp, new Field.Const(offset * xzScale));
}
private static Field scaled(Field field, double factor) {
if (factor == 1.0) {
return field;
}
return binary(Field.BinaryOp.MUL, new Field.Const(factor), field);
}
private static Field sum(Field total, Field addition) {
return total == null ? addition : binary(Field.BinaryOp.ADD, total, addition);
}
private static Field binary(Field.BinaryOp op, Field left, Field right) {
return new Field.Binary(op, left, right);
}
private static boolean fitted(CNG cng) {
NoiseType type = NoiseLeaves.typeOf(cng.getGenerator());
return type != NoiseType.FLAT && (warped(cng) || NoiseLeaves.fold(type).kind() != NoiseLeaves.FoldKind.NONE);
}
private static boolean warped(CNG cng) {
return cng.getFracture() != null && !cng.isNoscale();
}
private static boolean cellular(IrisGeneratorStyle style, CNG root) {
for (IrisGeneratorStyle current = style; current != null; current = current.getFracture()) {
if (current.getCellularFrequency() > 0.0) {
return true;
}
}
for (CNG current = root; current != null; current = current.getFracture()) {
if (NoiseLeaves.isCellular(NoiseLeaves.typeOf(current.getGenerator()))) {
return true;
}
}
return false;
}
private static void requireSupported(CNG cng) {
if (cng.getChildren() != null && !cng.getChildren().isEmpty()) {
throw new IllegalArgumentException("Noise graphs with injected children are not exportable");
}
if (cng.getCustomGenerator() != null) {
throw new IllegalArgumentException("Noise graphs with custom generators are not exportable");
}
}
private static CNG build(IrisGeneratorStyle style, int octaveMultiplier) {
return style.createForLayer(new RNG(BUILD_SEED), null, octaveMultiplier);
}
private static IrisGeneratorStyle prepared(IrisGeneratorStyle style, double zoom) {
if (style.getExpression() != null) {
throw new IllegalArgumentException("Noise style expression '" + style.getExpression() + "' is not exportable");
}
IrisGeneratorStyle copy = new IrisGeneratorStyle(style.getStyle());
copy.setCellularFrequency(style.getCellularFrequency());
copy.setCellularZoom(style.getCellularZoom());
copy.setZoom(style.getZoom() * zoom);
copy.setMultiplier(style.getMultiplier());
copy.setExponent(style.getExponent());
copy.setCacheSize(0);
copy.setFracture(style.getFracture() == null ? null : prepared(style.getFracture(), 1.0));
return copy;
}
private static String styleName(IrisGeneratorStyle style) {
return style.getStyle() == null ? "flat" : style.getStyle().name().toLowerCase(Locale.ROOT);
}
private static double positive(double value, String role) {
if (!Double.isFinite(value) || value <= 0.0) {
throw new IllegalArgumentException(role + " must be finite and positive, got " + value);
}
return value;
}
public record Warp(Field x, Field z) {
public Warp {
Objects.requireNonNull(x, "x");
Objects.requireNonNull(z, "z");
}
}
private record LeafKey(NoiseType type, int octaves, DataVersion target) {
}
private record LevelKey(LevelSpec spec, DataVersion target) {
}
private record LevelSpec(NoiseType type, int octaves, double scale, double opacity, double power, double offset, double patch,
double fractureScale, LevelSpec fracture) {
private static LevelSpec of(CNG cng) {
NoiseType type = NoiseLeaves.typeOf(cng.getGenerator());
boolean warped = warped(cng);
return new LevelSpec(type, NoiseLeaves.effectiveOctaves(type, cng.getOct()), cng.getEffectiveScale(), cng.getOpacity(),
cng.getPower(), cng.getUp() - cng.getDown(), cng.getPatch(), warped ? cng.getFscale() : 0.0,
warped ? of(cng.getFracture()) : null);
}
private LevelSpec canonical() {
double factor = type == NoiseType.FLAT || scale == 0.0 ? 1.0 : type.getCoordinateScale() / scale;
return new LevelSpec(type, octaves, rounded(scale * factor), 1.0, 1.0, 0.0, 1.0, rounded(fractureScale / factor),
fracture == null ? null : fracture.rescaled(factor));
}
private LevelSpec rescaled(double factor) {
return new LevelSpec(type, octaves, rounded(scale * factor), opacity, power, offset, patch, rounded(fractureScale / factor),
fracture == null ? null : fracture.rescaled(factor));
}
private CNG build(long seed) {
CNG cng = new CNG(new RNG(seed), type, opacity, octaves);
cng.scale(scale).bake();
cng.pow(power);
cng.up(offset);
cng.patch(patch);
if (fracture != null) {
cng.fractureWith(fracture.build(seed + 1L), fractureScale);
}
return cng;
}
private static double rounded(double value) {
return (float) value;
}
}
private record Domain(double zoom, double offsetX, double offsetZ, Field warpX, Field warpZ) {
private static final Domain IDENTITY = new Domain(1.0, 0.0, 0.0, null, null);
private Domain layer(double layerZoom, double layerOffsetX, double layerOffsetZ, Field layerWarpX, Field layerWarpZ) {
double combined = zoom * layerZoom;
return new Domain(combined, offsetX + combined * layerOffsetX, offsetZ + combined * layerOffsetZ,
sum(warpX, layerWarpX == null ? null : scaled(layerWarpX, zoom)),
sum(warpZ, layerWarpZ == null ? null : scaled(layerWarpZ, zoom)));
}
private Domain swapped() {
return new Domain(zoom, offsetZ, offsetX, warpZ, warpX);
}
}
private record Displacement(double offsetX, double offsetZ, Field warpX, Field warpZ) {
private static final Displacement NONE = new Displacement(0.0, 0.0, null, null);
private Displacement swapped() {
return new Displacement(offsetZ, offsetX, warpZ, warpX);
}
private Displacement warped(Field addedX, Field addedZ) {
return new Displacement(offsetX, offsetZ, sum(warpX, addedX), sum(warpZ, addedZ));
}
private Displacement shared(Emit emit, String path) {
return new Displacement(offsetX, offsetZ, emit.shared(path + "/dx", warpX), emit.shared(path + "/dz", warpZ));
}
}
private record Emit(NoiseRegistry registry, ExportCapabilities capabilities, long seedSalt) {
private Emit {
Objects.requireNonNull(registry, "registry");
Objects.requireNonNull(capabilities, "capabilities");
}
private String noise(LeafNoiseFit.FittedLeaf leaf, String path) {
NoiseDef template = leaf.noise();
String id = NOISE_PREFIX + template.id() + "/" + HexFormat.of().toHexDigits(salt(path));
return registry.register(new NoiseDef(id, template.firstOctave(), template.amplitudes()));
}
private Field shared(String path, Field expr) {
if (expr == null || expr.cost() == CostClass.CHEAP) {
return expr;
}
return new Field.Shared(SHARED_PREFIX + HexFormat.of().toHexDigits(mix(salt(path) ^ digest(expr.toString()))), expr);
}
private static long digest(String text) {
long hash = 0xCBF29CE484222325L;
for (int index = 0; index < text.length(); index++) {
hash = (hash ^ text.charAt(index)) * 0x100000001B3L;
}
return hash;
}
private long salt(String path) {
long hash = mix(seedSalt);
for (int index = 0; index < path.length(); index++) {
hash = mix(hash ^ path.charAt(index));
}
return hash;
}
private static long mix(long value) {
long mixed = value + 0x9E3779B97F4A7C15L;
mixed = (mixed ^ mixed >>> 30) * 0xBF58476D1CE4E5B9L;
mixed = (mixed ^ mixed >>> 27) * 0x94D049BB133111EBL;
return mixed ^ mixed >>> 31;
}
}
}
@@ -0,0 +1,194 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import java.util.Arrays;
import java.util.SplittableRandom;
import java.util.TreeSet;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoubleUnaryOperator;
final class RankCorrelation {
private static final double HALF_CORRELATION = 0.5;
private static final double MINIMUM_CORRELATION = 0.03;
private static final double MINIMUM_STRUCTURE = 1.0e-4;
private static final double STRUCTURE_FLOOR = 1.0e-6;
private static final double RANK_VARIOGRAM_SCALE = 6.0;
private RankCorrelation() {
}
static double[] profile(DoubleBinaryOperator field, double[] lags, int basePoints, double span, boolean integral, long seed) {
int stride = 1 + 2 * lags.length;
double[] values = new double[basePoints * stride];
SplittableRandom random = new SplittableRandom(seed);
for (int point = 0; point < basePoints; point++) {
double x = coordinate(random, span, integral);
double z = coordinate(random, span, integral);
int offset = point * stride;
values[offset] = field.applyAsDouble(x, z);
for (int lag = 0; lag < lags.length; lag++) {
values[offset + 1 + 2 * lag] = field.applyAsDouble(x + lags[lag], z);
values[offset + 2 + 2 * lag] = field.applyAsDouble(x, z + lags[lag]);
}
}
double[] ranks = ranks(values);
double[] correlations = new double[lags.length];
for (int lag = 0; lag < lags.length; lag++) {
double sum = 0.0;
for (int point = 0; point < basePoints; point++) {
int offset = point * stride;
double base = ranks[offset];
double alongX = base - ranks[offset + 1 + 2 * lag];
double alongZ = base - ranks[offset + 2 + 2 * lag];
sum += alongX * alongX + alongZ * alongZ;
}
correlations[lag] = 1.0 - RANK_VARIOGRAM_SCALE * sum / (2.0 * basePoints);
}
return correlations;
}
static double[] geometricLags(double minimum, double maximum, int count, boolean integral) {
double ratio = Math.pow(maximum / minimum, 1.0 / (count - 1));
TreeSet<Double> lags = new TreeSet<>();
double lag = minimum;
for (int index = 0; index < count; index++) {
lags.add(integral ? Math.max(1.0, Math.rint(lag)) : lag);
lag *= ratio;
}
double[] result = new double[lags.size()];
int index = 0;
for (double value : lags) {
result[index++] = value;
}
return result;
}
static double halfCorrelationLag(double[] lags, double[] correlations) {
for (int index = 0; index < lags.length; index++) {
if (correlations[index] < HALF_CORRELATION) {
if (index == 0) {
return lags[0];
}
double before = correlations[index - 1];
double fraction = (before - HALF_CORRELATION) / (before - correlations[index]);
return Math.exp(Math.log(lags[index - 1]) + fraction * (Math.log(lags[index]) - Math.log(lags[index - 1])));
}
}
return lags[lags.length - 1];
}
static double structure(double[] lags, double[] correlations, double lag) {
if (lag <= lags[0]) {
double first = structure(correlations[0]);
return Math.exp(Math.log(first) + 2.0 * (Math.log(lag) - Math.log(lags[0])));
}
if (lag >= lags[lags.length - 1]) {
return structure(correlations[correlations.length - 1]);
}
int upper = Arrays.binarySearch(lags, lag);
if (upper >= 0) {
return structure(correlations[upper]);
}
upper = -upper - 1;
int lower = upper - 1;
double fraction = (Math.log(lag) - Math.log(lags[lower])) / (Math.log(lags[upper]) - Math.log(lags[lower]));
double lowerLog = Math.log(structure(correlations[lower]));
double upperLog = Math.log(structure(correlations[upper]));
return Math.exp(lowerLog + fraction * (upperLog - lowerLog));
}
static int[] window(double[] correlations) {
int count = 0;
int[] window = new int[correlations.length];
for (int index = 0; index < correlations.length; index++) {
if (correlations[index] >= MINIMUM_CORRELATION && 1.0 - correlations[index] >= MINIMUM_STRUCTURE) {
window[count++] = index;
}
}
if (count == 0) {
for (int index = 0; index < correlations.length; index++) {
window[count++] = index;
}
}
return Arrays.copyOf(window, count);
}
static double structureError(double expectedStructure, double actualStructure) {
double difference = Math.log(Math.max(expectedStructure, STRUCTURE_FLOOR)) - Math.log(Math.max(actualStructure, STRUCTURE_FLOOR));
return difference * difference;
}
static double structure(double correlation) {
return Math.max(STRUCTURE_FLOOR, 1.0 - correlation);
}
static double goldenMinimum(DoubleUnaryOperator objective, double lower, double upper, int iterations) {
double ratio = (Math.sqrt(5.0) - 1.0) / 2.0;
double left = upper - ratio * (upper - lower);
double right = lower + ratio * (upper - lower);
double leftValue = objective.applyAsDouble(left);
double rightValue = objective.applyAsDouble(right);
for (int iteration = 0; iteration < iterations; iteration++) {
if (leftValue <= rightValue) {
upper = right;
right = left;
rightValue = leftValue;
left = upper - ratio * (upper - lower);
leftValue = objective.applyAsDouble(left);
} else {
lower = left;
left = right;
leftValue = rightValue;
right = lower + ratio * (upper - lower);
rightValue = objective.applyAsDouble(right);
}
}
return leftValue <= rightValue ? left : right;
}
private static double coordinate(SplittableRandom random, double span, boolean integral) {
double value = (random.nextDouble() * 2.0 - 1.0) * span;
return integral ? Math.floor(value) : value;
}
private static double[] ranks(double[] values) {
double[] sorted = values.clone();
Arrays.sort(sorted);
double[] ranks = new double[values.length];
double count = values.length;
for (int index = 0; index < values.length; index++) {
double value = values[index];
int first = firstIndex(sorted, value);
int last = lastIndex(sorted, value);
ranks[index] = (first + last + 1) / (2.0 * count);
}
return ranks;
}
private static int firstIndex(double[] sorted, double value) {
int low = 0;
int high = sorted.length;
while (low < high) {
int middle = (low + high) >>> 1;
if (sorted[middle] < value) {
low = middle + 1;
} else {
high = middle;
}
}
return low;
}
private static int lastIndex(double[] sorted, double value) {
int low = 0;
int high = sorted.length;
while (low < high) {
int middle = (low + high) >>> 1;
if (sorted[middle] <= value) {
low = middle + 1;
} else {
high = middle;
}
}
return low - 1;
}
}
@@ -0,0 +1,13 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
public interface StyleCompiler {
Field compile(IrisGeneratorStyle style, long seedSalt, NoiseRegistry registry, ExportCapabilities capabilities);
default StyleCompiler selectors() {
return this;
}
}
@@ -0,0 +1,86 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.export.ir.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public final class ValueDistributionFit {
private static final double[] LEVELS = {
0.0, 0.0005, 0.002, 0.005, 0.01, 0.02, 0.035, 0.05, 0.075, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45,
0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.925, 0.95, 0.965, 0.98, 0.99, 0.995, 0.998, 0.9995, 1.0
};
private static final int MINIMUM_SAMPLES = 64;
private ValueDistributionFit() {
}
public static List<Field.Knot> fit(double[] source, double[] target) {
double[] sortedSource = sorted(Objects.requireNonNull(source, "source"), "source");
double[] sortedTarget = sorted(Objects.requireNonNull(target, "target"), "target");
List<Double> locations = new ArrayList<>(LEVELS.length);
List<Double> values = new ArrayList<>(LEVELS.length);
for (double level : LEVELS) {
double location = (float) quantile(sortedSource, level);
double value = (float) quantile(sortedTarget, level);
if (!locations.isEmpty() && location <= locations.getLast()) {
continue;
}
locations.add(location);
values.add(value);
}
if (locations.size() < 2) {
throw new IllegalArgumentException("Value distribution fit needs a source with spread, got a constant source of " + sortedSource[0]);
}
double[] derivatives = monotoneDerivatives(locations, values);
List<Field.Knot> knots = new ArrayList<>(locations.size());
for (int index = 0; index < locations.size(); index++) {
knots.add(new Field.Knot(locations.get(index), new Field.Const(values.get(index)), derivatives[index]));
}
return List.copyOf(knots);
}
public static double quantile(double[] sorted, double level) {
if (level <= 0.0) {
return sorted[0];
}
if (level >= 1.0) {
return sorted[sorted.length - 1];
}
double position = level * (sorted.length - 1);
int lower = (int) Math.floor(position);
double fraction = position - lower;
return lower + 1 >= sorted.length ? sorted[lower] : sorted[lower] + fraction * (sorted[lower + 1] - sorted[lower]);
}
private static double[] sorted(double[] samples, String role) {
if (samples.length < MINIMUM_SAMPLES) {
throw new IllegalArgumentException("Value distribution fit needs at least " + MINIMUM_SAMPLES + " " + role + " samples, got " + samples.length);
}
double[] copy = samples.clone();
for (double sample : copy) {
if (!Double.isFinite(sample)) {
throw new IllegalArgumentException("Value distribution fit " + role + " samples must be finite, got " + sample);
}
}
Arrays.sort(copy);
return copy;
}
private static double[] monotoneDerivatives(List<Double> locations, List<Double> values) {
int count = locations.size();
double[] secants = new double[count - 1];
for (int index = 0; index < secants.length; index++) {
secants[index] = (values.get(index + 1) - values.get(index)) / (locations.get(index + 1) - locations.get(index));
}
double[] derivatives = new double[count];
for (int index = 1; index < count - 1; index++) {
double before = secants[index - 1];
double after = secants[index];
derivatives[index] = before <= 0.0 || after <= 0.0 ? 0.0 : (float) (2.0 / (1.0 / before + 1.0 / after));
}
return derivatives;
}
}
@@ -0,0 +1,376 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.WorldgenExportTargets;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class VanillaNoiseModel {
private static final double SECOND_SAMPLER_INPUT_FACTOR = 1.0181268882175227D;
private static final double TARGET_DEVIATION = 0.3333333333333333D;
private static final double OCTAVE_DEVIATION = 0.2702247831245211D;
private static final double MIN_BASE_AMPLITUDE = 9.999999747378752E-6D;
private static final double MAX_AMPLITUDE = 1000000.0D;
private static final int MAX_OCTAVE = 32;
private static final int MAX_OCTAVE_COUNT = 32;
private static final String DEFAULT_NAMESPACE = "minecraft";
private static final String OCTAVE_SEED_PREFIX = "octave_";
private final SinglePrecisionField field;
private VanillaNoiseModel(SinglePrecisionField field) {
this.field = field;
}
public static VanillaNoiseModel create(String noiseId, int firstOctave, List<Double> amplitudes, long worldSeed,
DataVersion target) {
Objects.requireNonNull(target, "target");
if (!WorldgenExportTargets.SUPPORTED.contains(target)) {
throw WorldgenExportTargets.unsupported(target.getVersion());
}
return create(noiseId, OctaveProfile.fromAmplitudes(firstOctave, amplitudes), worldSeed);
}
public static VanillaNoiseModel create(String noiseId, OctaveProfile profile, long worldSeed) {
Objects.requireNonNull(profile, "profile");
return new VanillaNoiseModel(SinglePrecisionField.create(noiseSource(noiseId, worldSeed), profile));
}
public double sample(double x, double y, double z) {
return field.sample(x, y, z);
}
private static XoroshiroSource noiseSource(String noiseId, long worldSeed) {
String identifier = canonicalIdentifier(Objects.requireNonNull(noiseId, "noiseId"));
return XoroshiroSource.fromWorldSeed(worldSeed).forkPositional().fromHashOf(identifier);
}
private static String canonicalIdentifier(String noiseId) {
return noiseId.indexOf(':') >= 0 ? noiseId : DEFAULT_NAMESPACE + ":" + noiseId;
}
private static double spanDeviation(double[] amplitudes) {
int lowest = Integer.MAX_VALUE;
int highest = Integer.MIN_VALUE;
for (int index = 0; index < amplitudes.length; index++) {
if (amplitudes[index] != 0.0D) {
lowest = Math.min(lowest, index);
highest = Math.max(highest, index);
}
}
return 0.1D * (1.0D + 1.0D / (highest - lowest + 1));
}
public enum Normalization {
ENABLED,
DISABLED,
LEGACY
}
public record OctaveProfile(int baseOctave, int octaveCount, List<Double> amplitudeModifiers, double baseAmplitude,
Normalization normalization) {
public OctaveProfile {
Objects.requireNonNull(normalization, "normalization");
amplitudeModifiers = List.copyOf(Objects.requireNonNull(amplitudeModifiers, "amplitudeModifiers"));
if (baseOctave < -MAX_OCTAVE || baseOctave > MAX_OCTAVE) {
throw new IllegalArgumentException("Noise base octave " + baseOctave + " is outside [-" + MAX_OCTAVE + ", "
+ MAX_OCTAVE + "]");
}
if (octaveCount < 1 || octaveCount > MAX_OCTAVE_COUNT) {
throw new IllegalArgumentException("Noise octave count " + octaveCount + " is outside [1, " + MAX_OCTAVE_COUNT
+ "]");
}
if (!amplitudeModifiers.isEmpty() && amplitudeModifiers.size() != octaveCount) {
throw new IllegalArgumentException("Noise amplitude modifiers had size " + amplitudeModifiers.size()
+ ", but octave count was " + octaveCount);
}
for (Double modifier : amplitudeModifiers) {
if (!(modifier >= 0.0D && modifier <= MAX_AMPLITUDE)) {
throw new IllegalArgumentException("Noise amplitude modifier " + modifier + " is outside [0, "
+ MAX_AMPLITUDE + "]");
}
}
if (!(baseAmplitude >= MIN_BASE_AMPLITUDE && baseAmplitude <= MAX_AMPLITUDE)) {
throw new IllegalArgumentException("Noise base amplitude " + baseAmplitude + " is outside ["
+ MIN_BASE_AMPLITUDE + ", " + MAX_AMPLITUDE + "]");
}
}
public static OctaveProfile fromAmplitudes(int firstOctave, List<Double> amplitudes) {
Objects.requireNonNull(amplitudes, "amplitudes");
return new OctaveProfile(firstOctave, amplitudes.size(), amplitudes, 1.0D, Normalization.DISABLED);
}
private double modifier(int octave) {
return amplitudeModifiers.isEmpty() ? 1.0D : amplitudeModifiers.get(octave);
}
}
private static final class SinglePrecisionField {
private final GradientLattice[] lattices;
private final double[] frequencies;
private final float[] amplitudes;
private SinglePrecisionField(GradientLattice[] lattices, double[] frequencies, float[] amplitudes) {
this.lattices = lattices;
this.frequencies = frequencies;
this.amplitudes = amplitudes;
}
private static SinglePrecisionField create(XoroshiroSource source, OctaveProfile profile) {
List<ActiveOctave> octaves = activeOctaves(profile);
double normalization = normalizationFactor(profile, octaves);
HashedSeedFactory firstSeeds = source.forkPositional();
HashedSeedFactory secondSeeds = source.forkPositional();
int layers = octaves.size() * 2;
GradientLattice[] lattices = new GradientLattice[layers];
double[] frequencies = new double[layers];
float[] amplitudes = new float[layers];
for (int index = 0; index < octaves.size(); index++) {
ActiveOctave octave = octaves.get(index);
String seed = OCTAVE_SEED_PREFIX + octave.octave();
float layerAmplitude = (float) (normalization * octave.amplitude());
lattices[index * 2] = new GradientLattice(firstSeeds.fromHashOf(seed));
lattices[index * 2 + 1] = new GradientLattice(secondSeeds.fromHashOf(seed));
frequencies[index * 2] = octave.frequency();
frequencies[index * 2 + 1] = octave.frequency() * SECOND_SAMPLER_INPUT_FACTOR;
amplitudes[index * 2] = layerAmplitude;
amplitudes[index * 2 + 1] = layerAmplitude;
}
return new SinglePrecisionField(lattices, frequencies, amplitudes);
}
private double sample(double x, double y, double z) {
float value = 0.0F;
for (int index = 0; index < lattices.length; index++) {
double frequency = frequencies[index];
value += amplitudes[index] * lattices[index].sampleFloat(x * frequency, y * frequency, z * frequency);
}
return value;
}
private static List<ActiveOctave> activeOctaves(OctaveProfile profile) {
int count = profile.octaveCount();
List<ActiveOctave> octaves = new ArrayList<>(count);
double frequency = Math.pow(2.0D, profile.baseOctave());
double amplitude = profile.baseAmplitude();
if (profile.normalization() != Normalization.DISABLED) {
amplitude *= Math.pow(0.5D, -(count - 1)) / (Math.pow(0.5D, -count) - 1.0D);
}
for (int index = 0; index < count; index++) {
double modifier = profile.modifier(index);
if (modifier != 0.0D) {
octaves.add(new ActiveOctave(profile.baseOctave() + index, frequency, amplitude * modifier));
}
frequency *= 2.0D;
amplitude *= 0.5D;
}
return octaves;
}
private static double normalizationFactor(OctaveProfile profile, List<ActiveOctave> octaves) {
double absoluteSum = octaves.stream().mapToDouble(octave -> Math.abs(octave.amplitude())).sum();
double squaredDeviation = 0.0D;
for (ActiveOctave octave : octaves) {
double deviation = OCTAVE_DEVIATION * Math.abs(octave.amplitude());
squaredDeviation += deviation * deviation;
}
double deviation = Math.sqrt(squaredDeviation);
if (deviation == 0.0D) {
return 0.0D;
}
double factor = absoluteSum * TARGET_DEVIATION / (deviation * Math.sqrt(2.0D));
if (profile.normalization() != Normalization.LEGACY) {
return factor;
}
double[] modifiers = new double[profile.octaveCount()];
for (int index = 0; index < modifiers.length; index++) {
modifiers[index] = profile.modifier(index);
}
return profile.baseAmplitude() * 0.5D * TARGET_DEVIATION / spanDeviation(modifiers);
}
private record ActiveOctave(int octave, double frequency, double amplitude) {
}
}
private static final class GradientLattice {
private static final double PERIOD = 33554432.0D;
private static final double HALF_PERIOD = Math.nextDown(16777216.0D);
private static final int[][] GRADIENTS = {
{1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, {-1, -1, 0},
{1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1},
{0, 1, 1}, {0, -1, 1}, {0, 1, -1}, {0, -1, -1},
{1, 1, 0}, {0, -1, 1}, {-1, 1, 0}, {0, -1, -1}
};
private final byte[] permutation;
private final double offsetX;
private final double offsetY;
private final double offsetZ;
private GradientLattice(XoroshiroSource source) {
offsetX = source.nextDouble() * 256.0D;
offsetY = source.nextDouble() * 256.0D;
offsetZ = source.nextDouble() * 256.0D;
permutation = new byte[256];
for (int index = 0; index < 256; index++) {
permutation[index] = (byte) index;
}
for (int index = 0; index < 256; index++) {
int offset = source.nextInt(256 - index);
byte swap = permutation[index];
permutation[index] = permutation[index + offset];
permutation[index + offset] = swap;
}
}
private float sampleFloat(double x, double y, double z) {
double shiftedX = wrap(x) + offsetX;
double shiftedY = wrap(y) + offsetY;
double shiftedZ = wrap(z) + offsetZ;
int cellX = (int) Math.floor(shiftedX);
int cellY = (int) Math.floor(shiftedY);
int cellZ = (int) Math.floor(shiftedZ);
float localX = (float) (shiftedX - cellX);
float localY = (float) (shiftedY - cellY);
float localZ = (float) (shiftedZ - cellZ);
int low = hash(cellX);
int high = hash(cellX + 1);
int lowLow = hash(low + cellY);
int lowHigh = hash(low + cellY + 1);
int highLow = hash(high + cellY);
int highHigh = hash(high + cellY + 1);
float corner000 = dot(hash(lowLow + cellZ), localX, localY, localZ);
float corner100 = dot(hash(highLow + cellZ), localX - 1.0F, localY, localZ);
float corner010 = dot(hash(lowHigh + cellZ), localX, localY - 1.0F, localZ);
float corner110 = dot(hash(highHigh + cellZ), localX - 1.0F, localY - 1.0F, localZ);
float corner001 = dot(hash(lowLow + cellZ + 1), localX, localY, localZ - 1.0F);
float corner101 = dot(hash(highLow + cellZ + 1), localX - 1.0F, localY, localZ - 1.0F);
float corner011 = dot(hash(lowHigh + cellZ + 1), localX, localY - 1.0F, localZ - 1.0F);
float corner111 = dot(hash(highHigh + cellZ + 1), localX - 1.0F, localY - 1.0F, localZ - 1.0F);
float fadeX = fade(localX);
float fadeY = fade(localY);
float fadeZ = fade(localZ);
float bottom = lerp(fadeY, lerp(fadeX, corner000, corner100), lerp(fadeX, corner010, corner110));
float top = lerp(fadeY, lerp(fadeX, corner001, corner101), lerp(fadeX, corner011, corner111));
return lerp(fadeZ, bottom, top);
}
private int hash(int value) {
return permutation[value & 255] & 255;
}
private static double wrap(double coordinate) {
if (coordinate >= -HALF_PERIOD && coordinate < HALF_PERIOD) {
return coordinate;
}
return coordinate - Math.floor(coordinate / PERIOD + 0.5D) * PERIOD;
}
private static float dot(int hash, float x, float y, float z) {
int[] gradient = GRADIENTS[hash & 15];
return gradient[0] * x + gradient[1] * y + gradient[2] * z;
}
private static float fade(float value) {
return value * value * value * (value * (value * 6.0F - 15.0F) + 10.0F);
}
private static float lerp(float alpha, float start, float end) {
return start + alpha * (end - start);
}
}
private record HashedSeedFactory(long seedLow, long seedHigh) {
private XoroshiroSource fromHashOf(String name) {
byte[] digest = md5(name);
long low = 0L;
long high = 0L;
for (int index = 0; index < 8; index++) {
low = low << 8 | digest[index] & 0xFFL;
high = high << 8 | digest[index + 8] & 0xFFL;
}
return new XoroshiroSource(low ^ seedLow, high ^ seedHigh);
}
private static byte[] md5(String name) {
try {
return MessageDigest.getInstance("MD5").digest(name.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException("MD5 digest is unavailable for noise seed '" + name + "'", failure);
}
}
}
private static final class XoroshiroSource {
private static final long SILVER_RATIO = 7640891576956012809L;
private static final long GOLDEN_RATIO = -7046029254386353131L;
private static final double DOUBLE_UNIT = 1.1102230246251565E-16D;
private long low;
private long high;
private XoroshiroSource(long low, long high) {
if ((low | high) == 0L) {
this.low = GOLDEN_RATIO;
this.high = SILVER_RATIO;
return;
}
this.low = low;
this.high = high;
}
private static XoroshiroSource fromWorldSeed(long seed) {
long unmixedLow = seed ^ SILVER_RATIO;
long unmixedHigh = unmixedLow + GOLDEN_RATIO;
return new XoroshiroSource(mix(unmixedLow), mix(unmixedHigh));
}
private HashedSeedFactory forkPositional() {
long seedLow = nextLong();
long seedHigh = nextLong();
return new HashedSeedFactory(seedLow, seedHigh);
}
private long nextLong() {
long currentLow = low;
long currentHigh = high;
long result = Long.rotateLeft(currentLow + currentHigh, 17) + currentLow;
currentHigh ^= currentLow;
low = Long.rotateLeft(currentLow, 49) ^ currentHigh ^ currentHigh << 21;
high = Long.rotateLeft(currentHigh, 28);
return result;
}
private int nextInt(int bound) {
long random = Integer.toUnsignedLong((int) nextLong());
long product = random * bound;
long fraction = product & 0xFFFFFFFFL;
if (fraction < bound) {
int threshold = Integer.remainderUnsigned(~bound + 1, bound);
while (fraction < threshold) {
random = Integer.toUnsignedLong((int) nextLong());
product = random * bound;
fraction = product & 0xFFFFFFFFL;
}
}
return (int) (product >> 32);
}
private double nextDouble() {
return (nextLong() >>> 11) * DOUBLE_UNIT;
}
private static long mix(long value) {
long mixed = (value ^ value >>> 30) * -4658895280553007687L;
mixed = (mixed ^ mixed >>> 27) * -7723592293110705685L;
return mixed ^ mixed >>> 31;
}
}
}
@@ -0,0 +1,99 @@
package art.arcane.iris.pack.datapack.export.compile.noise;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.ir.Field;
import art.arcane.iris.pack.datapack.export.ir.FieldEvaluator;
import art.arcane.iris.pack.datapack.export.ir.NoiseDef;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.SplittableRandom;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoubleFunction;
public final class WarpFit {
private static final int BASE_POINTS = 2048;
private static final int INSTANCES = 16;
private static final long INSTANCE_STRIDE = 0x9E37_79B9L;
private static final double SPAN = 1 << 18;
private static final double[] SURVEY_LAGS = RankCorrelation.geometricLags(1.0, 65_536.0, 48, true);
private static final double FIT_LAG_MINIMUM = 0.25;
private static final double FIT_LAG_MAXIMUM = 4.0;
private static final int FIT_LAG_COUNT = 16;
private static final double SEARCH_RATIO = 2.0;
private static final int SEARCH_ITERATIONS = 18;
private static final int MARGINAL_SAMPLES = 65_536;
private WarpFit() {
}
public static LevelFit fit(DoubleBinaryOperator iris, DoubleFunction<Field> lowered, NoiseRegistry scratch, DataVersion target) {
Objects.requireNonNull(iris, "iris");
Objects.requireNonNull(lowered, "lowered");
Objects.requireNonNull(scratch, "scratch");
Objects.requireNonNull(target, "target");
double[] survey = RankCorrelation.profile(iris, SURVEY_LAGS, BASE_POINTS, SPAN, true, LeafNoiseFit.FIT_SEED + 11L);
double half = RankCorrelation.halfCorrelationLag(SURVEY_LAGS, survey);
double[] lags = RankCorrelation.geometricLags(Math.max(1.0, half * FIT_LAG_MINIMUM), Math.max(2.0, half * FIT_LAG_MAXIMUM), FIT_LAG_COUNT, true);
double[] irisProfile = RankCorrelation.profile(iris, lags, BASE_POINTS, SPAN, true, LeafNoiseFit.FIT_SEED + 13L);
int[] window = RankCorrelation.window(irisProfile);
double logMultiplier = RankCorrelation.goldenMinimum(
candidate -> error(lowered.apply(Math.exp(candidate)), scratch, target, lags, irisProfile, window),
-Math.log(SEARCH_RATIO), Math.log(SEARCH_RATIO), SEARCH_ITERATIONS);
double multiplier = Math.exp(logMultiplier);
Field fitted = lowered.apply(multiplier);
double[] loweredMarginal = new double[0];
for (int instance = 0; instance < INSTANCES; instance++) {
double[] sampled = marginal(sampler(fitted, scratch, target, instance), MARGINAL_SAMPLES / INSTANCES, instance);
double[] joined = Arrays.copyOf(loweredMarginal, loweredMarginal.length + sampled.length);
System.arraycopy(sampled, 0, joined, loweredMarginal.length, sampled.length);
loweredMarginal = joined;
}
List<Field.Knot> distribution = ValueDistributionFit.fit(loweredMarginal, marginal(iris, MARGINAL_SAMPLES, INSTANCES));
return new LevelFit(multiplier, distribution);
}
private static double error(Field field, NoiseRegistry scratch, DataVersion target, double[] lags, double[] irisProfile, int[] window) {
double[] profile = new double[lags.length];
for (int instance = 0; instance < INSTANCES; instance++) {
double[] sampled = RankCorrelation.profile(sampler(field, scratch, target, instance), lags, BASE_POINTS / INSTANCES, SPAN, true,
LeafNoiseFit.FIT_SEED + 13L + instance);
for (int index = 0; index < profile.length; index++) {
profile[index] += sampled[index] / INSTANCES;
}
}
double sum = 0.0;
for (int index : window) {
sum += RankCorrelation.structureError(RankCorrelation.structure(irisProfile[index]), RankCorrelation.structure(profile[index]));
}
return sum / window.length;
}
private static DoubleBinaryOperator sampler(Field field, NoiseRegistry scratch, DataVersion target, int instance) {
Map<String, NoiseDef> noises = scratch.byReference();
long worldSeed = LeafNoiseFit.FIT_SEED + instance * INSTANCE_STRIDE;
return (x, z) -> FieldEvaluator.sample(field, noises, worldSeed, target, (int) x, 0, (int) z);
}
private static double[] marginal(DoubleBinaryOperator field, int count, int stream) {
SplittableRandom random = new SplittableRandom(LeafNoiseFit.FIT_SEED + 17L + stream);
double[] samples = new double[count];
for (int index = 0; index < samples.length; index++) {
double x = Math.floor((random.nextDouble() * 2.0 - 1.0) * SPAN);
double z = Math.floor((random.nextDouble() * 2.0 - 1.0) * SPAN);
samples[index] = field.applyAsDouble(x, z);
}
return samples;
}
public record LevelFit(double multiplier, List<Field.Knot> distribution) {
public LevelFit {
if (!(multiplier > 0.0) || !Double.isFinite(multiplier)) {
throw new IllegalArgumentException("Level frequency multiplier must be positive and finite, got " + multiplier);
}
distribution = List.copyOf(distribution);
}
}
}
@@ -0,0 +1,16 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import java.util.Objects;
public record BakedVariant(String id, TemplateVolume volume, int baseY) {
public BakedVariant {
VanillaResourceIdentifier.requireIdentifier(id, "template id");
Objects.requireNonNull(volume, "Template volume");
if (baseY > 0) {
throw new IllegalArgumentException("Template base offset must not be positive, got " + baseY);
}
}
}
@@ -0,0 +1,221 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
final class BlockStateRotation {
private static final int SIGN_STEPS = 16;
private static final int SIGN_QUARTER = 4;
private final Map<Direction, Direction> directions;
private final int yQuarters;
private final boolean flipped;
private BlockStateRotation(int xQuarters, int yQuarters, int zQuarters) {
this.yQuarters = Math.floorMod(yQuarters, 4);
directions = new HashMap<>();
for (Direction direction : Direction.values()) {
int[] vector = {direction.dx, direction.dy, direction.dz};
vector = turn(vector, Math.floorMod(xQuarters, 4), Axis.X);
vector = turn(vector, Math.floorMod(zQuarters, 4), Axis.Z);
vector = turn(vector, this.yQuarters, Axis.Y);
directions.put(direction, Direction.of(vector[0], vector[1], vector[2]));
}
flipped = directions.get(Direction.UP) == Direction.DOWN;
}
static BlockStateRotation of(double xDegrees, double yDegrees, double zDegrees) {
return new BlockStateRotation(quarters(xDegrees), quarters(yDegrees), quarters(zDegrees));
}
boolean identity() {
for (Map.Entry<Direction, Direction> entry : directions.entrySet()) {
if (entry.getKey() != entry.getValue()) {
return false;
}
}
return true;
}
VanillaBlockState apply(VanillaBlockState state) {
if (identity() || state.properties().isEmpty()) {
return state;
}
Map<String, String> source = state.properties();
Map<String, String> rotated = new TreeMap<>();
for (Map.Entry<String, String> property : source.entrySet()) {
Direction connection = Direction.named(property.getKey());
if (connection != null) {
rotated.put(directions.get(connection).serialized, property.getValue());
continue;
}
rotated.put(property.getKey(), value(property.getKey(), property.getValue()));
}
return new VanillaBlockState(state.name(), rotated);
}
private String value(String key, String value) {
return switch (key) {
case "facing", "vertical_direction", "hopper_facing" -> mapDirection(value);
case "axis" -> mapAxis(value);
case "rotation" -> mapSignRotation(value);
case "orientation", "shape" -> mapCompound(value);
case "half", "type" -> flipped ? flipVertical(value) : value;
case "face", "attach_face" -> flipped ? flipFace(value) : value;
default -> value;
};
}
private String mapDirection(String value) {
Direction direction = Direction.named(value);
return direction == null ? value : directions.get(direction).serialized;
}
private String mapAxis(String value) {
Direction direction = switch (value) {
case "x" -> Direction.EAST;
case "y" -> Direction.UP;
case "z" -> Direction.SOUTH;
default -> null;
};
if (direction == null) {
return value;
}
return switch (directions.get(direction)) {
case EAST, WEST -> "x";
case UP, DOWN -> "y";
case NORTH, SOUTH -> "z";
};
}
private String mapSignRotation(String value) {
try {
int rotation = Integer.parseInt(value);
return Integer.toString(Math.floorMod(rotation - yQuarters * SIGN_QUARTER, SIGN_STEPS));
} catch (NumberFormatException exception) {
return value;
}
}
private String mapCompound(String value) {
String[] parts = value.split("_");
if (parts.length == 2 && parts[0].equals("ascending")) {
Direction direction = Direction.named(parts[1]);
return direction == null ? value : "ascending_" + directions.get(direction).serialized;
}
if (parts.length != 2) {
return value;
}
Direction first = Direction.named(parts[0]);
Direction second = Direction.named(parts[1]);
if (first == null || second == null) {
return value;
}
Direction mappedFirst = directions.get(first);
Direction mappedSecond = directions.get(second);
if (!mappedFirst.horizontal() || !mappedSecond.horizontal() || !first.horizontal() || !second.horizontal()) {
return mappedFirst.serialized + "_" + mappedSecond.serialized;
}
if (mappedFirst.northSouth() && mappedSecond.northSouth()) {
return "north_south";
}
if (!mappedFirst.northSouth() && !mappedSecond.northSouth()) {
return "east_west";
}
Direction northSouth = mappedFirst.northSouth() ? mappedFirst : mappedSecond;
Direction eastWest = mappedFirst.northSouth() ? mappedSecond : mappedFirst;
return northSouth.serialized + "_" + eastWest.serialized;
}
private static String flipVertical(String value) {
return switch (value) {
case "top" -> "bottom";
case "bottom" -> "top";
default -> value;
};
}
private static String flipFace(String value) {
return switch (value) {
case "floor" -> "ceiling";
case "ceiling" -> "floor";
default -> value;
};
}
private static int quarters(double degrees) {
return (int) Math.floorMod(Math.round(degrees / 90.0), 4L);
}
private static int[] turn(int[] vector, int quarters, Axis axis) {
int[] result = vector.clone();
for (int index = 0; index < quarters; index++) {
int x = result[0];
int y = result[1];
int z = result[2];
result = switch (axis) {
case X -> new int[]{x, -z, y};
case Y -> new int[]{z, y, -x};
case Z -> new int[]{-y, x, z};
};
}
return result;
}
private enum Axis {
X,
Y,
Z
}
private enum Direction {
NORTH(0, 0, -1),
SOUTH(0, 0, 1),
EAST(1, 0, 0),
WEST(-1, 0, 0),
UP(0, 1, 0),
DOWN(0, -1, 0);
private final int dx;
private final int dy;
private final int dz;
private final String serialized;
Direction(int dx, int dy, int dz) {
this.dx = dx;
this.dy = dy;
this.dz = dz;
serialized = name().toLowerCase(Locale.ROOT);
}
private boolean horizontal() {
return dy == 0;
}
private boolean northSouth() {
return dz != 0;
}
private static Direction named(String value) {
for (Direction direction : values()) {
if (direction.serialized.equals(value)) {
return direction;
}
}
return null;
}
private static Direction of(int dx, int dy, int dz) {
for (Direction direction : values()) {
if (direction.dx == dx && direction.dy == dy && direction.dz == dz) {
return direction;
}
}
throw new IllegalStateException("Quarter turn produced a non-axis direction " + dx + "," + dy + "," + dz);
}
}
}
@@ -0,0 +1,16 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.compile.decoration.GenerationStep;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import java.util.List;
import java.util.Objects;
public record CompiledPlacement(GenerationStep step, List<String> placedFeatures, List<String> structures, List<JsonResource> resources) {
public CompiledPlacement {
Objects.requireNonNull(step, "Placement step");
placedFeatures = List.copyOf(placedFeatures);
structures = List.copyOf(structures);
resources = List.copyOf(resources);
}
}
@@ -0,0 +1,32 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import com.google.gson.JsonElement;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
final class ContentIds {
private static final int HASH_CHARACTERS = 12;
private ContentIds() {
}
static String hash(String content) {
try {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(content.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest).substring(0, HASH_CHARACTERS);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable for export resource ids", exception);
}
}
static String hash(JsonElement json) {
return hash(json.toString());
}
static long seed(String content) {
return Long.parseUnsignedLong(hash(content), 16);
}
}
@@ -0,0 +1,82 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.compile.WorldgenSyntax;
import art.arcane.iris.pack.datapack.export.compile.decoration.Placements;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.ArrayList;
import java.util.List;
final class FeatureJson {
private static final int OFFSET_LIMIT = 16;
private final WorldgenSyntax syntax;
FeatureJson(ExportCapabilities capabilities) {
syntax = WorldgenSyntax.forTarget(capabilities.target());
}
WorldgenSyntax syntax() {
return syntax;
}
List<JsonObject> chanceAndCount(double chance, int count, double share) {
List<JsonObject> modifiers = new ArrayList<>();
if (chance < 1) {
modifiers.add(Placements.randomChance(chance));
}
if (count > 1) {
modifiers.add(Placements.count(new JsonPrimitive(count)));
}
if (share < 1) {
modifiers.add(Placements.randomChance(share));
}
return modifiers;
}
List<JsonObject> offset(int x, int yMin, int yMax, int z) {
List<JsonObject> modifiers = new ArrayList<>();
int remainingX = x;
int remainingZ = z;
int remainingY = yMin;
int spread = yMax - yMin;
while (remainingX != 0 || remainingZ != 0 || remainingY != 0 || spread > 0) {
int stepX = clampStep(remainingX);
int stepZ = clampStep(remainingZ);
int stepY = clampStep(remainingY);
int stepSpread = Math.min(spread, OFFSET_LIMIT - Math.abs(stepY));
JsonObject offset = new JsonObject();
offset.addProperty("type", "minecraft:offset");
offset.addProperty("x", stepX);
offset.add("y", stepSpread > 0 ? Placements.uniform(stepY, stepY + stepSpread) : new JsonPrimitive(stepY));
offset.addProperty("z", stepZ);
modifiers.add(offset);
remainingX -= stepX;
remainingZ -= stepZ;
remainingY -= stepY;
spread -= stepSpread;
}
return modifiers;
}
static JsonArray vector(int x, int y, int z) {
JsonArray vector = new JsonArray(3);
vector.add(x);
vector.add(y);
vector.add(z);
return vector;
}
static JsonObject absolute(int y) {
JsonObject anchor = new JsonObject();
anchor.addProperty("absolute", y);
return anchor;
}
private static int clampStep(int remaining) {
return Math.max(-OFFSET_LIMIT, Math.min(OFFSET_LIMIT, remaining));
}
}
@@ -0,0 +1,416 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.validation.ContentGate;
import art.arcane.iris.structure.object.IObjectLoot;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.world.entity.IrisEnchantment;
import art.arcane.iris.world.loot.InventorySlotType;
import art.arcane.iris.world.loot.IrisLoot;
import art.arcane.iris.world.loot.IrisLootMode;
import art.arcane.iris.world.loot.IrisLootReference;
import art.arcane.iris.world.loot.IrisLootTable;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
public final class LootCompiler {
private static final long WEIGHT_CEILING = 1_000_000L;
private static final String TABLE_FOLDER = "iris/";
private static final String BINDING_FOLDER = "object_loot/";
private static final Set<String> CONTAINERS = Set.of("minecraft:chest", "minecraft:trapped_chest", "minecraft:barrel",
"minecraft:dispenser", "minecraft:dropper", "minecraft:hopper");
private static final Set<String> DAMAGEABLE_SUFFIXES = Set.of("_sword", "_pickaxe", "_axe", "_shovel", "_hoe", "_helmet",
"_chestplate", "_leggings", "_boots", "_spear", "bow", "trident", "shield", "fishing_rod", "shears",
"flint_and_steel", "elytra", "mace", "_on_a_stick", "brush");
private final String namespace;
private final IrisData data;
private final Map<String, JsonObject> tables = new TreeMap<>();
private final Map<String, String> tableIds = new TreeMap<>();
public LootCompiler(String namespace, IrisData data) {
this.namespace = VanillaResourceIdentifier.requireNamespace(namespace, "loot namespace");
this.data = data;
}
public static boolean container(VanillaBlockState state) {
return CONTAINERS.contains(state.name()) || state.name().endsWith("shulker_box");
}
public Optional<String> binding(IrisObjectPlacement placement, String containerBlock, LootScope scope, List<ExportDiagnostic> diagnostics) {
List<JsonObject> pools = new ArrayList<>();
JsonObject objectPool = objectPool(placement, containerBlock, diagnostics);
if (objectPool != null) {
pools.add(objectPool);
}
if (objectPool == null || !placement.isOverrideGlobalLoot()) {
boolean objectDefined = !placement.getLoot().isEmpty() || !placement.getVanillaLoot().isEmpty();
for (IrisLootTable table : environment(scope, objectDefined)) {
JsonObject pool = new JsonObject();
pool.add("entries", single(reference(table, diagnostics)));
pool.addProperty("rolls", 1);
pools.add(pool);
}
}
if (pools.isEmpty()) {
return Optional.empty();
}
JsonObject binding = chestTable(pools);
String id = namespace + ":" + BINDING_FOLDER + ContentIds.hash(binding);
tables.put(id, binding);
return Optional.of(id);
}
public JsonObject table(IrisLootTable table, List<ExportDiagnostic> diagnostics) {
String resource = "loot/" + table.getLoadKey();
List<IrisLoot> storage = new ArrayList<>();
for (IrisLoot loot : table.getLoot()) {
if (loot != null && loot.getSlotTypes() == InventorySlotType.STORAGE) {
storage.add(loot);
}
}
BigInteger common = BigInteger.ONE;
for (IrisLoot loot : storage) {
BigInteger rarity = BigInteger.valueOf(Math.max(1, loot.getRarity()));
common = common.multiply(rarity).divide(common.gcd(rarity));
}
boolean exact = common.compareTo(BigInteger.valueOf(WEIGHT_CEILING)) <= 0;
JsonArray entries = new JsonArray();
for (IrisLoot loot : storage) {
Optional<String> item = item(loot.getTypeKey());
if (item.isEmpty()) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.WARNING, ExportLimit.CUSTOM_ITEM, resource,
"Loot item '" + loot.getTypeKey() + "' is not a Minecraft item and is not exported"));
continue;
}
int rarity = Math.max(1, loot.getRarity());
long weight = exact ? common.longValue() / rarity : Math.max(1, Math.round(WEIGHT_CEILING / (double) rarity));
JsonObject entry = new JsonObject();
entry.addProperty("type", "minecraft:item");
entry.addProperty("name", item.get());
entry.addProperty("weight", weight);
List<JsonObject> functions = functions(loot, item.get());
if (!functions.isEmpty()) {
entry.add("modifier", list(functions));
}
entries.add(entry);
}
JsonObject pool = new JsonObject();
if (table.getRarity() > 1) {
JsonObject chance = condition("minecraft:random_chance");
chance.addProperty("chance", 1.0f / table.getRarity());
pool.add("condition", chance);
}
pool.add("entries", entries);
pool.add("rolls", range(Math.min(table.getMinPicked(), table.getMaxPicked()), Math.max(table.getMinPicked(), table.getMaxPicked())));
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.LOOT_RARITY, resource,
"Loot rarity exports as proportional entry weight"));
return chestTable(List.of(pool));
}
JsonObject appendLoot(Map<VanillaBlockState, String> bindings, FeatureJson json) {
JsonArray rules = new JsonArray(bindings.size());
for (Map.Entry<VanillaBlockState, String> binding : bindings.entrySet()) {
JsonObject modifier = new JsonObject();
modifier.addProperty("type", "minecraft:append_loot");
modifier.addProperty("loot_table", binding.getValue());
JsonObject input = new JsonObject();
input.addProperty("predicate_type", "minecraft:blockstate_match");
input.add("block_state", json.syntax().blockState(binding.getKey()));
JsonObject location = new JsonObject();
location.addProperty("predicate_type", "minecraft:always_true");
JsonObject rule = new JsonObject();
rule.add("block_entity_modifier", modifier);
rule.add("input_predicate", input);
rule.add("location_predicate", location);
rule.add("output_state", json.syntax().blockState(binding.getKey()));
rules.add(rule);
}
JsonObject processor = new JsonObject();
processor.addProperty("processor_type", "minecraft:rule");
processor.add("rules", rules);
return processor;
}
public List<JsonResource> resources() {
List<JsonResource> resources = new ArrayList<>(tables.size());
for (Map.Entry<String, JsonObject> table : tables.entrySet()) {
String path = table.getKey().substring(namespace.length() + 1);
resources.add(new JsonResource("data/" + namespace + "/loot_table/" + path + ".json", table.getValue()));
}
return resources;
}
private JsonObject objectPool(IrisObjectPlacement placement, String containerBlock, List<ExportDiagnostic> diagnostics) {
List<JsonObject> global = new ArrayList<>();
List<JsonObject> filtered = new ArrayList<>();
List<IObjectLoot> sources = new ArrayList<>();
sources.addAll(placement.getVanillaLoot());
sources.addAll(placement.getLoot());
int vanillaCount = placement.getVanillaLoot().size();
for (int index = 0; index < sources.size(); index++) {
IObjectLoot loot = sources.get(index);
if (loot == null || loot.getWeight() <= 0 || loot.getName() == null) {
continue;
}
String id;
if (index < vanillaCount) {
id = normalizeId(loot.getName()).orElse(null);
} else {
IrisLootTable table = data == null ? null : data.getLootLoader().load(loot.getName(), false);
id = table == null ? null : reference(table, diagnostics);
}
if (id == null) {
continue;
}
JsonObject entry = new JsonObject();
entry.addProperty("type", "minecraft:loot_table");
entry.addProperty("value", id);
entry.addProperty("weight", loot.getWeight());
if (loot.getFilter().isEmpty()) {
global.add(entry);
} else if (filterMatches(loot, containerBlock)) {
filtered.add(entry);
}
}
List<JsonObject> chosen = filtered.isEmpty() ? global : filtered;
if (chosen.isEmpty()) {
return null;
}
JsonObject pool = new JsonObject();
pool.add("entries", list(chosen));
pool.addProperty("rolls", 1);
return pool;
}
private List<IrisLootTable> environment(LootScope scope, boolean objectDefined) {
List<IrisLootTable> sources = new ArrayList<>();
double multiplier = 1;
List<IrisLootReference> references = new ArrayList<>(3);
if (scope.dimension() != null) {
references.add(scope.dimension().getLoot());
}
if (scope.region() != null) {
references.add(scope.region().getLoot());
}
if (scope.biome() != null) {
references.add(scope.biome().getLoot());
}
for (IrisLootReference reference : references) {
if (reference == null) {
continue;
}
multiplier *= reference.getMultiplier();
inject(sources, load(reference), reference.getMode(), !objectDefined);
}
int target = (int) Math.round(sources.size() * multiplier);
if (target == sources.size() || sources.isEmpty()) {
return sources;
}
List<IrisLootTable> scaled = new ArrayList<>(Math.max(0, target));
for (int index = 0; index < target; index++) {
scaled.add(sources.get(index % sources.size()));
}
return scaled;
}
private static void inject(List<IrisLootTable> sources, List<IrisLootTable> additions, IrisLootMode mode, boolean fallback) {
if (mode == IrisLootMode.FALLBACK && !fallback) {
return;
}
if (mode == IrisLootMode.CLEAR) {
sources.clear();
return;
}
if (mode == IrisLootMode.REPLACE) {
sources.clear();
}
sources.addAll(additions);
}
private List<IrisLootTable> load(IrisLootReference reference) {
List<IrisLootTable> loaded = new ArrayList<>();
if (data == null || reference.getMode() == IrisLootMode.CLEAR) {
return loaded;
}
for (String key : reference.getTables()) {
IrisLootTable table = key == null ? null : data.getLootLoader().load(key, false);
if (table != null) {
loaded.add(table);
}
}
return loaded;
}
private String reference(IrisLootTable table, List<ExportDiagnostic> diagnostics) {
String key = table.getLoadKey();
String existing = tableIds.get(key);
if (existing != null) {
return existing;
}
String id = namespace + ":" + TABLE_FOLDER + VanillaResourceIdentifier.sanitizePath(key);
tableIds.put(key, id);
tables.put(id, table(table, diagnostics));
return id;
}
private static boolean filterMatches(IObjectLoot loot, String containerBlock) {
for (IrisBlockData filter : loot.getFilter()) {
String state = ContentGate.normalizeState(filter.stateKey());
if (state != null && ContentGate.baseKey(state).equals(containerBlock)) {
return true;
}
}
return false;
}
private List<JsonObject> functions(IrisLoot loot, String item) {
List<JsonObject> functions = new ArrayList<>();
int minAmount = Math.min(loot.getMinAmount(), loot.getMaxAmount());
int maxAmount = Math.max(loot.getMinAmount(), loot.getMaxAmount());
if (minAmount != 1 || maxAmount != 1) {
JsonObject count = function("minecraft:set_count");
count.add("count", range(minAmount, maxAmount));
functions.add(count);
}
if (damageable(item) && (loot.getMinDurability() != 1 || loot.getMaxDurability() != 1)) {
JsonObject damage = function("minecraft:set_damage");
JsonObject provider = new JsonObject();
provider.addProperty("type", "minecraft:uniform");
provider.addProperty("max", (float) clampUnit(Math.max(loot.getMinDurability(), loot.getMaxDurability())));
provider.addProperty("min", (float) clampUnit(Math.min(loot.getMinDurability(), loot.getMaxDurability())));
damage.add("damage", provider);
functions.add(damage);
}
if (loot.getDisplayName() != null && !loot.getDisplayName().isBlank()) {
JsonObject name = function("minecraft:set_name");
name.addProperty("name", loot.getDisplayName());
name.addProperty("target", "item_name");
functions.add(name);
}
if (!loot.getLore().isEmpty()) {
JsonObject lore = function("minecraft:set_lore");
JsonArray lines = new JsonArray(loot.getLore().size());
for (String line : loot.getLore()) {
lines.add(line);
}
lore.add("lore", lines);
lore.addProperty("mode", "replace_all");
functions.add(lore);
}
for (IrisEnchantment enchantment : loot.getEnchantments()) {
Optional<String> id = enchantment == null ? Optional.empty() : normalizeId(enchantment.getEnchantment());
if (id.isEmpty()) {
continue;
}
JsonObject enchant = function("minecraft:set_enchantments");
JsonObject levels = new JsonObject();
levels.add(id.get(), range(Math.min(enchantment.getMinLevel(), enchantment.getMaxLevel()),
Math.max(enchantment.getMinLevel(), enchantment.getMaxLevel())));
enchant.add("enchantments", levels);
enchant.addProperty("add", true);
if (enchantment.getChance() < 1) {
JsonObject chance = condition("minecraft:random_chance");
chance.addProperty("chance", (float) Math.max(0, enchantment.getChance()));
enchant.add("condition", chance);
}
functions.add(enchant);
}
return functions;
}
private JsonObject function(String type) {
JsonObject function = new JsonObject();
function.addProperty("type", type);
return function;
}
private JsonObject condition(String type) {
JsonObject condition = new JsonObject();
condition.addProperty("type", type);
return condition;
}
private static JsonObject chestTable(List<JsonObject> pools) {
JsonObject table = new JsonObject();
table.add("pools", list(pools));
table.addProperty("type", "minecraft:chest");
return table;
}
private static JsonArray single(String id) {
JsonObject entry = new JsonObject();
entry.addProperty("type", "minecraft:loot_table");
entry.addProperty("value", id);
return list(List.of(entry));
}
private static JsonElement range(int min, int max) {
if (min == max) {
return new JsonPrimitive(min);
}
JsonObject range = new JsonObject();
range.addProperty("type", "minecraft:uniform");
range.addProperty("max", max);
range.addProperty("min", min);
return range;
}
private static JsonArray list(List<JsonObject> values) {
JsonArray array = new JsonArray(values.size());
for (JsonObject value : values) {
array.add(value);
}
return array;
}
private static Optional<String> item(String type) {
Optional<String> id = normalizeId(type);
return id.isPresent() && id.get().startsWith("minecraft:") ? id : Optional.empty();
}
private static Optional<String> normalizeId(String raw) {
if (raw == null || raw.isBlank()) {
return Optional.empty();
}
String lowered = raw.trim().toLowerCase(Locale.ROOT);
String id = lowered.indexOf(':') < 0 ? "minecraft:" + lowered : lowered;
return VanillaResourceIdentifier.validIdentifier(id) ? Optional.of(id) : Optional.empty();
}
private static boolean damageable(String item) {
for (String suffix : DAMAGEABLE_SUFFIXES) {
if (item.endsWith(suffix)) {
return true;
}
}
return false;
}
private static double clampUnit(double value) {
return Math.max(0, Math.min(1, value));
}
public record LootScope(IrisDimension dimension, IrisRegion region, IrisBiome biome) {
}
}
@@ -0,0 +1,65 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import java.util.List;
import java.util.Map;
public record ObjectEdit(List<VanillaBlockState> find, boolean exact, List<WeightedState> replace, double chance) {
public ObjectEdit {
find = List.copyOf(find);
replace = List.copyOf(replace);
if (find.isEmpty() || replace.isEmpty()) {
throw new IllegalArgumentException("Object edits need at least one find and one replace state");
}
if (!Double.isFinite(chance) || chance < 0 || chance > 1) {
throw new IllegalArgumentException("Object edit chance must be within [0, 1], got " + chance);
}
}
public boolean deterministic() {
return chance >= 1;
}
public boolean matches(VanillaBlockState state) {
for (VanillaBlockState candidate : find) {
if (!candidate.name().equals(state.name())) {
continue;
}
if (!exact || propertiesMatch(candidate, state)) {
return true;
}
}
return false;
}
public int totalWeight() {
int total = 0;
for (WeightedState state : replace) {
total += state.weight();
}
return total;
}
public String descriptor() {
StringBuilder builder = new StringBuilder("find=");
for (VanillaBlockState state : find) {
builder.append(state.canonical()).append('|');
}
builder.append(";exact=").append(exact).append(";replace=");
for (WeightedState state : replace) {
builder.append(state.state().canonical()).append('*').append(state.weight()).append('|');
}
return builder.append(";chance=").append(chance).toString();
}
private static boolean propertiesMatch(VanillaBlockState find, VanillaBlockState state) {
for (Map.Entry<String, String> property : find.properties().entrySet()) {
if (!property.getValue().equals(state.properties().get(property.getKey()))) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,429 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.StructureTemplateEncoder;
import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.CompileStage;
import art.arcane.iris.pack.datapack.export.compile.PackCompileContext;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.iris.pack.datapack.export.ir.BinaryResource;
import art.arcane.iris.pack.datapack.export.ir.BiomeDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
public final class ObjectStage implements CompileStage {
private static final String NAME = "objects";
@Override
public String name() {
return NAME;
}
@Override
public void apply(PackCompileContext context) {
if (!context.objects()) {
return;
}
new Run(context).compile();
}
private static final class Run {
private static final int MAX_STILT_BASES = 16;
private static final String TREE_FOLDER = "trees/";
private static final String CAVE_SUFFIX = "_cave";
private final PackCompileContext context;
private final String namespace;
private final DataVersion target;
private final IrisData data;
private final List<ExportDiagnostic> diagnostics;
private final FeatureJson json;
private final ObjectVariantBaker baker;
private final VariantPlanner planner;
private final PlacementCompiler compiler;
private final LootCompiler loot;
private final WideObjectStructures wide;
private final StructureTemplateEncoder encoder = new StructureTemplateEncoder();
private final Map<String, TemplateVolume> volumes = new HashMap<>();
private final Map<String, BakedVariant> baked = new HashMap<>();
private final Map<IrisObjectPlacement, VariantPlanner.Plan> plans = new IdentityHashMap<>();
private final Map<IrisObjectPlacement, List<ObjectTemplates>> templates = new IdentityHashMap<>();
private final Map<IrisObjectPlacement, Boolean> reported = new IdentityHashMap<>();
private final Map<IrisObjectPlacement, Set<VanillaBlockState>> containers = new IdentityHashMap<>();
private final Map<IrisObjectPlacement, List<VanillaBlockState>> stiltBases = new IdentityHashMap<>();
private final Map<IrisObjectPlacement, Map<String, CompiledPlacement>> compiled = new IdentityHashMap<>();
private final Map<String, JsonResource> jsonResources = new TreeMap<>();
private final Map<String, BinaryResource> binaryResources = new TreeMap<>();
private final Map<String, Set<String>> structureBiomes = new TreeMap<>();
private int exported;
private Run(PackCompileContext context) {
this.context = context;
namespace = context.namespace();
target = context.capabilities().target();
data = context.data();
diagnostics = context.diagnostics();
json = new FeatureJson(context.capabilities());
baker = new ObjectVariantBaker();
planner = new VariantPlanner(context.dimension(), data, preventLeafDecay());
compiler = new PlacementCompiler(namespace, context.capabilities(), context.dimensionDef());
loot = new LootCompiler(namespace, data);
wide = new WideObjectStructures(namespace);
}
private void compile() {
IrisDimension dimension = context.dimension();
reportSpawners(dimension.getEntitySpawners(), "dimensions/" + dimension.getLoadKey());
Map<String, IrisRegion> regions = regions(dimension);
Set<String> defined = new TreeSet<>();
for (BiomeDef biome : context.biomes()) {
defined.add(namespace + ":" + biome.id());
}
for (Map.Entry<String, String> entry : context.biomeCodes().loadKeyByBiomeId().entrySet()) {
IrisBiome biome = defined.contains(entry.getKey()) ? data.getBiomeLoader().load(entry.getValue(), false) : null;
if (biome == null) {
continue;
}
reportSpawners(biome.getEntitySpawners(), "biomes/" + biome.getLoadKey());
IrisRegion region = regions.get(entry.getValue());
LootCompiler.LootScope scope = new LootCompiler.LootScope(dimension, region, biome);
List<IrisObjectPlacement> placements = new ArrayList<>(biome.getObjects());
if (region != null) {
placements.addAll(region.getObjects());
}
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace().isEmpty()) {
continue;
}
if (placement.getCarvingSupport().supportsSurface()) {
place(entry.getKey(), placement, PlacementInput.Anchor.SURFACE, scope);
}
if (placement.getCarvingSupport().supportsCarving()) {
place(entry.getKey(), placement, PlacementInput.Anchor.CAVE, scope);
}
}
}
if (exported > 0) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.STRUCTURE_AVOIDANCE,
"dimensions/" + dimension.getLoadKey(), "Objects do not avoid Minecraft structures"));
}
emit();
}
private void place(String biomeId, IrisObjectPlacement placement, PlacementInput.Anchor anchor, LootCompiler.LootScope scope) {
String name = name(placement, anchor);
VariantPlanner.Plan plan = plans.computeIfAbsent(placement, ignored -> planner.plan(placement, name, diagnostics));
List<ObjectTemplates> objects = templates.computeIfAbsent(placement, ignored -> bake(placement, plan));
if (objects.isEmpty()) {
return;
}
reportMarkers(placement, name);
Map<VanillaBlockState, String> bindings = bindings(placement, objects, scope);
Map<String, CompiledPlacement> byBinding = compiled.computeIfAbsent(placement, ignored -> new HashMap<>());
String key = anchor + "|" + bindings;
CompiledPlacement result = byBinding.get(key);
if (result == null) {
result = compile(placement, anchor, plan, objects, bindings, name);
byBinding.put(key, result);
}
for (String placed : result.placedFeatures()) {
context.features().add(biomeId, result.step(), placed);
}
for (String structure : result.structures()) {
structureBiomes.computeIfAbsent(structure, ignored -> new TreeSet<>()).add(biomeId);
}
exported++;
}
private CompiledPlacement compile(IrisObjectPlacement placement, PlacementInput.Anchor anchor, VariantPlanner.Plan plan,
List<ObjectTemplates> objects, Map<VanillaBlockState, String> bindings, String name) {
List<JsonObject> processors = new ArrayList<>();
for (ObjectEdit edit : plan.processorEdits()) {
processors.add(editProcessor(edit));
}
if (!bindings.isEmpty()) {
processors.add(loot.appendLoot(bindings, json));
}
List<VanillaBlockState> bases = PlacementCompiler.stilted(placement.getMode())
? stiltBases.computeIfAbsent(placement, ignored -> stiltBases(objects))
: List.of();
PlacementInput input = new PlacementInput(placement, anchor, tree(placement), objects, processors, bases, name);
CompiledPlacement result = compiler.compile(input, diagnostics);
for (JsonResource resource : result.resources()) {
addJson(resource);
}
for (ObjectTemplates object : objects) {
for (TemplateChoice choice : object.choices()) {
addTemplate(choice.variant());
}
}
return result;
}
private List<ObjectTemplates> bake(IrisObjectPlacement placement, VariantPlanner.Plan plan) {
List<String> keys = new ArrayList<>();
List<List<TemplateChoice>> choices = new ArrayList<>();
List<BakedVariant> all = new ArrayList<>();
for (String objectKey : placement.getPlace()) {
TemplateVolume volume = volume(objectKey);
if (volume == null) {
continue;
}
String objectPath = VanillaResourceIdentifier.sanitizePath(objectKey);
List<TemplateChoice> objectChoices = new ArrayList<>(plan.variants().size());
for (VariantPlanner.PlannedVariant planned : plan.variants()) {
String id = ObjectVariantBaker.templateId(namespace, objectPath, planned.spec(), target);
BakedVariant variant = baked.computeIfAbsent(id,
ignored -> baker.bake(id, volume, planned.spec(), "objects/" + objectKey, diagnostics));
objectChoices.add(new TemplateChoice(variant, planned.quarters(), planned.weight()));
all.add(variant);
}
keys.add(objectKey);
choices.add(objectChoices);
}
Map<String, BakedVariant> aligned = new HashMap<>();
List<BakedVariant> lifted = ObjectVariantBaker.alignBases(all);
for (int index = 0; index < all.size(); index++) {
aligned.put(all.get(index).id(), lifted.get(index));
}
List<ObjectTemplates> objects = new ArrayList<>(keys.size());
for (int index = 0; index < keys.size(); index++) {
List<TemplateChoice> alignedChoices = new ArrayList<>();
for (TemplateChoice choice : choices.get(index)) {
alignedChoices.add(new TemplateChoice(aligned.get(choice.variant().id()), choice.quarters(), choice.weight()));
}
objects.add(new ObjectTemplates(keys.get(index), alignedChoices));
}
return objects;
}
private TemplateVolume volume(String objectKey) {
if (volumes.containsKey(objectKey)) {
return volumes.get(objectKey);
}
IrisObject object = data.getObjectLoader().load(objectKey, false);
TemplateVolume volume = object == null ? null : StructureTemplateEncoder.volume(object, "objects/" + objectKey, diagnostics);
volumes.put(objectKey, volume);
return volume;
}
private Map<VanillaBlockState, String> bindings(IrisObjectPlacement placement, List<ObjectTemplates> objects, LootCompiler.LootScope scope) {
Set<VanillaBlockState> found = containers.computeIfAbsent(placement, ignored -> containers(objects));
Map<String, String> byBlock = new TreeMap<>();
Map<VanillaBlockState, String> bindings = new LinkedHashMap<>();
for (VanillaBlockState container : found) {
String table = byBlock.computeIfAbsent(container.name(),
block -> loot.binding(placement, block, scope, diagnostics).orElse(""));
if (!table.isEmpty()) {
bindings.put(container, table);
}
}
return bindings;
}
private static Set<VanillaBlockState> containers(List<ObjectTemplates> objects) {
Set<VanillaBlockState> found = new TreeSet<>((first, second) -> first.canonical().compareTo(second.canonical()));
for (ObjectTemplates object : objects) {
for (TemplateChoice choice : object.choices()) {
for (TemplateBlock block : choice.variant().volume().blocks()) {
if (LootCompiler.container(block.state())) {
found.add(block.state());
}
}
}
}
return found;
}
private JsonObject editProcessor(ObjectEdit edit) {
JsonArray rules = new JsonArray();
int total = edit.totalWeight();
for (VanillaBlockState find : edit.find()) {
double consumed = 0;
for (WeightedState replacement : edit.replace()) {
double share = edit.chance() * replacement.weight() / total;
double probability = consumed >= 1 ? 1 : Math.min(1, share / (1 - consumed));
consumed += share;
JsonObject input = new JsonObject();
if (edit.exact()) {
input.addProperty("predicate_type", "minecraft:random_blockstate_match");
input.add("block_state", json.syntax().blockState(find));
} else {
input.addProperty("predicate_type", "minecraft:random_block_match");
input.addProperty("block", find.name());
}
input.addProperty("probability", (float) probability);
JsonObject location = new JsonObject();
location.addProperty("predicate_type", "minecraft:always_true");
JsonObject rule = new JsonObject();
rule.add("input_predicate", input);
rule.add("location_predicate", location);
rule.add("output_state", json.syntax().blockState(replacement.state()));
rules.add(rule);
}
}
JsonObject processor = new JsonObject();
processor.addProperty("processor_type", "minecraft:rule");
processor.add("rules", rules);
return processor;
}
private static List<VanillaBlockState> stiltBases(List<ObjectTemplates> objects) {
Map<String, Integer> counts = new TreeMap<>();
Map<String, VanillaBlockState> states = new HashMap<>();
for (ObjectTemplates object : objects) {
for (TemplateChoice choice : object.choices()) {
int lowest = Integer.MAX_VALUE;
for (TemplateBlock block : choice.variant().volume().blocks()) {
lowest = Math.min(lowest, block.y());
}
for (TemplateBlock block : choice.variant().volume().blocks()) {
if (block.y() == lowest) {
counts.merge(block.state().name(), 1, Integer::sum);
states.putIfAbsent(block.state().name(), block.state());
}
}
}
}
List<String> names = new ArrayList<>(counts.keySet());
names.sort((first, second) -> Integer.compare(counts.get(second), counts.get(first)));
List<VanillaBlockState> bases = new ArrayList<>(Math.min(MAX_STILT_BASES, names.size()));
for (String name : names.subList(0, Math.min(MAX_STILT_BASES, names.size()))) {
bases.add(states.get(name));
}
return bases;
}
private Map<String, IrisRegion> regions(IrisDimension dimension) {
Map<String, IrisRegion> regions = new HashMap<>();
for (String regionKey : dimension.getRegions()) {
IrisRegion region = data.getRegionLoader().load(regionKey, false);
if (region == null) {
continue;
}
reportSpawners(region.getEntitySpawners(), "regions/" + region.getLoadKey());
List<String> roots = new ArrayList<>();
roots.addAll(region.getLandBiomes());
roots.addAll(region.getSeaBiomes());
roots.addAll(region.getShoreBiomes());
roots.addAll(region.getCaveBiomes());
for (String biome : members(roots)) {
regions.putIfAbsent(biome, region);
}
}
return regions;
}
private Set<String> members(List<String> roots) {
Set<String> members = new TreeSet<>();
List<String> pending = new ArrayList<>(roots);
while (!pending.isEmpty()) {
String key = pending.removeLast();
if (key == null || !members.add(key)) {
continue;
}
IrisBiome biome = data.getBiomeLoader().load(key, false);
if (biome != null) {
pending.addAll(biome.getChildren());
}
}
return members;
}
private void reportMarkers(IrisObjectPlacement placement, String name) {
if (placement.getMarkers().isEmpty() || reported.put(placement, Boolean.TRUE) != null) {
return;
}
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.CUSTOM_ENTITY, name,
placement.getMarkers().size() + " object markers are not exported"));
}
private void reportSpawners(List<String> spawners, String resource) {
if (spawners == null || spawners.isEmpty()) {
return;
}
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.CUSTOM_ENTITY, resource,
spawners.size() + " entity spawner rules are not exported"));
}
private void addTemplate(BakedVariant variant) {
String path = "data/" + namespace + "/structure/" + variant.id().substring(namespace.length() + 1) + ".nbt";
if (binaryResources.containsKey(path)) {
return;
}
try {
binaryResources.put(path, new BinaryResource(path, encoder.encode(variant.volume(), target)));
} catch (IOException exception) {
throw new UncheckedIOException("Could not encode object template " + variant.id(), exception);
}
}
private void addJson(JsonResource resource) {
JsonResource existing = jsonResources.putIfAbsent(resource.path(), resource);
if (existing != null && !existing.json().equals(resource.json())) {
throw new IllegalStateException("Object export produced two different resources at " + resource.path());
}
}
private void emit() {
for (JsonResource resource : loot.resources()) {
addJson(resource);
}
for (Map.Entry<String, Set<String>> structure : structureBiomes.entrySet()) {
addJson(wide.biomeTag(structure.getKey(), structure.getValue()));
}
context.jsonResources().addAll(jsonResources.values());
context.binaryResources().addAll(binaryResources.values());
}
private String name(IrisObjectPlacement placement, PlacementInput.Anchor anchor) {
String first = Objects.requireNonNull(placement.getPlace().getFirst(), "Object placement key");
String path = VanillaResourceIdentifier.sanitizePath(first);
String leaf = path.substring(path.lastIndexOf('/') + 1);
return anchor == PlacementInput.Anchor.CAVE ? leaf + CAVE_SUFFIX : leaf;
}
private static boolean tree(IrisObjectPlacement placement) {
if (placement.getTrees() != null && !placement.getTrees().isEmpty()) {
return true;
}
for (String key : placement.getPlace()) {
if (key != null && key.startsWith(TREE_FOLDER)) {
return true;
}
}
return false;
}
private static boolean preventLeafDecay() {
IrisSettings settings = IrisSettings.settings;
return settings == null || settings.getGenerator().preventLeafDecay;
}
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import java.util.List;
import java.util.Objects;
public record ObjectTemplates(String objectKey, List<TemplateChoice> choices) {
public ObjectTemplates {
Objects.requireNonNull(objectKey, "Object key");
choices = List.copyOf(choices);
}
public int footprint() {
int footprint = 0;
for (TemplateChoice choice : choices) {
footprint = Math.max(footprint, choice.variant().volume().footprint());
}
return footprint;
}
public long totalWeight() {
long total = 0;
for (TemplateChoice choice : choices) {
total += choice.weight();
}
return total;
}
}
@@ -0,0 +1,328 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.export.TemplateBlock;
import art.arcane.iris.pack.datapack.export.TemplateVolume;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public final class ObjectVariantBaker {
private static final Set<String> AIR = Set.of("minecraft:air", "minecraft:cave_air", "minecraft:void_air");
private static final String SNOW = "minecraft:snow";
private static final int SNOW_LAYER_STEPS = 7;
private static final int WARP_SPACING_X = 4099;
private static final int WARP_SPACING_Z = 7919;
private static final long EDIT_SALT = 0x5DEECE66DL;
private static final long PICK_SALT = 0x2545F4914F6CDD1DL;
private static final long SNOW_SALT = 0x9E3779B97F4A7C15L;
private static final Comparator<Voxel> VOXEL_ORDER = Comparator
.comparingInt(Voxel::y)
.thenComparingInt(Voxel::x)
.thenComparingInt(Voxel::z);
public static String templateId(String namespace, String objectPath, VariantSpec spec, DataVersion target) {
VanillaResourceIdentifier.requireNamespace(namespace, "template namespace");
return namespace + ":object/" + objectPath + "/" + ContentIds.hash(spec.descriptor() + ";target=" + target.name());
}
public BakedVariant bake(String templateId, TemplateVolume source, VariantSpec spec, String resource, List<ExportDiagnostic> diagnostics) {
long seed = ContentIds.seed(spec.descriptor());
List<Voxel> voxels = prepare(source, spec, seed);
if (!spec.reshapes()) {
return new BakedVariant(templateId, volume(source.sizeX(), source.sizeY(), source.sizeZ(), voxels), 0);
}
int sizeX = source.sizeX();
int sizeY = source.sizeY();
int sizeZ = source.sizeZ();
if (spec.scale() != 1) {
sizeX = scaledSize(sizeX, spec.scale());
sizeY = scaledSize(sizeY, spec.scale());
sizeZ = scaledSize(sizeZ, spec.scale());
voxels = scale(voxels, spec.scale(), sizeX, sizeY, sizeZ);
}
voxels = anchor(voxels, spec.rotation(), sizeX / 2, sizeY / 2, sizeZ / 2);
if (spec.warp() != null) {
voxels = warp(voxels, spec.warp());
}
if (spec.snow() > 0) {
voxels = snow(voxels, spec.snow(), seed);
}
return reshaped(templateId, voxels, resource, diagnostics);
}
public static List<BakedVariant> alignBases(List<BakedVariant> variants) {
int lowest = 0;
for (BakedVariant variant : variants) {
lowest = Math.min(lowest, variant.baseY());
}
List<BakedVariant> aligned = new ArrayList<>(variants.size());
for (BakedVariant variant : variants) {
int lift = variant.baseY() - lowest;
if (lift == 0) {
aligned.add(variant);
continue;
}
TemplateVolume volume = variant.volume();
List<TemplateBlock> blocks = new ArrayList<>(volume.blocks().size());
for (TemplateBlock block : volume.blocks()) {
blocks.add(new TemplateBlock(block.x(), block.y() + lift, block.z(), block.state(), block.nbt()));
}
aligned.add(new BakedVariant(variant.id() + "_lift" + lift,
new TemplateVolume(volume.sizeX(), volume.sizeY() + lift, volume.sizeZ(), blocks), lowest));
}
return aligned;
}
private List<Voxel> prepare(TemplateVolume source, VariantSpec spec, long seed) {
List<Voxel> voxels = new ArrayList<>(source.blocks().size());
for (TemplateBlock block : source.blocks()) {
if (AIR.contains(block.state().name())) {
continue;
}
VanillaBlockState state = block.state();
CompoundTag nbt = block.nbt();
for (int index = 0; index < spec.edits().size(); index++) {
ObjectEdit edit = spec.edits().get(index);
if (!edit.deterministic() && unit(seed, block.x(), block.y(), block.z(), index, EDIT_SALT) >= edit.chance()) {
continue;
}
if (!edit.matches(state)) {
continue;
}
VanillaBlockState replacement = pick(edit, unit(seed, block.x(), block.y(), block.z(), index, PICK_SALT));
if (replacement.name().equals(state.name())) {
state = merge(state, replacement);
} else {
state = replacement;
nbt = null;
}
}
if (AIR.contains(state.name())) {
continue;
}
if (spec.preventLeafDecay()) {
state = persistentLeaves(state);
}
voxels.add(new Voxel(block.x(), block.y(), block.z(), state, nbt));
}
return voxels;
}
private static VanillaBlockState pick(ObjectEdit edit, double roll) {
double cursor = roll * edit.totalWeight();
for (WeightedState state : edit.replace()) {
cursor -= state.weight();
if (cursor < 0) {
return state.state();
}
}
return edit.replace().getLast().state();
}
private static VanillaBlockState merge(VanillaBlockState original, VanillaBlockState replacement) {
Map<String, String> properties = new TreeMap<>(original.properties());
properties.putAll(replacement.properties());
return new VanillaBlockState(original.name(), properties);
}
private static VanillaBlockState persistentLeaves(VanillaBlockState state) {
Map<String, String> properties = state.properties();
if (!properties.containsKey("distance") || !"false".equals(properties.get("persistent"))) {
return state;
}
Map<String, String> persistent = new TreeMap<>(properties);
persistent.put("persistent", "true");
return new VanillaBlockState(state.name(), persistent);
}
private static int scaledSize(int size, double scale) {
return Math.max(1, (int) Math.ceil(size * scale));
}
private static List<Voxel> scale(List<Voxel> voxels, double scale, int sizeX, int sizeY, int sizeZ) {
List<Voxel> sorted = new ArrayList<>(voxels);
sorted.sort(VOXEL_ORDER);
Map<Long, Voxel> scaled = new LinkedHashMap<>();
for (Voxel voxel : sorted) {
int minX = Math.min(sizeX - 1, (int) Math.floor(voxel.x() * scale));
int minY = Math.min(sizeY - 1, (int) Math.floor(voxel.y() * scale));
int minZ = Math.min(sizeZ - 1, (int) Math.floor(voxel.z() * scale));
int maxX = Math.min(sizeX - 1, Math.max(minX, (int) Math.ceil((voxel.x() + 1) * scale) - 1));
int maxY = Math.min(sizeY - 1, Math.max(minY, (int) Math.ceil((voxel.y() + 1) * scale) - 1));
int maxZ = Math.min(sizeZ - 1, Math.max(minZ, (int) Math.ceil((voxel.z() + 1) * scale) - 1));
for (int y = minY; y <= maxY; y++) {
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
scaled.put(key(x, y, z), new Voxel(x, y, z, voxel.state(), voxel.nbt()));
}
}
}
}
return new ArrayList<>(scaled.values());
}
private static List<Voxel> anchor(List<Voxel> voxels, VariantSpec.Rotation rotation, int centerX, int centerY, int centerZ) {
BlockStateRotation states = BlockStateRotation.of(rotation.x(), rotation.y(), rotation.z());
double[] lift = rotate(0, centerY, 0, rotation);
int rotatedCenterY = (int) Math.floor(lift[1]);
List<Voxel> sorted = new ArrayList<>(voxels);
sorted.sort(VOXEL_ORDER);
Map<Long, Voxel> anchored = new LinkedHashMap<>();
for (Voxel voxel : sorted) {
double[] rotated = rotate(voxel.x() - centerX, voxel.y() - centerY, voxel.z() - centerZ, rotation);
int x = (int) Math.floor(rotated[0]);
int y = (int) Math.floor(rotated[1]) + rotatedCenterY;
int z = (int) Math.floor(rotated[2]);
anchored.put(key(x, y, z), new Voxel(x, y, z, states.apply(voxel.state()), voxel.nbt()));
}
return new ArrayList<>(anchored.values());
}
private static double[] rotate(double x, double y, double z, VariantSpec.Rotation rotation) {
double rx = x;
double ry = y;
double rz = z;
if (rotation.x() != 0) {
double angle = Math.toRadians(rotation.x());
double cos = cos(angle);
double sin = sin(angle);
double nextY = cos * ry - sin * rz;
rz = sin * ry + cos * rz;
ry = nextY;
}
if (rotation.z() != 0) {
double angle = Math.toRadians(rotation.z());
double cos = cos(angle);
double sin = sin(angle);
double nextX = cos * rx - sin * ry;
ry = sin * rx + cos * ry;
rx = nextX;
}
if (rotation.y() != 0) {
double angle = Math.toRadians(rotation.y());
double cos = cos(angle);
double sin = sin(angle);
double nextX = cos * rx + sin * rz;
rz = -sin * rx + cos * rz;
rx = nextX;
}
return new double[]{rx, ry, rz};
}
private static double cos(double angle) {
double value = Math.cos(angle);
return Math.abs(value) < 1.0e-12 ? 0 : value;
}
private static double sin(double angle) {
double value = Math.sin(angle);
return Math.abs(value) < 1.0e-12 ? 0 : value;
}
private static List<Voxel> warp(List<Voxel> voxels, VariantWarp warp) {
double originX = (double) warp.index() * WARP_SPACING_X;
double originZ = (double) warp.index() * WARP_SPACING_Z;
List<Voxel> sorted = new ArrayList<>(voxels);
sorted.sort(VOXEL_ORDER);
Map<Long, Voxel> warped = new LinkedHashMap<>();
for (Voxel voxel : sorted) {
double worldX = voxel.x() + originX;
double worldZ = voxel.z() + originZ;
int x = (int) (voxel.x() + warp.field().offset(worldX, voxel.y(), worldZ));
int z = (int) (voxel.z() + warp.field().offset(worldZ, voxel.y(), worldX));
warped.put(key(x, voxel.y(), z), new Voxel(x, voxel.y(), z, voxel.state(), voxel.nbt()));
}
return new ArrayList<>(warped.values());
}
private static List<Voxel> snow(List<Voxel> voxels, double snow, long seed) {
Map<Long, Voxel> tops = new TreeMap<>();
Map<Long, Voxel> occupied = new LinkedHashMap<>();
for (Voxel voxel : voxels) {
occupied.put(key(voxel.x(), voxel.y(), voxel.z()), voxel);
long column = key(voxel.x(), 0, voxel.z());
Voxel top = tops.get(column);
if (top == null || top.y() < voxel.y()) {
tops.put(column, voxel);
}
}
int steps = (int) (snow * SNOW_LAYER_STEPS);
List<Voxel> covered = new ArrayList<>(voxels);
for (Voxel top : tops.values()) {
if (occupied.containsKey(key(top.x(), top.y() + 1, top.z()))) {
continue;
}
int index = steps <= 0 ? 0 : (int) Math.floor(unit(seed, top.x(), top.y(), top.z(), 0, SNOW_SALT) * steps);
int layers = Math.max(0, Math.min(index, SNOW_LAYER_STEPS)) + 1;
covered.add(new Voxel(top.x(), top.y() + 1, top.z(),
new VanillaBlockState(SNOW, Map.of("layers", Integer.toString(layers))), null));
}
return covered;
}
private BakedVariant reshaped(String templateId, List<Voxel> voxels, String resource, List<ExportDiagnostic> diagnostics) {
if (voxels.isEmpty()) {
return new BakedVariant(templateId, new TemplateVolume(1, 1, 1, List.of()), 0);
}
int extentX = 0;
int extentZ = 0;
int minY = 0;
int maxY = Integer.MIN_VALUE;
for (Voxel voxel : voxels) {
extentX = Math.max(extentX, Math.abs(voxel.x()));
extentZ = Math.max(extentZ, Math.abs(voxel.z()));
minY = Math.min(minY, voxel.y());
maxY = Math.max(maxY, voxel.y());
}
List<Voxel> local = new ArrayList<>(voxels.size());
for (Voxel voxel : voxels) {
local.add(new Voxel(voxel.x() + extentX, voxel.y() - minY, voxel.z() + extentZ, voxel.state(), voxel.nbt()));
}
TemplateVolume volume = volume(extentX * 2 + 1, maxY - minY + 1, extentZ * 2 + 1, local);
return new BakedVariant(templateId, volume, minY);
}
private static TemplateVolume volume(int sizeX, int sizeY, int sizeZ, List<Voxel> voxels) {
List<Voxel> sorted = new ArrayList<>(voxels);
sorted.sort(VOXEL_ORDER);
List<TemplateBlock> blocks = new ArrayList<>(sorted.size());
for (Voxel voxel : sorted) {
blocks.add(new TemplateBlock(voxel.x(), voxel.y(), voxel.z(), voxel.state(), voxel.nbt()));
}
return new TemplateVolume(sizeX, sizeY, sizeZ, blocks);
}
private static long key(int x, int y, int z) {
return ((long) (x & 0x1FFFFF) << 42) | ((long) (y & 0x1FFFFF) << 21) | (z & 0x1FFFFF);
}
private static double unit(long seed, int x, int y, int z, int index, long salt) {
long mixed = mix(seed ^ salt);
mixed = mix(mixed ^ x);
mixed = mix(mixed ^ ((long) y << 21));
mixed = mix(mixed ^ ((long) z << 42));
mixed = mix(mixed ^ index);
return (mixed >>> 11) * 0x1.0p-53;
}
private static long mix(long value) {
long mixed = value + 0x9E3779B97F4A7C15L;
mixed = (mixed ^ (mixed >>> 30)) * 0xBF58476D1CE4E5B9L;
mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL;
return mixed ^ (mixed >>> 31);
}
private record Voxel(int x, int y, int z, VanillaBlockState state, CompoundTag nbt) {
}
}
@@ -0,0 +1,396 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.iris.pack.datapack.export.compile.decoration.GenerationStep;
import art.arcane.iris.pack.datapack.export.compile.decoration.Placements;
import art.arcane.iris.pack.datapack.export.ir.DimensionDef;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.datapack.export.ir.JsonResource;
import art.arcane.iris.pack.datapack.export.lower.ExportCapabilities;
import art.arcane.iris.structure.object.IrisObjectLimit;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public final class PlacementCompiler {
public static final int MAX_FEATURE_WIDTH = 32;
private static final int TILE_WIDTH = 17;
private static final int WINDOW_REACH = 16;
private static final int CAVE_SCAN_STEPS = 32;
private static final int CAVE_FLOOR_MARGIN = 8;
private static final int MIN_ANCHOR = -2032;
private static final int MAX_ANCHOR = 2031;
private static final long WEIGHT_CEILING = 1_000_000L;
private static final List<String> QUARTER_NAMES = List.of("none", "counterclockwise_90", "180", "clockwise_90");
private static final List<String> FLUIDS = List.of("minecraft:water", "minecraft:flowing_water", "minecraft:lava", "minecraft:flowing_lava");
private static final Set<ObjectPlaceMode> STILT_MODES = Set.of(ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
ObjectPlaceMode.MIN_STILT, ObjectPlaceMode.FAST_MIN_STILT, ObjectPlaceMode.CENTER_STILT,
ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT);
private static final Set<ObjectPlaceMode> VACUUM_MODES = Set.of(ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_HIGH,
ObjectPlaceMode.VACUUM_FAST, ObjectPlaceMode.VACUUM_ORGANIC, ObjectPlaceMode.VACUUM_WAVY);
private final String namespace;
private final FeatureJson json;
private final DimensionDef dimension;
private final WideObjectStructures wide;
public PlacementCompiler(String namespace, ExportCapabilities capabilities, DimensionDef dimension) {
this.namespace = VanillaResourceIdentifier.requireNamespace(namespace, "placement namespace");
json = new FeatureJson(capabilities);
this.dimension = dimension;
wide = new WideObjectStructures(namespace);
}
public static boolean stilted(ObjectPlaceMode mode) {
return STILT_MODES.contains(mode);
}
public CompiledPlacement compile(PlacementInput input, List<ExportDiagnostic> diagnostics) {
IrisObjectPlacement placement = input.placement();
GenerationStep step = input.tree() ? GenerationStep.VEGETAL_DECORATION : GenerationStep.SURFACE_STRUCTURES;
int reach = Math.max(Math.abs(placement.getTranslate().getX()), Math.abs(placement.getTranslate().getZ()));
List<ObjectTemplates> narrow = new ArrayList<>();
List<ObjectTemplates> wideObjects = new ArrayList<>();
for (ObjectTemplates object : input.objects()) {
if (object.choices().isEmpty()) {
continue;
}
if (object.footprint() + 2 * reach <= MAX_FEATURE_WIDTH) {
narrow.add(object);
} else {
wideObjects.add(object);
}
}
int total = narrow.size() + wideObjects.size();
List<String> placed = new ArrayList<>(1);
List<String> structures = new ArrayList<>(1);
List<JsonResource> resources = new ArrayList<>();
if (total == 0) {
return new CompiledPlacement(step, placed, structures, resources);
}
double chance = Math.max(0, Math.min(1, placement.getChance()));
int density = Math.max(1, placement.getDensity());
if (VACUUM_MODES.contains(placement.getMode())) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.OBJECT_VACUUM, input.name(),
"Vacuum terrain shaping is not exported; the object places at its center height"));
}
if (!narrow.isEmpty()) {
compileNarrow(input, narrow, chance, density, (double) narrow.size() / total, resources, placed);
}
if (!wideObjects.isEmpty()) {
List<TemplateChoice> templates = weighted(wideObjects);
double perChunk = chance * density * wideObjects.size() / total;
int startY = placement.getTranslate().getY() - 1 + lowestBase(templates);
WideObjectStructures.Compiled compiled = wide.compile(input.name(), templates, perChunk, startY, processorList(input.processors()));
resources.addAll(compiled.resources());
structures.add(compiled.structureId());
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.WIDE_OBJECT, input.name(),
wideObjects.size() + " objects wider than " + MAX_FEATURE_WIDTH + " blocks export as single-piece structures"));
}
return new CompiledPlacement(step, placed, structures, resources);
}
private void compileNarrow(PlacementInput input, List<ObjectTemplates> narrow, double chance, int density, double share,
List<JsonResource> resources, List<String> placed) {
IrisObjectPlacement placement = input.placement();
List<TemplateChoice> templates = weighted(narrow);
boolean paint = placement.getMode() == ObjectPlaceMode.PAINT && input.anchor() == PlacementInput.Anchor.SURFACE;
List<JsonObject> processors = new ArrayList<>(input.processors());
if (paint) {
processors.add(gravity(placement));
}
JsonObject feature = template(templates, processors);
if (stilted(placement.getMode())) {
feature = stilts(feature, templates, input.stiltBases());
}
String name = input.name();
String featurePath = "object/" + name + "_" + ContentIds.hash(feature);
resources.add(new JsonResource("data/" + namespace + "/" + json.syntax().featureFolder() + "/" + featurePath + ".json", feature));
JsonObject placedFeature = new JsonObject();
placedFeature.addProperty("feature", namespace + ":" + featurePath);
placedFeature.add("placement", modifiers(input, templates, chance, density, share, paint));
String placedPath = "object/" + name + "_" + ContentIds.hash(placedFeature);
resources.add(new JsonResource("data/" + namespace + "/worldgen/placed_feature/" + placedPath + ".json", placedFeature));
placed.add(namespace + ":" + placedPath);
}
private JsonArray modifiers(PlacementInput input, List<TemplateChoice> templates, double chance, int density, double share, boolean paint) {
IrisObjectPlacement placement = input.placement();
int translateX = placement.getTranslate().getX();
int translateY = placement.getTranslate().getY();
int translateZ = placement.getTranslate().getZ();
int yRandom = placement.getTranslate().getYRandom();
int spreadLow = yRandom < 0 ? yRandom : 0;
int spreadHigh = yRandom > 0 ? yRandom - 1 : yRandom < 0 ? -1 : 0;
int base = lowestBase(templates);
List<JsonObject> modifiers = new ArrayList<>(json.chanceAndCount(chance, density, share));
modifiers.add(Placements.typed("minecraft:in_square"));
if (input.anchor() == PlacementInput.Anchor.CAVE) {
boolean ceiling = placement.getMode() == ObjectPlaceMode.CEILING_HANG;
modifiers.add(caveRange());
modifiers.add(environmentScan(ceiling));
int lift = ceiling ? -tallest(templates) : 0;
modifiers.addAll(json.offset(translateX, lift + translateY + base + spreadLow, lift + translateY + base + spreadHigh, translateZ));
} else if (placement.getMode() == ObjectPlaceMode.FLOATING) {
modifiers.add(fixedHeight(translateY + base + spreadLow, translateY + base + spreadHigh));
} else {
modifiers.add(Placements.heightmap(placement.isUnderwater() ? "OCEAN_FLOOR_WG" : "WORLD_SURFACE_WG"));
if (!placement.isForcePlace() && placement.isRequireSurfaceSupport()) {
modifiers.add(Placements.filter(support(templates, placement)));
}
if (paint) {
modifiers.addAll(json.offset(translateX, 0, 0, translateZ));
} else {
modifiers.addAll(json.offset(translateX, translateY - 1 + base + spreadLow, translateY - 1 + base + spreadHigh, translateZ));
}
}
JsonObject clamp = clamp(placement.getClamp());
if (clamp != null) {
modifiers.add(Placements.filter(clamp));
}
modifiers.add(Placements.typed("minecraft:biome"));
JsonArray array = new JsonArray(modifiers.size());
for (JsonObject modifier : modifiers) {
array.add(modifier);
}
return array;
}
private JsonObject template(List<TemplateChoice> templates, List<JsonObject> processors) {
JsonArray entries = new JsonArray(templates.size());
for (TemplateChoice template : templates) {
JsonObject data = new JsonObject();
data.addProperty("id", template.variant().id());
if (template.quarters().size() < QUARTER_NAMES.size()) {
JsonArray rotations = new JsonArray(template.quarters().size());
for (int quarter : template.quarters()) {
rotations.add(QUARTER_NAMES.get(quarter));
}
data.add("rotations", rotations);
}
JsonObject entry = new JsonObject();
entry.add("data", data);
entry.addProperty("weight", template.weight());
entries.add(entry);
}
JsonObject fields = new JsonObject();
fields.add("templates", entries);
if (!processors.isEmpty()) {
fields.add("processors", processorList(processors));
}
return json.syntax().feature("minecraft:template", fields);
}
private JsonObject stilts(JsonObject template, List<TemplateChoice> templates, List<VanillaBlockState> bases) {
JsonArray features = new JsonArray();
JsonObject anchored = new JsonObject();
anchored.add("feature", template);
anchored.add("placement", new JsonArray());
features.add(anchored);
int footprint = 1;
for (TemplateChoice choice : templates) {
footprint = Math.max(footprint, choice.variant().volume().footprint());
}
int start = -(footprint / 2);
int end = footprint - 1 - footprint / 2;
int tiles = (int) Math.ceil(footprint / (double) TILE_WIDTH);
int width = (int) Math.ceil(footprint / (double) tiles);
int size = Math.max(1, width - 1);
JsonObject canReplace = canReplace();
JsonObject pillar = pillar(bases, canReplace);
JsonObject gate = Placements.allOf(List.of(Placements.not(Placements.air(0, 1, 0)), canReplace));
for (int tileX = 0; tileX < tiles; tileX++) {
for (int tileZ = 0; tileZ < tiles; tileZ++) {
int x = Math.min(start + tileX * width, end - size);
int z = Math.min(start + tileZ * width, end - size);
JsonArray placement = new JsonArray(3);
JsonObject offset = Placements.typed("minecraft:offset");
offset.addProperty("x", x);
offset.addProperty("y", -2);
offset.addProperty("z", z);
placement.add(offset);
JsonObject cuboid = Placements.typed("minecraft:cuboid");
cuboid.addProperty("xz_size", size);
cuboid.addProperty("y_size", 1);
placement.add(cuboid);
placement.add(Placements.filter(gate));
JsonObject tile = new JsonObject();
tile.add("feature", pillar);
tile.add("placement", placement);
features.add(tile);
}
}
JsonObject fields = new JsonObject();
fields.add("features", features);
return json.syntax().feature("minecraft:overlay", fields);
}
private JsonObject pillar(List<VanillaBlockState> bases, JsonObject canReplace) {
JsonObject provider;
if (bases.isEmpty()) {
VanillaBlockState fill = VanillaBlockState.parse(dimension == null ? "minecraft:stone" : dimension.defaultBlock());
provider = json.syntax().provider(List.of(new WeightedState(fill, 1)));
} else {
JsonArray rules = new JsonArray(bases.size());
for (VanillaBlockState base : bases) {
JsonObject match = Placements.typed("minecraft:matching_blocks");
match.addProperty("blocks", base.name());
match.add("offset", FeatureJson.vector(0, 1, 0));
JsonObject rule = new JsonObject();
rule.add("if_true", match);
rule.add("then", json.syntax().provider(List.of(new WeightedState(base, 1))));
rules.add(rule);
}
provider = new JsonObject();
provider.addProperty("type", "minecraft:rule_based");
provider.add("fallback", json.syntax().provider(List.of(new WeightedState(bases.getFirst(), 1))));
provider.add("rules", rules);
}
JsonObject fields = new JsonObject();
fields.add("block", provider);
fields.add("can_replace", canReplace);
fields.addProperty("direction", "down");
return json.syntax().feature("minecraft:single_block_pillar", fields);
}
private static JsonObject canReplace() {
JsonObject fluids = Placements.typed("minecraft:matching_fluids");
JsonArray list = new JsonArray(FLUIDS.size());
for (String fluid : FLUIDS) {
list.add(fluid);
}
fluids.add("fluids", list);
return Placements.anyOf(List.of(Placements.air(0, 0, 0), fluids, Placements.typed("minecraft:replaceable")));
}
private static JsonObject gravity(IrisObjectPlacement placement) {
JsonObject gravity = new JsonObject();
gravity.addProperty("processor_type", "minecraft:gravity");
gravity.addProperty("heightmap", placement.isUnderwater() ? "OCEAN_FLOOR_WG" : "WORLD_SURFACE_WG");
gravity.addProperty("offset", placement.getTranslate().getY() - 1);
return gravity;
}
private static JsonObject processorList(List<JsonObject> processors) {
JsonArray list = new JsonArray(processors.size());
for (JsonObject processor : processors) {
list.add(processor.deepCopy());
}
JsonObject wrapper = new JsonObject();
wrapper.add("processors", list);
return wrapper;
}
private static JsonObject support(List<TemplateChoice> templates, IrisObjectPlacement placement) {
int half = Integer.MAX_VALUE;
for (TemplateChoice template : templates) {
half = Math.min(half, Math.min(template.variant().volume().sizeX(), template.variant().volume().sizeZ()) / 2);
}
int reach = Math.min(WINDOW_REACH, Math.max(0, half + Math.max(0, placement.getSurfaceSupportBuffer())));
int depth = Math.max(1, placement.getSurfaceSupportDepth());
JsonObject predicate = Placements.typed("minecraft:volume_match");
predicate.add("match", Placements.typed("minecraft:solid"));
predicate.add("max", FeatureJson.vector(reach, -1, reach));
predicate.add("min", FeatureJson.vector(-reach, -depth, -reach));
return predicate;
}
private JsonObject clamp(IrisObjectLimit limit) {
if (limit == null) {
return null;
}
int bottom = dimension == null ? MIN_ANCHOR : dimension.minY();
int top = dimension == null ? MAX_ANCHOR : dimension.minY() + dimension.height() - 1;
int min = Math.max(MIN_ANCHOR, limit.getMinimumHeight());
int max = Math.min(MAX_ANCHOR, limit.getMaximumHeight());
if (min <= bottom && max >= top) {
return null;
}
JsonObject predicate = Placements.typed("minecraft:height_range");
predicate.add("max_inclusive", FeatureJson.absolute(Math.max(min, max)));
predicate.add("min_inclusive", FeatureJson.absolute(Math.min(min, max)));
return predicate;
}
private JsonObject caveRange() {
int floor = dimension == null ? 0 : dimension.minY() + CAVE_FLOOR_MARGIN;
int ceiling = dimension == null ? 63 : dimension.fluidHeight();
JsonObject height = new JsonObject();
height.addProperty("type", "minecraft:uniform");
height.add("max_inclusive", FeatureJson.absolute(Math.max(floor, ceiling)));
height.add("min_inclusive", FeatureJson.absolute(Math.min(floor, ceiling)));
JsonObject modifier = Placements.typed("minecraft:height_range");
modifier.add("height", height);
return modifier;
}
private static JsonObject environmentScan(boolean ceiling) {
JsonObject scan = Placements.typed("minecraft:environment_scan");
scan.add("allowed_search_condition", Placements.air(0, 0, 0));
scan.addProperty("direction_of_search", ceiling ? "up" : "down");
scan.addProperty("max_steps", CAVE_SCAN_STEPS);
scan.add("target_condition", Placements.typed("minecraft:solid"));
return scan;
}
private static JsonObject fixedHeight(int low, int high) {
JsonObject modifier = Placements.typed("minecraft:height_range");
int clampedLow = Math.max(MIN_ANCHOR, Math.min(MAX_ANCHOR, low));
int clampedHigh = Math.max(MIN_ANCHOR, Math.min(MAX_ANCHOR, high));
if (clampedLow == clampedHigh) {
modifier.add("height", FeatureJson.absolute(clampedLow));
return modifier;
}
JsonObject height = new JsonObject();
height.addProperty("type", "minecraft:uniform");
height.add("max_inclusive", FeatureJson.absolute(clampedHigh));
height.add("min_inclusive", FeatureJson.absolute(clampedLow));
modifier.add("height", height);
return modifier;
}
static List<TemplateChoice> weighted(List<ObjectTemplates> objects) {
BigInteger common = BigInteger.ONE;
for (ObjectTemplates object : objects) {
BigInteger total = BigInteger.valueOf(object.totalWeight());
common = common.multiply(total).divide(common.gcd(total));
}
boolean exact = common.compareTo(BigInteger.valueOf(WEIGHT_CEILING)) <= 0;
List<TemplateChoice> weighted = new ArrayList<>();
for (ObjectTemplates object : objects) {
long total = object.totalWeight();
for (TemplateChoice choice : object.choices()) {
long weight = exact
? common.longValue() / total * choice.weight()
: Math.max(1, Math.round(WEIGHT_CEILING * (double) choice.weight() / total));
weighted.add(new TemplateChoice(choice.variant(), choice.quarters(), weight));
}
}
return weighted;
}
private static int lowestBase(List<TemplateChoice> templates) {
int base = 0;
for (TemplateChoice template : templates) {
base = Math.min(base, template.variant().baseY());
}
return base;
}
private static int tallest(List<TemplateChoice> templates) {
int tallest = 1;
for (TemplateChoice template : templates) {
tallest = Math.max(tallest, template.variant().volume().sizeY());
}
return tallest;
}
}
@@ -0,0 +1,26 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.VanillaResourceIdentifier;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.Objects;
public record PlacementInput(IrisObjectPlacement placement, Anchor anchor, boolean tree, List<ObjectTemplates> objects,
List<JsonObject> processors, List<VanillaBlockState> stiltBases, String name) {
public PlacementInput {
Objects.requireNonNull(placement, "Object placement");
Objects.requireNonNull(anchor, "Placement anchor");
objects = List.copyOf(objects);
processors = List.copyOf(processors);
stiltBases = List.copyOf(stiltBases);
VanillaResourceIdentifier.requirePath(name, "placement name");
}
public enum Anchor {
SURFACE,
CAVE
}
}
@@ -0,0 +1,22 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import java.util.List;
import java.util.Objects;
public record TemplateChoice(BakedVariant variant, List<Integer> quarters, long weight) {
public TemplateChoice {
Objects.requireNonNull(variant, "Template choice variant");
quarters = List.copyOf(quarters);
if (quarters.isEmpty()) {
throw new IllegalArgumentException("Template choice " + variant.id() + " needs at least one rotation");
}
for (int quarter : quarters) {
if (quarter < 0 || quarter > 3) {
throw new IllegalArgumentException("Template rotation quarter must be within [0, 3], got " + quarter);
}
}
if (weight < 1) {
throw new IllegalArgumentException("Template choice " + variant.id() + " needs a positive weight, got " + weight);
}
}
}
@@ -0,0 +1,263 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.datapack.export.VanillaBlockState;
import art.arcane.iris.pack.datapack.export.compile.WeightedState;
import art.arcane.iris.pack.datapack.export.ir.ExportDiagnostic;
import art.arcane.iris.pack.datapack.export.ir.ExportLimit;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectReplace;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectScale;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
final class VariantPlanner {
private static final int MAX_VARIANTS = 32;
private static final int MAX_SCALE_VARIANTS = 32;
private static final int WARP_VARIANTS = 3;
private static final int SPIN_STEPS = 360;
private static final double FULL_TURN = 360;
private static final double QUARTER = 90;
private static final double ANGLE_PRECISION = 1.0e6;
private static final Gson GSON = new Gson();
private final IrisDimension dimension;
private final IrisData data;
private final boolean preventLeafDecay;
VariantPlanner(IrisDimension dimension, IrisData data, boolean preventLeafDecay) {
this.dimension = dimension;
this.data = data;
this.preventLeafDecay = preventLeafDecay;
}
Plan plan(IrisObjectPlacement placement, String resource, List<ExportDiagnostic> diagnostics) {
List<ObjectEdit> baked = new ArrayList<>();
List<ObjectEdit> processed = new ArrayList<>();
for (IrisObjectReplace replace : placement.getEdit()) {
ObjectEdit edit = edit(replace);
if (edit == null) {
continue;
}
if (edit.deterministic()) {
baked.add(edit);
} else {
processed.add(edit);
}
}
List<Double> scales = scales(placement);
IrisObjectRotation rotation = placement.getRotation() == null ? new IrisObjectRotation() : placement.getRotation();
Map<Double, Integer> xAngles = angles(rotation.getXAxis(), rotation.isEnabled());
if (placement.getMode() == ObjectPlaceMode.CEILING_HANG) {
xAngles = flipped(xAngles);
}
Map<Double, Integer> zAngles = angles(rotation.getZAxis(), rotation.isEnabled());
List<YChoice> yChoices = yChoices(angles(rotation.getYAxis(), rotation.isEnabled()));
List<VariantWarp> warps = warps(placement);
List<PlannedVariant> variants = new ArrayList<>();
boolean reshaped = false;
for (double scale : scales) {
for (Map.Entry<Double, Integer> x : xAngles.entrySet()) {
for (Map.Entry<Double, Integer> z : zAngles.entrySet()) {
for (YChoice y : yChoices) {
for (VariantWarp warp : warps) {
VariantSpec.Rotation spin = new VariantSpec.Rotation(x.getKey(), y.residue(), z.getKey());
VariantSpec spec = VariantSpec.builder()
.scale(scale)
.rotation(spin)
.warp(warp)
.snow(Math.max(0, Math.min(1, placement.getSnow())))
.edits(baked)
.preventLeafDecay(preventLeafDecay)
.build();
reshaped |= scale != 1 || !spin.none() || warp != null;
variants.add(new PlannedVariant(spec, y.quarters(), (long) x.getValue() * z.getValue() * y.weight()));
}
}
}
}
}
int planned = variants.size();
if (planned > MAX_VARIANTS) {
variants = subset(variants);
}
if (reshaped) {
diagnostics.add(new ExportDiagnostic(ExportDiagnostic.Severity.INFO, ExportLimit.OBJECT_VARIANT, resource,
"Rotation, scale and warp export as " + variants.size() + " fixed variants"
+ (planned > variants.size() ? " out of " + planned : "")));
}
return new Plan(variants, processed);
}
static Map<Double, Integer> angles(IrisAxisRotationClamp clamp, boolean enabled) {
Map<Double, Integer> angles = new TreeMap<>();
if (!enabled || clamp == null || !clamp.isEnabled()) {
angles.put(0.0, 1);
return angles;
}
if (clamp.isForceLock() || clamp.isLocked()) {
angles.put(normalize(clamp.getMax()), 1);
return angles;
}
boolean unlimited = clamp.isUnlimited();
double interval = unlimited ? Math.max(1, clamp.getInterval()) : clamp.getInterval() <= 0 ? 1 : clamp.getInterval();
double low = Math.min(clamp.getMin(), clamp.getMax());
double high = Math.max(clamp.getMin(), clamp.getMax());
for (int spin = 0; spin < SPIN_STEPS; spin++) {
double angle = (interval * Math.ceil(spin / interval)) % FULL_TURN;
if (!unlimited) {
angle = Math.max(low, Math.min(high, angle));
}
angles.merge(normalize(angle), 1, Integer::sum);
}
return angles;
}
static List<YChoice> yChoices(Map<Double, Integer> angles) {
Map<Double, Map<Integer, Integer>> residues = new TreeMap<>();
for (Map.Entry<Double, Integer> angle : angles.entrySet()) {
int quarter = (int) Math.floor(angle.getKey() / QUARTER) % 4;
double residue = round(angle.getKey() - quarter * QUARTER);
residues.computeIfAbsent(residue, ignored -> new TreeMap<>()).merge(quarter, angle.getValue(), Integer::sum);
}
List<YChoice> choices = new ArrayList<>();
for (Map.Entry<Double, Map<Integer, Integer>> residue : residues.entrySet()) {
Map<Integer, Integer> quarters = residue.getValue();
int first = quarters.values().iterator().next();
boolean even = true;
int total = 0;
for (int count : quarters.values()) {
even &= count == first;
total += count;
}
if (even) {
choices.add(new YChoice(residue.getKey(), List.copyOf(quarters.keySet()), total));
continue;
}
for (Map.Entry<Integer, Integer> quarter : quarters.entrySet()) {
choices.add(new YChoice(residue.getKey(), List.of(quarter.getKey()), quarter.getValue()));
}
}
return choices;
}
private List<Double> scales(IrisObjectPlacement placement) {
IrisObjectScale scale = placement.getScale();
if (scale == null) {
return List.of(dimension == null ? 1.0 : dimension.getAllObjectScaleFactor());
}
if (!scale.shouldScale()) {
return List.of(1.0);
}
if (scale.getSize() != 1) {
return List.of(scale.getSize());
}
if (scale.getMinimumScale() == scale.getMaximumScale()) {
return List.of(scale.getMinimumScale());
}
int count = Math.max(1, Math.min(scale.getVariations(), MAX_SCALE_VARIANTS));
List<Double> scales = new ArrayList<>(count);
double step = (scale.getMaximumScale() - scale.getMinimumScale()) / count;
for (int index = 0; index < count; index++) {
scales.add(scale.getMinimumScale() + step * index);
}
return scales;
}
private List<VariantWarp> warps(IrisObjectPlacement placement) {
IrisGeneratorStyle style = placement.getWarp();
List<VariantWarp> warps = new ArrayList<>(WARP_VARIANTS);
if (style == null || style.isFlat()) {
warps.add(null);
return warps;
}
String key = ContentIds.hash(GSON.toJson(style));
CNG generator = style.create(new RNG(ContentIds.seed(key)), data);
double half = style.getMultiplier() / 2;
WarpField field = (x, y, z) -> generator.fitDouble(-half, half, x, y, z);
for (int index = 0; index < WARP_VARIANTS; index++) {
warps.add(new VariantWarp(key, index, field));
}
return warps;
}
private static ObjectEdit edit(IrisObjectReplace replace) {
if (replace == null || replace.getReplace() == null || replace.getReplace().getPalette() == null) {
return null;
}
List<VanillaBlockState> find = new ArrayList<>();
for (IrisBlockData block : replace.getFind()) {
VanillaBlockState state = state(block);
if (state != null) {
find.add(state);
}
}
List<WeightedState> palette = new ArrayList<>();
for (IrisBlockData block : replace.getReplace().getPalette()) {
VanillaBlockState state = state(block);
if (state != null) {
palette.add(new WeightedState(state, Math.max(1, block.getWeight())));
}
}
if (find.isEmpty() || palette.isEmpty()) {
return null;
}
return new ObjectEdit(find, replace.isExact(), palette, Math.max(0, Math.min(1, replace.getChance())));
}
private static VanillaBlockState state(IrisBlockData block) {
if (block == null) {
return null;
}
try {
return VanillaBlockState.parse(block.stateKey());
} catch (IllegalArgumentException exception) {
return null;
}
}
private static Map<Double, Integer> flipped(Map<Double, Integer> angles) {
Map<Double, Integer> flipped = new TreeMap<>();
for (Map.Entry<Double, Integer> angle : angles.entrySet()) {
flipped.merge(normalize(angle.getKey() + FULL_TURN / 2), angle.getValue(), Integer::sum);
}
return flipped;
}
private static List<PlannedVariant> subset(List<PlannedVariant> variants) {
List<PlannedVariant> kept = new ArrayList<>(MAX_VARIANTS);
for (int index = 0; index < MAX_VARIANTS; index++) {
kept.add(variants.get((int) ((long) index * variants.size() / MAX_VARIANTS)));
}
return kept;
}
private static double normalize(double angle) {
return round(((angle % FULL_TURN) + FULL_TURN) % FULL_TURN);
}
private static double round(double value) {
return Math.round(value * ANGLE_PRECISION) / ANGLE_PRECISION;
}
record Plan(List<PlannedVariant> variants, List<ObjectEdit> processorEdits) {
}
record PlannedVariant(VariantSpec spec, List<Integer> quarters, long weight) {
}
record YChoice(double residue, List<Integer> quarters, int weight) {
}
}
@@ -0,0 +1,94 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import java.util.List;
import java.util.Objects;
public record VariantSpec(double scale, Rotation rotation, VariantWarp warp, double snow, List<ObjectEdit> edits,
boolean preventLeafDecay) {
public VariantSpec {
if (!Double.isFinite(scale) || scale <= 0) {
throw new IllegalArgumentException("Variant scale must be positive, got " + scale);
}
Objects.requireNonNull(rotation, "Variant rotation");
if (!Double.isFinite(snow) || snow < 0 || snow > 1) {
throw new IllegalArgumentException("Variant snow must be within [0, 1], got " + snow);
}
edits = List.copyOf(edits);
}
public static Builder builder() {
return new Builder();
}
public boolean reshapes() {
return scale != 1 || !rotation.none() || warp != null || snow > 0;
}
public String descriptor() {
StringBuilder builder = new StringBuilder()
.append("scale=").append(scale)
.append(";rotation=").append(rotation.x()).append(',').append(rotation.y()).append(',').append(rotation.z())
.append(";warp=").append(warp == null ? "none" : warp.key() + "#" + warp.index())
.append(";snow=").append(snow)
.append(";leaves=").append(preventLeafDecay)
.append(";edits=");
for (ObjectEdit edit : edits) {
builder.append('[').append(edit.descriptor()).append(']');
}
return builder.toString();
}
public record Rotation(double x, double y, double z) {
public static final Rotation NONE = new Rotation(0, 0, 0);
public boolean none() {
return x == 0 && y == 0 && z == 0;
}
}
public static final class Builder {
private double scale = 1;
private Rotation rotation = Rotation.NONE;
private VariantWarp warp;
private double snow;
private List<ObjectEdit> edits = List.of();
private boolean preventLeafDecay = true;
private Builder() {
}
public Builder scale(double value) {
scale = value;
return this;
}
public Builder rotation(Rotation value) {
rotation = Objects.requireNonNull(value);
return this;
}
public Builder warp(VariantWarp value) {
warp = value;
return this;
}
public Builder snow(double value) {
snow = value;
return this;
}
public Builder edits(List<ObjectEdit> value) {
edits = Objects.requireNonNull(value);
return this;
}
public Builder preventLeafDecay(boolean value) {
preventLeafDecay = value;
return this;
}
public VariantSpec build() {
return new VariantSpec(scale, rotation, warp, snow, edits, preventLeafDecay);
}
}
}
@@ -0,0 +1,13 @@
package art.arcane.iris.pack.datapack.export.compile.object;
import java.util.Objects;
public record VariantWarp(String key, int index, WarpField field) {
public VariantWarp {
Objects.requireNonNull(key, "Warp key");
Objects.requireNonNull(field, "Warp field");
if (index < 0) {
throw new IllegalArgumentException("Warp variant index must not be negative, got " + index);
}
}
}
Loaded 100 of 385 files, more files were not shown because too many files have changed in this diff. Show more