mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
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:
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));
|
||||
}
|
||||
}
|
||||
+17
-16
@@ -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;
|
||||
}
|
||||
}
|
||||
+53
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+186
@@ -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
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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()) {
|
||||
+104
@@ -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 + "'");
|
||||
}
|
||||
}
|
||||
+170
@@ -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);
|
||||
}
|
||||
}
|
||||
+128
@@ -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();
|
||||
}
|
||||
}
|
||||
+390
@@ -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);
|
||||
}
|
||||
}
|
||||
+141
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+299
@@ -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) {
|
||||
}
|
||||
}
|
||||
+299
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
+192
@@ -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());
|
||||
}
|
||||
}
|
||||
+239
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+162
@@ -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);
|
||||
}
|
||||
}
|
||||
+115
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+184
@@ -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) {
|
||||
}
|
||||
}
|
||||
+119
@@ -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;
|
||||
}
|
||||
}
|
||||
+60
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+294
@@ -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) {
|
||||
}
|
||||
}
|
||||
+185
@@ -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());
|
||||
}
|
||||
}
|
||||
+78
@@ -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);
|
||||
}
|
||||
}
|
||||
+114
@@ -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);
|
||||
}
|
||||
}
|
||||
+69
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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());
|
||||
}
|
||||
}
|
||||
+331
@@ -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) {
|
||||
}
|
||||
}
|
||||
+261
@@ -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);
|
||||
}
|
||||
}
|
||||
+114
@@ -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();
|
||||
}
|
||||
}
|
||||
+15
@@ -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
|
||||
}
|
||||
+283
@@ -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;
|
||||
}
|
||||
}
|
||||
+105
@@ -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);
|
||||
}
|
||||
}
|
||||
+319
@@ -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) {
|
||||
}
|
||||
}
|
||||
+29
@@ -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") + "/";
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package art.arcane.iris.pack.datapack.export.compile.noise;
|
||||
|
||||
public enum CellularMode {
|
||||
GRID_3X3,
|
||||
QUADRANT_2X2,
|
||||
GRID_3X3X3
|
||||
}
|
||||
+15
@@ -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");
|
||||
}
|
||||
}
|
||||
+9
@@ -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);
|
||||
}
|
||||
+71
@@ -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;
|
||||
}
|
||||
}
|
||||
+397
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+173
@@ -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));
|
||||
}
|
||||
}
|
||||
+224
@@ -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) {
|
||||
}
|
||||
}
|
||||
+101
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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);
|
||||
}
|
||||
}
|
||||
+550
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+194
@@ -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;
|
||||
}
|
||||
}
|
||||
+13
@@ -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;
|
||||
}
|
||||
}
|
||||
+86
@@ -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;
|
||||
}
|
||||
}
|
||||
+376
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+221
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
+82
@@ -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));
|
||||
}
|
||||
}
|
||||
+416
@@ -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) {
|
||||
}
|
||||
}
|
||||
+65
@@ -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;
|
||||
}
|
||||
}
|
||||
+429
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
+328
@@ -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) {
|
||||
}
|
||||
}
|
||||
+396
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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
|
||||
}
|
||||
}
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+263
@@ -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) {
|
||||
}
|
||||
}
|
||||
+94
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -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
Reference in new issue
Block a user