diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java
index 486cb5a37..ffdb4e1f5 100644
--- a/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java
+++ b/core/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java
@@ -26,6 +26,7 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.runtime.ChunkClearer;
+import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.runtime.InPlaceChunkRegenerator;
import art.arcane.iris.core.service.IrisEngineSVC;
import art.arcane.iris.core.service.StudioSVC;
@@ -361,4 +362,49 @@ public class CommandDeveloper implements DirectorExecutor {
new InPlaceChunkRegenerator(world, engine, sender(), centerX, centerZ, radius).start();
}
+ @Director(name = "goldenhash", aliases = {"gold"}, description = "Generate chunks into buffers (no world writes) and hash blocks+biomes; captures a golden file or verifies against an existing one. Resets mantle in the scanned area - use on disposable test worlds.", origin = DirectorOrigin.BOTH)
+ public void goldenhash(
+ @Param(description = "The world to scan", contextual = true)
+ World world,
+ @Param(name = "radius", description = "Radius in chunks around the center", defaultValue = "8")
+ int radius,
+ @Param(name = "center-x", description = "Center chunk X", defaultValue = "0")
+ int centerX,
+ @Param(name = "center-z", description = "Center chunk Z", defaultValue = "0")
+ int centerZ,
+ @Param(name = "reset-mantle", description = "Delete mantle data in the scan area first for full regeneration from scratch", defaultValue = "true")
+ boolean resetMantle,
+ @Param(name = "threads", description = "Concurrent chunk generations; 1 = strictly serial for order-dependence testing", defaultValue = "8")
+ int threads,
+ @Param(name = "deep", description = "Also dump full per-chunk non-air blockstates for offline diffing", defaultValue = "false")
+ boolean deep
+ ) {
+ if (radius < 0) {
+ sender().sendMessage(C.RED + "Radius must be 0 or greater.");
+ return;
+ }
+
+ if (world == null || !IrisToolbelt.isIrisWorld(world)) {
+ sender().sendMessage(C.RED + "Target must be an Iris world.");
+ return;
+ }
+
+ PlatformChunkGenerator access = IrisToolbelt.access(world);
+ if (access == null || access.getEngine() == null) {
+ sender().sendMessage(C.RED + "The engine access for this world is null.");
+ return;
+ }
+
+ int chunks = (radius * 2 + 1) * (radius * 2 + 1);
+ sender().sendMessage(C.GREEN + "GoldenHash started: " + C.GOLD + chunks + C.GREEN
+ + " chunk(s) around " + C.GOLD + centerX + "," + centerZ + C.GREEN
+ + " in buffers (world untouched).");
+ Iris.info("goldenhash start: world=" + world.getName()
+ + " center=" + centerX + "," + centerZ
+ + " radius=" + radius
+ + " chunks=" + chunks);
+
+ new GoldenHashScanner(world, access.getEngine(), sender(), centerX, centerZ, radius, resetMantle, threads, deep).start();
+ }
+
}
diff --git a/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java b/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java
new file mode 100644
index 000000000..186b75cd8
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/core/runtime/GoldenHashScanner.java
@@ -0,0 +1,443 @@
+/*
+ * 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 .
+ */
+
+package art.arcane.iris.core.runtime;
+
+import art.arcane.iris.Iris;
+import art.arcane.iris.engine.data.chunk.TerrainChunk;
+import art.arcane.iris.engine.framework.Engine;
+import art.arcane.iris.engine.mantle.EngineMantle;
+import art.arcane.iris.util.common.format.C;
+import art.arcane.iris.util.common.parallel.MultiBurst;
+import art.arcane.iris.util.common.plugin.VolmitSender;
+import art.arcane.volmlib.util.mantle.runtime.Mantle;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.block.Biome;
+import org.bukkit.block.data.BlockData;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HexFormat;
+import java.util.IdentityHashMap;
+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 GoldenHashScanner {
+ private static final AtomicInteger ACTIVE_SCANS = new AtomicInteger(0);
+ private static final String FORMAT = "iris-goldenhash v1";
+
+ public static boolean isScanActive() {
+ return ACTIVE_SCANS.get() > 0;
+ }
+ private static final int BIOME_STEP = 4;
+ private static final int MAX_REPORTED_MISMATCHES = 10;
+ private static final byte[] NULL_BIOME = "null\n".getBytes(StandardCharsets.UTF_8);
+
+ private final World world;
+ private final Engine engine;
+ private final VolmitSender sender;
+ private final int centerChunkX;
+ private final int centerChunkZ;
+ private final int radius;
+ private final boolean resetMantle;
+ private final int threads;
+ private final boolean deep;
+ private final File goldenFile;
+ private final ChunkJobReporter reporter;
+
+ public GoldenHashScanner(World world, Engine engine, VolmitSender sender, int centerChunkX, int centerChunkZ, int radius, boolean resetMantle, int threads, boolean deep) {
+ this.world = world;
+ this.engine = engine;
+ this.sender = sender;
+ this.centerChunkX = centerChunkX;
+ this.centerChunkZ = centerChunkZ;
+ this.radius = Math.max(0, radius);
+ this.resetMantle = resetMantle;
+ this.threads = Math.max(1, threads);
+ this.deep = deep;
+ this.goldenFile = Iris.instance.getDataFile("golden", engine.getDimension().getLoadKey()
+ + "-s" + world.getSeed()
+ + "-c" + centerChunkX + "x" + centerChunkZ
+ + "-r" + this.radius + ".hashes");
+ this.reporter = new ChunkJobReporter(sender, "GoldenHash", world);
+ }
+
+ public void start() {
+ reporter.start();
+ Thread thread = new Thread(this::run, "Iris GoldenHash");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ private void run() {
+ boolean error = false;
+ ACTIVE_SCANS.incrementAndGet();
+ try {
+ if (resetMantle) {
+ reporter.setStage("Resetting mantle");
+ resetMantleFull();
+ }
+
+ List targets = ChunkJobReporter.orderedTargets(centerChunkX, centerChunkZ, radius);
+ reporter.setTotal(targets.size());
+ reporter.setStage("Generating");
+ Map lines = scan(targets);
+
+ if (lines.size() != targets.size()) {
+ sender.sendMessage(C.RED + "GoldenHash aborted: " + (targets.size() - lines.size()) + " chunk(s) failed to generate. No golden file written.");
+ error = true;
+ return;
+ }
+
+ reporter.setStage("Comparing");
+ if (goldenFile.exists()) {
+ error = !verify(lines);
+ } else {
+ capture(lines);
+ }
+ } catch (Throwable e) {
+ Iris.reportError(e);
+ error = true;
+ } finally {
+ ACTIVE_SCANS.decrementAndGet();
+ reporter.finish(error);
+ }
+ }
+
+ private void resetMantleFull() {
+ Mantle mantle = engine.getMantle().getMantle();
+ mantle.saveAll();
+ File folder = mantle.getDataFolder();
+ File[] files = folder.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile()) {
+ file.delete();
+ }
+ }
+ }
+ }
+
+ private Map scan(List targets) throws InterruptedException {
+ Map lines = new ConcurrentHashMap<>();
+ Semaphore inFlight = new Semaphore(threads);
+ CountDownLatch done = new CountDownLatch(targets.size());
+
+ for (int[] target : targets) {
+ int chunkX = target[0];
+ int chunkZ = target[1];
+
+ inFlight.acquire();
+ MultiBurst.burst.lazy(() -> {
+ boolean ok = false;
+ try {
+ TerrainChunk buffer = TerrainChunk.create(world);
+ engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
+ lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, buffer));
+ if (deep) {
+ writeDeepDump(chunkX, chunkZ, buffer);
+ }
+ ok = true;
+ } catch (Throwable e) {
+ Iris.reportError(e);
+ } finally {
+ reporter.countApplied(ok);
+ inFlight.release();
+ done.countDown();
+ }
+ });
+ }
+
+ done.await();
+ return lines;
+ }
+
+ private String hashChunk(int chunkX, int chunkZ, TerrainChunk buffer) {
+ MessageDigest blockDigest = sha256();
+ MessageDigest biomeDigest = sha256();
+ int minY = buffer.getMinHeight();
+ int maxY = buffer.getMaxHeight();
+ IdentityHashMap blockCache = new IdentityHashMap<>();
+ Map biomeCache = new HashMap<>();
+
+ for (int x = 0; x < 16; x++) {
+ for (int z = 0; z < 16; z++) {
+ for (int y = minY; y < maxY; y++) {
+ BlockData data = buffer.getBlockData(x, y, z);
+ byte[] bytes = blockCache.computeIfAbsent(data, d -> (d.getAsString() + "\n").getBytes(StandardCharsets.UTF_8));
+ blockDigest.update(bytes);
+ }
+ }
+ }
+
+ for (int x = 0; x < 16; x += BIOME_STEP) {
+ for (int z = 0; z < 16; z += BIOME_STEP) {
+ for (int y = minY; y < maxY; y += BIOME_STEP) {
+ Biome biome = buffer.getBiome(x, y, z);
+ byte[] bytes = biome == null
+ ? NULL_BIOME
+ : biomeCache.computeIfAbsent(biome, b -> (biomeKey(b) + "\n").getBytes(StandardCharsets.UTF_8));
+ biomeDigest.update(bytes);
+ }
+ }
+ }
+
+ return chunkX + " " + chunkZ + " "
+ + HexFormat.of().formatHex(blockDigest.digest()) + " "
+ + HexFormat.of().formatHex(biomeDigest.digest());
+ }
+
+ private void capture(Map lines) throws IOException {
+ List body = orderedBody(lines);
+ String combined = combinedHash(body);
+ List out = new ArrayList<>();
+ out.add("#" + FORMAT);
+ out.add("#world=" + world.getName());
+ out.add("#dim=" + engine.getDimension().getLoadKey());
+ out.add("#seed=" + world.getSeed());
+ out.add("#mc=" + Bukkit.getBukkitVersion());
+ out.add("#minY=" + world.getMinHeight() + " maxY=" + world.getMaxHeight());
+ out.add("#center=" + centerChunkX + "," + centerChunkZ);
+ out.add("#radius=" + radius);
+ out.addAll(body);
+ out.add("#combined=" + combined);
+ Files.write(goldenFile.toPath(), out, StandardCharsets.UTF_8);
+
+ sender.sendMessage(C.GREEN + "Golden captured: " + C.GOLD + body.size() + " chunks" + C.GREEN + " combined=" + C.GOLD + shortHash(combined));
+ sender.sendMessage(C.GRAY + goldenFile.getAbsolutePath());
+ Iris.info("goldenhash captured: " + goldenFile.getAbsolutePath() + " combined=" + combined);
+ }
+
+ private boolean verify(Map lines) throws IOException {
+ List existing = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8);
+ Map meta = new HashMap<>();
+ Map goldenChunks = new HashMap<>();
+ for (String line : existing) {
+ if (line.startsWith("#")) {
+ int eq = line.indexOf('=');
+ if (eq > 0) {
+ meta.put(line.substring(1, eq), line.substring(eq + 1));
+ }
+ } else if (!line.isBlank()) {
+ int second = line.indexOf(' ', line.indexOf(' ') + 1);
+ goldenChunks.put(line.substring(0, second), line);
+ }
+ }
+
+ String expectedSeed = String.valueOf(world.getSeed());
+ String expectedDim = engine.getDimension().getLoadKey();
+ if (!expectedSeed.equals(meta.get("seed")) || !expectedDim.equals(meta.get("dim"))) {
+ sender.sendMessage(C.RED + "Golden file is for dim=" + meta.get("dim") + " seed=" + meta.get("seed")
+ + " but this world is dim=" + expectedDim + " seed=" + expectedSeed + ". Aborting.");
+ return false;
+ }
+ String mc = Bukkit.getBukkitVersion();
+ if (!mc.equals(meta.get("mc"))) {
+ sender.sendMessage(C.YELLOW + "Golden was captured on mc=" + meta.get("mc") + ", running mc=" + mc + ". Diffs may be version-induced.");
+ }
+
+ List body = orderedBody(lines);
+ List mismatches = new ArrayList<>();
+ for (String line : body) {
+ int second = line.indexOf(' ', line.indexOf(' ') + 1);
+ String key = line.substring(0, second);
+ String golden = goldenChunks.get(key);
+ if (!line.equals(golden)) {
+ mismatches.add(key + (golden == null ? " (missing in golden)" : ""));
+ }
+ }
+
+ String combined = combinedHash(body);
+ if (mismatches.isEmpty()) {
+ sender.sendMessage(C.GREEN + "GOLDEN MATCH: " + C.GOLD + body.size() + "/" + goldenChunks.size() + C.GREEN
+ + " chunks, combined=" + C.GOLD + shortHash(combined));
+ Iris.info("goldenhash MATCH: " + goldenFile.getName() + " combined=" + combined);
+ return true;
+ }
+
+ sender.sendMessage(C.RED + "GOLDEN MISMATCH: " + mismatches.size() + "/" + body.size() + " chunks differ.");
+ for (int i = 0; i < Math.min(MAX_REPORTED_MISMATCHES, mismatches.size()); i++) {
+ sender.sendMessage(C.RED + " chunk " + mismatches.get(i));
+ }
+ if (mismatches.size() > MAX_REPORTED_MISMATCHES) {
+ sender.sendMessage(C.RED + " ... and " + (mismatches.size() - MAX_REPORTED_MISMATCHES) + " more");
+ }
+
+ File current = new File(goldenFile.getParentFile(), goldenFile.getName() + ".new");
+ List out = new ArrayList<>(body);
+ out.add("#combined=" + combined);
+ Files.write(current.toPath(), out, StandardCharsets.UTF_8);
+ sender.sendMessage(C.YELLOW + "Current hashes written to " + current.getName());
+ Iris.info("goldenhash MISMATCH: " + mismatches.size() + "/" + body.size() + " -> " + current.getAbsolutePath());
+
+ reporter.setStage("Diagnosing");
+ diagnose(mismatches.getFirst());
+ return false;
+ }
+
+ private void diagnose(String mismatchKey) {
+ try {
+ String[] parts = mismatchKey.trim().split(" ");
+ int chunkX = Integer.parseInt(parts[0]);
+ int chunkZ = Integer.parseInt(parts[1]);
+
+ TerrainChunk first = TerrainChunk.create(world);
+ engine.generate(chunkX << 4, chunkZ << 4, first, false);
+ TerrainChunk second = TerrainChunk.create(world);
+ engine.generate(chunkX << 4, chunkZ << 4, second, false);
+
+ int minY = first.getMinHeight();
+ int maxY = first.getMaxHeight();
+ List diffs = new ArrayList<>();
+ for (int x = 0; x < 16 && diffs.size() < 50; x++) {
+ for (int z = 0; z < 16 && diffs.size() < 50; z++) {
+ for (int y = minY; y < maxY && diffs.size() < 50; y++) {
+ String a = first.getBlockData(x, y, z).getAsString();
+ String b = second.getBlockData(x, y, z).getAsString();
+ if (!a.equals(b)) {
+ diffs.add(x + " " + y + " " + z + " | " + a + " | " + b);
+ }
+ }
+ }
+ }
+
+ EngineMantle engineMantle = engine.getMantle();
+ int margin = Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
+ for (int dx = -margin; dx <= margin; dx++) {
+ for (int dz = -margin; dz <= margin; dz++) {
+ engineMantle.getMantle().deleteChunk(chunkX + dx, chunkZ + dz);
+ }
+ }
+ TerrainChunk reset = TerrainChunk.create(world);
+ engine.generate(chunkX << 4, chunkZ << 4, reset, false);
+ List mantleDiffs = new ArrayList<>();
+ for (int x = 0; x < 16 && mantleDiffs.size() < 80; x++) {
+ for (int z = 0; z < 16 && mantleDiffs.size() < 80; z++) {
+ for (int y = minY; y < maxY && mantleDiffs.size() < 80; y++) {
+ String a = first.getBlockData(x, y, z).getAsString();
+ String b = reset.getBlockData(x, y, z).getAsString();
+ if (!a.equals(b)) {
+ mantleDiffs.add(x + " " + y + " " + z + " | scan: " + a + " | mantle-reset: " + b);
+ }
+ }
+ }
+ }
+
+ List report = new ArrayList<>();
+ report.add("#goldenhash diagnosis chunk=" + chunkX + "," + chunkZ);
+ report.add("#repeat-generation: " + (diffs.isEmpty() ? "STABLE (nondeterminism is order/state-dependent, not per-call)" : "UNSTABLE (" + diffs.size() + "+ diffs between two back-to-back generations)"));
+ report.addAll(diffs);
+ report.add("#mantle-reset regeneration: " + (mantleDiffs.isEmpty() ? "STABLE (mantle rebuild reproduces scan output)" : "DIVERGED (" + mantleDiffs.size() + "+ diffs - mantle build is state/order dependent)"));
+ report.addAll(mantleDiffs);
+ report.add("#full non-air dump of generation 1 follows (x y z state)");
+ for (int x = 0; x < 16; x++) {
+ for (int z = 0; z < 16; z++) {
+ for (int y = minY; y < maxY; y++) {
+ String state = first.getBlockData(x, y, z).getAsString();
+ if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
+ report.add(x + " " + y + " " + z + " " + state);
+ }
+ }
+ }
+ }
+
+ File diag = new File(goldenFile.getParentFile(), goldenFile.getName() + ".diag-c" + chunkX + "x" + chunkZ + ".txt");
+ Files.write(diag.toPath(), report, StandardCharsets.UTF_8);
+ sender.sendMessage((diffs.isEmpty() ? C.YELLOW + "Repeat-gen STABLE" : C.RED + "Repeat-gen UNSTABLE (" + diffs.size() + "+ block diffs)")
+ + C.GRAY + ", " + (mantleDiffs.isEmpty() ? C.YELLOW + "mantle-reset STABLE" : C.RED + "mantle-reset DIVERGED (" + mantleDiffs.size() + "+ diffs)")
+ + C.GRAY + " -> " + diag.getName());
+ Iris.info("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath());
+ } catch (Throwable e) {
+ Iris.reportError(e);
+ sender.sendMessage(C.RED + "Diagnosis failed: " + e.getMessage());
+ }
+ }
+
+ private List orderedBody(Map lines) {
+ Map sorted = new TreeMap<>(lines);
+ return new ArrayList<>(sorted.values());
+ }
+
+ private String combinedHash(List body) {
+ MessageDigest digest = sha256();
+ for (String line : body) {
+ digest.update((line + "\n").getBytes(StandardCharsets.UTF_8));
+ }
+ return HexFormat.of().formatHex(digest.digest());
+ }
+
+ private void writeDeepDump(int chunkX, int chunkZ, TerrainChunk buffer) throws IOException {
+ File dir = new File(goldenFile.getParentFile(), goldenFile.getName() + (goldenFile.exists() ? ".deep-verify" : ".deep"));
+ dir.mkdirs();
+ int minY = buffer.getMinHeight();
+ int maxY = buffer.getMaxHeight();
+ List out = new ArrayList<>();
+ for (int x = 0; x < 16; x++) {
+ for (int z = 0; z < 16; z++) {
+ for (int y = minY; y < maxY; y++) {
+ String state = buffer.getBlockData(x, y, z).getAsString();
+ if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
+ out.add(x + " " + y + " " + z + " " + state);
+ }
+ }
+ }
+ }
+ Files.write(new File(dir, chunkX + "_" + chunkZ + ".txt").toPath(), out, StandardCharsets.UTF_8);
+ }
+
+ private static String biomeKey(Biome biome) {
+ for (String method : new String[]{"getKeyOrNull", "getKeyOrThrow", "getKey"}) {
+ try {
+ Object key = Biome.class.getMethod(method).invoke(biome);
+ if (key != null) {
+ return key.toString();
+ }
+ } catch (Throwable ignored) {
+ }
+ }
+ return biome.toString();
+ }
+
+ private static String shortHash(String hex) {
+ return hex.substring(0, 12);
+ }
+
+ private static long chunkKey(int x, int z) {
+ return (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
+ }
+
+ private static MessageDigest sha256() {
+ try {
+ return MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java
index c01f923db..8fab1f93d 100644
--- a/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java
+++ b/core/src/main/java/art/arcane/iris/core/service/IrisEngineSVC.java
@@ -4,6 +4,7 @@ import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.ResourceLoader;
+import art.arcane.iris.core.runtime.GoldenHashScanner;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -310,6 +311,9 @@ public class IrisEngineSVC implements IrisService {
if (shouldSkipForMaintenance(engineWorld)) {
return;
}
+ if (GoldenHashScanner.isScanActive()) {
+ return;
+ }
try {
engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine));
@@ -338,6 +342,10 @@ public class IrisEngineSVC implements IrisService {
return;
}
+ if (GoldenHashScanner.isScanActive()) {
+ return;
+ }
+
try {
long unloadStart = System.currentTimeMillis();
int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engine));
diff --git a/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java b/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java
new file mode 100644
index 000000000..6aa09932a
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java
@@ -0,0 +1,126 @@
+/*
+ * 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 .
+ */
+
+package art.arcane.iris.engine;
+
+import art.arcane.iris.Iris;
+import art.arcane.iris.core.loader.IrisData;
+import art.arcane.iris.engine.framework.Engine;
+import art.arcane.iris.engine.object.IrisBiome;
+import art.arcane.iris.engine.object.IrisDecorator;
+import art.arcane.iris.engine.object.IrisObjectPlacement;
+import art.arcane.iris.engine.object.IrisOreGenerator;
+import art.arcane.iris.engine.object.IrisProceduralObjects;
+import art.arcane.iris.engine.object.IrisProceduralPlacement;
+import art.arcane.iris.engine.object.IrisRegion;
+import art.arcane.volmlib.util.collection.KList;
+import art.arcane.volmlib.util.math.M;
+import art.arcane.volmlib.util.math.RNG;
+
+import java.util.Comparator;
+
+public final class GenerationCacheWarmer {
+ private GenerationCacheWarmer() {
+ }
+
+ public static void warm(Engine engine) {
+ long start = M.ms();
+ IrisData data = engine.getData();
+ RNG root = new RNG(engine.getSeedManager().getComponent() + 7777L);
+ int[] counter = {0};
+
+ try {
+ KList biomes = engine.getAllBiomes();
+ biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
+ for (IrisBiome biome : biomes) {
+ warmPlacements(biome.getObjects(), root, counter, data);
+ warmDecorators(biome.getDecorators(), root, counter, data);
+ warmOres(biome.getOres(), root, counter, data);
+ warmProcedural(biome.getProceduralObjects(), root, counter, data);
+ }
+
+ KList regions = engine.getDimension().getAllRegions(engine);
+ regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
+ for (IrisRegion region : regions) {
+ warmPlacements(region.getObjects(), root, counter, data);
+ warmOres(region.getOres(), root, counter, data);
+ warmProcedural(region.getProceduralObjects(), root, counter, data);
+ }
+
+ warmOres(engine.getDimension().getOres(), root, counter, data);
+ } catch (Throwable e) {
+ Iris.reportError(e);
+ Iris.warn("Generation cache warm pass failed: " + e.getMessage());
+ }
+
+ Iris.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
+ }
+
+ private static void warmPlacements(KList placements, RNG root, int[] counter, IrisData data) {
+ if (placements == null) {
+ return;
+ }
+ for (IrisObjectPlacement placement : placements) {
+ if (placement == null) {
+ continue;
+ }
+ RNG rng = root.nextParallelRNG(counter[0]++);
+ placement.getSurfaceWarp(rng, data);
+ placement.getDensity(rng, 0, 0, data);
+ }
+ }
+
+ private static void warmDecorators(KList decorators, RNG root, int[] counter, IrisData data) {
+ if (decorators == null) {
+ return;
+ }
+ for (IrisDecorator decorator : decorators) {
+ if (decorator == null) {
+ continue;
+ }
+ RNG rng = root.nextParallelRNG(counter[0]++);
+ decorator.getHeightGenerator(rng, data);
+ decorator.getGenerator(rng, data);
+ decorator.getVarianceGenerator(rng, data);
+ }
+ }
+
+ private static void warmOres(KList ores, RNG root, int[] counter, IrisData data) {
+ if (ores == null) {
+ return;
+ }
+ for (IrisOreGenerator ore : ores) {
+ if (ore == null) {
+ continue;
+ }
+ ore.warm(root.nextParallelRNG(counter[0]++), data);
+ }
+ }
+
+ private static void warmProcedural(IrisProceduralObjects procedural, RNG root, int[] counter, IrisData data) {
+ if (procedural == null) {
+ return;
+ }
+ for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
+ if (placement == null) {
+ continue;
+ }
+ placement.getVariantObject(data, root.nextParallelRNG(counter[0]++));
+ }
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java
index 1f50b2735..39fe0f686 100644
--- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java
+++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java
@@ -161,6 +161,9 @@ public class IrisEngine implements Engine {
_t0 = M.ms();
setupEngine();
Iris.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
+ _t0 = M.ms();
+ GenerationCacheWarmer.warm(this);
+ Iris.debug("[IrisEngine timing] cache warm total=" + (M.ms() - _t0) + "ms");
Iris.debug("Engine Initialized " + getCacheID());
}
diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java
index 1cb429872..1f2c1ab59 100644
--- a/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java
+++ b/core/src/main/java/art/arcane/iris/engine/mantle/MatterGenerator.java
@@ -92,7 +92,7 @@ public interface MatterGenerator {
int finalPassZ = passZ;
MantleChunk finalChunk = chunk;
MantleComponent finalComponent = component;
- Runnable task = () -> finalChunk.raiseFlagUnchecked(finalComponent.getFlag(),
+ Runnable task = () -> finalChunk.raiseFlagSuspend(finalComponent.getFlag(),
() -> finalComponent.generateLayer(writer, finalPassX, finalPassZ, context));
if (multicore) {
diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java
index 5804242b9..9d04f9e76 100644
--- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java
+++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java
@@ -34,6 +34,8 @@ import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D;
+import art.arcane.iris.util.simd.SimdKernels;
+import art.arcane.iris.util.simd.SimdSupport;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
@@ -162,9 +164,11 @@ public class MantleCarvingComponent extends IrisMantleComponent {
IrisCaveProfile[] profileField = blendScratch.profileField;
Map columnProfileWeights = blendScratch.columnProfileWeights;
IdentityHashMap activeProfiles = blendScratch.activeProfiles;
+ List profileOrder = blendScratch.profileOrder;
IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles;
double[] kernelProfileWeights = blendScratch.kernelProfileWeights;
activeProfiles.clear();
+ profileOrder.clear();
fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
@@ -217,34 +221,31 @@ public class MantleCarvingComponent extends IrisMantleComponent {
} else if (!activeProfiles.containsKey(profile)) {
Arrays.fill(weights, 0D);
}
- activeProfiles.put(profile, Boolean.TRUE);
+ if (activeProfiles.put(profile, Boolean.TRUE) == null) {
+ profileOrder.add(profile);
+ }
weights[columnIndex] = columnWeight;
}
}
}
List columnWeightedProfiles = new ArrayList<>();
- for (IrisCaveProfile profile : activeProfiles.keySet()) {
+ for (IrisCaveProfile profile : profileOrder) {
double[] weights = columnProfileWeights.get(profile);
if (weights == null) {
continue;
}
- double totalWeight = 0D;
- double maxWeight = 0D;
- for (double weight : weights) {
- totalWeight += weight;
- if (weight > maxWeight) {
- maxWeight = weight;
- }
- }
+ SimdKernels kernels = SimdSupport.kernels();
+ double totalWeight = kernels.sum(weights, weights.length);
+ double maxWeight = kernels.max(weights, weights.length);
if (maxWeight < MIN_WEIGHT) {
continue;
}
double averageWeight = totalWeight / CHUNK_AREA;
- columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null));
+ columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null, columnWeightedProfiles.size()));
}
List blendedProfiles = limitAndMergeBlendedProfiles(columnWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, CHUNK_AREA);
@@ -277,6 +278,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState);
Map rootProfileColumnWeights = new IdentityHashMap<>();
+ List rootProfileOrder = new ArrayList<>();
IrisRange worldYRange = entry.getWorldYRange();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex];
@@ -290,14 +292,17 @@ public class MantleCarvingComponent extends IrisMantleComponent {
continue;
}
- double[] columnWeights = rootProfileColumnWeights.computeIfAbsent(profile, key -> new double[CHUNK_AREA]);
+ double[] columnWeights = rootProfileColumnWeights.get(profile);
+ if (columnWeights == null) {
+ columnWeights = new double[CHUNK_AREA];
+ rootProfileColumnWeights.put(profile, columnWeights);
+ rootProfileOrder.add(profile);
+ }
columnWeights[columnIndex] = 1D;
}
- List> profileEntries = new ArrayList<>(rootProfileColumnWeights.entrySet());
- profileEntries.sort((a, b) -> Integer.compare(a.getKey().hashCode(), b.getKey().hashCode()));
- for (Map.Entry profileEntry : profileEntries) {
- weightedProfiles.add(new WeightedProfile(profileEntry.getKey(), profileEntry.getValue(), -1D, worldYRange));
+ for (IrisCaveProfile profile : rootProfileOrder) {
+ weightedProfiles.add(new WeightedProfile(profile, rootProfileColumnWeights.get(profile), -1D, worldYRange, weightedProfiles.size()));
}
}
@@ -545,7 +550,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
List mergedProfiles = new ArrayList<>();
for (WeightedProfile keptProfile : keptProfiles) {
double averageWeight = computeAverageWeight(keptProfile.columnWeights, areaSize);
- mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange));
+ mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange, keptProfile.sequence));
}
mergedProfiles.sort(MantleCarvingComponent::compareByCarveOrder);
return mergedProfiles;
@@ -556,7 +561,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
if (weightOrder != 0) {
return weightOrder;
}
- return Integer.compare(profileSortKey(a.profile), profileSortKey(b.profile));
+ return Integer.compare(a.sequence, b.sequence);
}
private static int compareByCarveOrder(WeightedProfile a, WeightedProfile b) {
@@ -564,14 +569,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
if (weightOrder != 0) {
return weightOrder;
}
- return Integer.compare(profileSortKey(a.profile), profileSortKey(b.profile));
- }
-
- private static int profileSortKey(IrisCaveProfile profile) {
- if (profile == null) {
- return 0;
- }
- return profile.hashCode();
+ return Integer.compare(a.sequence, b.sequence);
}
private static double computeAverageWeight(double[] weights) {
@@ -607,12 +605,14 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private final double[] columnWeights;
private final double averageWeight;
private final IrisRange worldYRange;
+ private final int sequence;
- private WeightedProfile(IrisCaveProfile profile, double[] columnWeights, double averageWeight, IrisRange worldYRange) {
+ private WeightedProfile(IrisCaveProfile profile, double[] columnWeights, double averageWeight, IrisRange worldYRange, int sequence) {
this.profile = profile;
this.columnWeights = columnWeights;
this.averageWeight = averageWeight;
this.worldYRange = worldYRange;
+ this.sequence = sequence;
}
private double averageWeight() {
@@ -627,6 +627,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private final IdentityHashMap columnProfileWeights = new IdentityHashMap<>();
private final IdentityHashMap dimensionColumnPlans = new IdentityHashMap<>();
private final IdentityHashMap activeProfiles = new IdentityHashMap<>();
+ private final List profileOrder = new ArrayList<>();
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final IrisRegion[] fieldRegions = new IrisRegion[FIELD_SIZE * FIELD_SIZE];
private final IrisBiome[] fieldSurfaceBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java
index 77155305a..062049e56 100644
--- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java
+++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java
@@ -1381,8 +1381,10 @@ public class MantleObjectComponent extends IrisMantleComponent {
vacuumPlacements.add(j);
}
}
+ updateProceduralRadiusBounds(region.getProceduralObjects(), xg, zg);
}
for (IrisBiome biome : dimension.getAllBiomes(this::getData)) {
+ updateProceduralRadiusBounds(biome.getProceduralObjects(), xg, zg);
for (IrisObjectPlacement j : biome.getObjects()) {
if (j.getScale().canScaleBeyond()) {
scalars.put(j.getScale(), j.getPlace());
@@ -1414,6 +1416,24 @@ public class MantleObjectComponent extends IrisMantleComponent {
return Math.max(xg.get(), zg.get());
}
+ private void updateProceduralRadiusBounds(IrisProceduralObjects procedural, AtomicInteger xg, AtomicInteger zg) {
+ if (procedural == null || procedural.isEmpty()) {
+ return;
+ }
+ for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
+ if (placement == null) {
+ continue;
+ }
+ for (IrisObject variant : placement.getVariantObjects(getData())) {
+ if (variant == null) {
+ continue;
+ }
+ xg.getAndSet(Math.max(variant.getW(), xg.get()));
+ zg.getAndSet(Math.max(variant.getD(), zg.get()));
+ }
+ }
+ }
+
private void updateRadiusBounds(
KMap sizeCache,
AtomicInteger xg,
diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java
index 7ac27ceaf..1b0fdc309 100644
--- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java
+++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisPostModifier.java
@@ -225,14 +225,17 @@ public class IrisPostModifier extends EngineAssignedModifier {
// Foliage
b = getPostBlock(x, h + 1, z, currentPostX, currentPostZ, currentData);
- if (B.isVineBlock(b) && b instanceof MultipleFacing f) {
+ if (B.isVineBlock(b) && b instanceof MultipleFacing) {
+ MultipleFacing f = (MultipleFacing) b.clone();
int finalH = h + 1;
f.getAllowedFaces().forEach(face -> {
BlockData d = getPostBlock(x + face.getModX(), finalH + face.getModY(), z + face.getModZ(), currentPostX, currentPostZ, currentData);
f.setFace(face, !B.isAir(d) && !B.isVineBlock(d));
});
- setPostBlock(x, h + 1, z, b, currentPostX, currentPostZ, currentData);
+ if (!f.equals(b)) {
+ setPostBlock(x, h + 1, z, f, currentPostX, currentPostZ, currentData);
+ }
}
if (B.isFoliage(b) || b.getMaterial().equals(Material.DEAD_BUSH)) {
diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java
index 88d1f6eb9..976ea3797 100644
--- a/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java
+++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObject.java
@@ -698,13 +698,19 @@ public class IrisObject extends IrisRegistrant {
}
double newRotation = config.getRotation().getYAxis().getMin() + slopeRotationY;
+ IrisObjectRotation originalRotation = config.getRotation();
+ IrisObjectRotation slopeRotation = new IrisObjectRotation();
+ slopeRotation.setXAxis(originalRotation.getXAxis());
+ slopeRotation.setZAxis(originalRotation.getZAxis());
if (newRotation == 0) {
- config.getRotation().setYAxis(new IrisAxisRotationClamp(false, false, 0, 0, 90));
- config.getRotation().setEnabled(config.getRotation().canRotateX() || config.getRotation().canRotateZ());
+ slopeRotation.setYAxis(new IrisAxisRotationClamp(false, false, 0, 0, 90));
+ slopeRotation.setEnabled(originalRotation.canRotateX() || originalRotation.canRotateZ());
} else {
- config.getRotation().setYAxis(new IrisAxisRotationClamp(true, false, newRotation, newRotation, 90));
- config.getRotation().setEnabled(true);
+ slopeRotation.setYAxis(new IrisAxisRotationClamp(true, false, newRotation, newRotation, 90));
+ slopeRotation.setEnabled(true);
}
+ config = config.toPlacement(config.getPlace().toArray(new String[0]));
+ config.setRotation(slopeRotation);
}
}
@@ -1048,7 +1054,9 @@ public class IrisObject extends IrisRegistrant {
}
if (placer.isPreventingDecay() && (data) instanceof Leaves && !((Leaves) (data)).isPersistent()) {
- ((Leaves) data).setPersistent(true);
+ Leaves leaves = (Leaves) data.clone();
+ leaves.setPersistent(true);
+ data = leaves;
}
for (IrisObjectReplace j : config.getEdit()) {
@@ -1103,17 +1111,24 @@ public class IrisObject extends IrisRegistrant {
}
if (data instanceof Waterlogged && shouldAutoWaterlogBlock(placer, config, yv, xx, yy, zz)) {
- ((Waterlogged) data).setWaterlogged(true);
+ Waterlogged waterlogged = (Waterlogged) data.clone();
+ waterlogged.setWaterlogged(true);
+ data = waterlogged;
}
if (B.isVineBlock(data)) {
- MultipleFacing f = (MultipleFacing) data;
+ MultipleFacing f = (MultipleFacing) data.clone();
+ boolean facesChanged = false;
for (BlockFace face : f.getAllowedFaces()) {
BlockData facingBlock = placer.get(xx + face.getModX(), yy + face.getModY(), zz + face.getModZ());
if (B.isSolid(facingBlock) && !B.isVineBlock(facingBlock)) {
f.setFace(face, true);
+ facesChanged = true;
}
}
+ if (facesChanged) {
+ data = f;
+ }
}
if (listener != null) {
@@ -1314,8 +1329,11 @@ public class IrisObject extends IrisRegistrant {
int highest = placer.getHighest(xx, zz, getLoader(), true);
- if (d instanceof Waterlogged && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz))
- ((Waterlogged) d).setWaterlogged(true);
+ if (d instanceof Waterlogged && shouldAutoWaterlogBlock(placer, config, yv, xx, highest, zz)) {
+ Waterlogged waterlogged = (Waterlogged) d.clone();
+ waterlogged.setWaterlogged(true);
+ d = waterlogged;
+ }
int lowerBound = highest - 1;
if (settings != null) {
@@ -1352,13 +1370,18 @@ public class IrisObject extends IrisRegistrant {
}
if (B.isVineBlock(d)) {
- MultipleFacing f = (MultipleFacing) d;
+ MultipleFacing f = (MultipleFacing) d.clone();
+ boolean facesChanged = false;
for (BlockFace face : f.getAllowedFaces()) {
BlockData facingBlock = placer.get(xx + face.getModX(), j + face.getModY(), zz + face.getModZ());
if (B.isSolid(facingBlock) && !B.isVineBlock(facingBlock)) {
f.setFace(face, true);
+ facesChanged = true;
}
}
+ if (facesChanged) {
+ d = f;
+ }
}
placer.set(xx, j, zz, d);
}
diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java
index f3660fba8..67f6e68b8 100644
--- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java
+++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectPlacement.java
@@ -21,6 +21,7 @@ package art.arcane.iris.engine.object;
import art.arcane.iris.Iris;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.data.cache.AtomicCache;
+import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -202,8 +203,11 @@ public class IrisObjectPlacement {
}
public CNG getSurfaceWarp(RNG rng, IrisData data) {
- return surfaceWarp.aquire(() ->
- getWarp().create(rng, data));
+ return surfaceWarp.aquire(() -> {
+ Engine engine = data.getEngine();
+ RNG warpRng = engine != null ? new RNG(engine.getSeedManager().getComponent() + 1024) : new RNG(8675309L);
+ return getWarp().create(warpRng, data);
+ });
}
public double warp(RNG rng, double x, double y, double z, IrisData data) {
diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java
index 63065776b..98e2a6881 100644
--- a/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java
+++ b/core/src/main/java/art/arcane/iris/engine/object/IrisOreGenerator.java
@@ -48,6 +48,11 @@ public class IrisOreGenerator {
private transient AtomicCache chanceCache = new AtomicCache<>();
+ public void warm(RNG rng, IrisData data) {
+ chanceCache.aquire(() -> chanceStyle.create(rng, data));
+ palette.getLayerGenerator(rng, data);
+ }
+
public BlockData generate(int x, int y, int z, RNG rng, IrisData data) {
if (palette.getPalette().isEmpty()) {
return null;
diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java
index d0a8ed67c..904c471b4 100644
--- a/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java
+++ b/core/src/main/java/art/arcane/iris/engine/object/IrisProceduralPlacement.java
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
+import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
/**
@@ -40,4 +41,6 @@ public interface IrisProceduralPlacement {
IrisObjectPlacement asPlacement();
IrisObject getVariantObject(IrisData data, RNG rng);
+
+ KList getVariantObjects(IrisData data);
}
diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java
index c25edac81..ca11cf63b 100644
--- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java
+++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java
@@ -23,7 +23,7 @@ public class MantleCarvingComponentTop2BlendTest {
@BeforeClass
public static void setup() throws Exception {
Class> weightedProfileClass = Class.forName("art.arcane.iris.engine.mantle.components.MantleCarvingComponent$WeightedProfile");
- weightedProfileConstructor = weightedProfileClass.getDeclaredConstructor(IrisCaveProfile.class, double[].class, double.class, Class.forName("art.arcane.iris.engine.object.IrisRange"));
+ weightedProfileConstructor = weightedProfileClass.getDeclaredConstructor(IrisCaveProfile.class, double[].class, double.class, Class.forName("art.arcane.iris.engine.object.IrisRange"), int.class);
weightedProfileConstructor.setAccessible(true);
limitMethod = MantleCarvingComponent.class.getDeclaredMethod("limitAndMergeBlendedProfiles", List.class, int.class, int.class);
limitMethod.setAccessible(true);
@@ -76,6 +76,33 @@ public class MantleCarvingComponentTop2BlendTest {
assertEquals(1.0D, byProfile.get(second)[254], 0D);
}
+ @Test
+ public void topTwoTieBreaksBySequenceNotIdentity() throws Exception {
+ IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
+ IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
+ IrisCaveProfile third = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.0D);
+
+ double[] weightsA = new double[256];
+ double[] weightsB = new double[256];
+ double[] weightsC = new double[256];
+ weightsA[0] = 0.5D;
+ weightsB[0] = 0.5D;
+ weightsC[0] = 0.5D;
+
+ List