mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-23 23:31:00 +00:00
Make world generation fully deterministic; add golden-hash verification harness
Generation now produces bit-identical output across repeated runs, server restarts, and parallel generation (golden-hash verified, 289/289 chunks). Engine fixes: - MatterGenerator: components flagged complete before generateLayer finished (raiseFlagUnchecked), letting concurrent neighbors consume half-written mantle data; switched to completion-locked raiseFlagSuspend - MantleObjectComponent: procedural objects (trees/ruins/formations/coral/ fungi/crystals) were absent from computeRadius, so canopies crossed chunk borders without ordering guarantees; variant footprints now counted - MantleCarvingComponent: carve profile ordering depended on IdentityHashMap iteration and hashCode sort ties; replaced with first-seen column-scan order and creation-sequence tiebreaks - IrisObject/IrisPostModifier: shared BlockData instances mutated in place (vine facing x3, auto-waterlog x2, leaf persistence); clone before mutate - IrisObject: slope-following placement mutated the shared pack-defined rotation config; now builds a local rotation on a copied placement - IrisObjectPlacement: surface-warp CNG seeded from first-caller chunk RNG; now seeded from the engine seed manager - GenerationCacheWarmer: engine init pre-bakes placement/decorator/ore/ procedural caches in sorted order, removing lazy first-touch ordering - IrisProceduralPlacement: expose getVariantObjects for radius computation Verification harness: - /iris dev goldenhash: buffered chunk generation (no world writes), SHA-256 per-chunk block+biome digests, golden capture/verify with per-chunk mismatch reporting, auto-diagnosis (repeat-gen + mantle-reset comparison, block-level diffs), deep per-chunk blockstate dumps, threads/deep params, full mantle reset (saveAll + tectonic file wipe) for reproducible runs - IrisEngineSVC: mantle trim/unload suspended while a goldenhash scan runs - MantleCarvingComponentTop2BlendTest: sequence-tiebreak regression test
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<int[]> targets = ChunkJobReporter.orderedTargets(centerChunkX, centerChunkZ, radius);
|
||||
reporter.setTotal(targets.size());
|
||||
reporter.setStage("Generating");
|
||||
Map<Long, String> 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<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());
|
||||
|
||||
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<BlockData, byte[]> blockCache = new IdentityHashMap<>();
|
||||
Map<Biome, byte[]> 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<Long, String> lines) throws IOException {
|
||||
List<String> body = orderedBody(lines);
|
||||
String combined = combinedHash(body);
|
||||
List<String> 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<Long, String> lines) throws IOException {
|
||||
List<String> existing = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8);
|
||||
Map<String, String> meta = new HashMap<>();
|
||||
Map<String, String> 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<String> body = orderedBody(lines);
|
||||
List<String> 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<String> 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<String> 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<String> 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<String> 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<String> orderedBody(Map<Long, String> lines) {
|
||||
Map<Long, String> sorted = new TreeMap<>(lines);
|
||||
return new ArrayList<>(sorted.values());
|
||||
}
|
||||
|
||||
private String combinedHash(List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<IrisBiome> 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<IrisRegion> 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<IrisObjectPlacement> 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<IrisDecorator> 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<IrisOreGenerator> 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]++));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ public interface MatterGenerator {
|
||||
int finalPassZ = passZ;
|
||||
MantleChunk<Matter> 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) {
|
||||
|
||||
+28
-27
@@ -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<IrisCaveProfile, double[]> columnProfileWeights = blendScratch.columnProfileWeights;
|
||||
IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = blendScratch.activeProfiles;
|
||||
List<IrisCaveProfile> 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<WeightedProfile> 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<WeightedProfile> 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<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
|
||||
List<IrisCaveProfile> 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<Map.Entry<IrisCaveProfile, double[]>> profileEntries = new ArrayList<>(rootProfileColumnWeights.entrySet());
|
||||
profileEntries.sort((a, b) -> Integer.compare(a.getKey().hashCode(), b.getKey().hashCode()));
|
||||
for (Map.Entry<IrisCaveProfile, double[]> 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<WeightedProfile> 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<IrisCaveProfile, double[]> columnProfileWeights = new IdentityHashMap<>();
|
||||
private final IdentityHashMap<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = new IdentityHashMap<>();
|
||||
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
|
||||
private final List<IrisCaveProfile> 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];
|
||||
|
||||
+20
@@ -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<String, BlockVector> sizeCache,
|
||||
AtomicInteger xg,
|
||||
|
||||
@@ -225,14 +225,17 @@ public class IrisPostModifier extends EngineAssignedModifier<BlockData> {
|
||||
// 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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -48,6 +48,11 @@ public class IrisOreGenerator {
|
||||
|
||||
private transient AtomicCache<CNG> 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;
|
||||
|
||||
@@ -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<IrisObject> getVariantObjects(IrisData data);
|
||||
}
|
||||
|
||||
+31
-4
@@ -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<Object> weighted = new ArrayList<>();
|
||||
weighted.add(weightedProfileConstructor.newInstance(first, weightsA, average(weightsA), null, 0));
|
||||
weighted.add(weightedProfileConstructor.newInstance(second, weightsB, average(weightsB), null, 1));
|
||||
weighted.add(weightedProfileConstructor.newInstance(third, weightsC, average(weightsC), null, 2));
|
||||
|
||||
List<?> limited = invokeLimit(weighted, 2);
|
||||
assertEquals(2, limited.size());
|
||||
|
||||
Map<IrisCaveProfile, double[]> byProfile = extractWeightsByProfile(limited);
|
||||
assertEquals(true, byProfile.containsKey(first));
|
||||
assertEquals(true, byProfile.containsKey(second));
|
||||
assertEquals(false, byProfile.containsKey(third));
|
||||
}
|
||||
|
||||
private WeightedInput createWeightedProfiles() throws Exception {
|
||||
IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.31D);
|
||||
IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.17D);
|
||||
@@ -99,9 +126,9 @@ public class MantleCarvingComponentTop2BlendTest {
|
||||
thirdWeights[255] = 0.4D;
|
||||
|
||||
List<Object> weighted = new ArrayList<>();
|
||||
weighted.add(weightedProfileConstructor.newInstance(first, firstWeights, average(firstWeights), null));
|
||||
weighted.add(weightedProfileConstructor.newInstance(second, secondWeights, average(secondWeights), null));
|
||||
weighted.add(weightedProfileConstructor.newInstance(third, thirdWeights, average(thirdWeights), null));
|
||||
weighted.add(weightedProfileConstructor.newInstance(first, firstWeights, average(firstWeights), null, 0));
|
||||
weighted.add(weightedProfileConstructor.newInstance(second, secondWeights, average(secondWeights), null, 1));
|
||||
weighted.add(weightedProfileConstructor.newInstance(third, thirdWeights, average(thirdWeights), null, 2));
|
||||
return new WeightedInput(weighted, profiles);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user