This commit is contained in:
Brian Neumann-Fopiano
2026-07-03 15:10:16 -04:00
parent 75e8c22268
commit a8e46e3590
104 changed files with 6002 additions and 2692 deletions
@@ -141,6 +141,10 @@ public class IrisSettings {
@Data
public static class IrisSettingsPregen {
private static final int REFERENCE_WORLD_HEIGHT = 384;
private static final int MIN_RESIDENT_TECTONIC_PLATES = 16;
private static final double MANTLE_HEAP_FRACTION = 0.6D;
private static final int REFERENCE_PLATE_MEGABYTES = 48;
public boolean useTicketQueue = true;
public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO;
public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO;
@@ -159,6 +163,17 @@ public class IrisSettings {
return Math.max(16, maxResidentTectonicPlates);
}
public int getEffectiveResidentTectonicPlates(int worldHeight) {
int baseCap = getMaxResidentTectonicPlates();
int normalizedHeight = Math.max(1, worldHeight);
int heightScaledCap = (int) Math.round((double) baseCap * REFERENCE_WORLD_HEIGHT / (double) normalizedHeight);
long maxHeapMegabytes = getHardware.getProcessMemory();
double plateMegabytes = (double) REFERENCE_PLATE_MEGABYTES * (double) normalizedHeight / (double) REFERENCE_WORLD_HEIGHT;
int byteBudgetCap = (int) Math.floor(MANTLE_HEAP_FRACTION * (double) maxHeapMegabytes / plateMegabytes);
int effective = Math.min(heightScaledCap, byteBudgetCap);
return Math.max(MIN_RESIDENT_TECTONIC_PLATES, Math.min(baseCap, effective));
}
public int getMantleBackpressureWaitMs() {
return Math.max(5, Math.min(mantleBackpressureWaitMs, 1_000));
}
@@ -25,10 +25,19 @@ import java.awt.GraphicsEnvironment;
public final class GuiHost {
private static volatile Provider provider = new Provider() {
};
private static volatile boolean desktopSuppressed = false;
private GuiHost() {
}
public static void suppressDesktop(boolean suppress) {
desktopSuppressed = suppress;
}
public static boolean isDesktopSuppressed() {
return desktopSuppressed;
}
public interface Provider {
default Engine findActiveEngine() {
return null;
@@ -56,6 +65,6 @@ public final class GuiHost {
}
public static boolean isAvailable() {
return !GraphicsEnvironment.isHeadless();
return !desktopSuppressed && !GraphicsEnvironment.isHeadless();
}
}
@@ -85,8 +85,11 @@ public final class PregenRenderer extends JPanel implements KeyListener {
}
public void close() {
if (frame != null) {
frame.setVisible(false);
JFrame activeFrame = frame;
if (activeFrame != null) {
frame = null;
activeFrame.setVisible(false);
activeFrame.dispose();
}
}
@@ -33,7 +33,6 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
import java.awt.Color;
import java.util.concurrent.ExecutorService;
@@ -63,12 +62,18 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
private final ChronoLatch cl = new ChronoLatch(TimeUnit.MINUTES.toMillis(1));
private final Engine engine;
private final ExecutorService service;
private final Thread worker;
private PregenRenderer renderer;
private Consumer2<Position2, Color> drawFunction;
private int rgc = 0;
private String[] info;
private volatile double lastChunksPerSecond = 0D;
private volatile long lastChunksRemaining = 0L;
private volatile long lastGenerated = 0L;
private volatile long lastTotalChunks = 0L;
private volatile long lastEta = 0L;
private volatile long lastElapsed = 0L;
private volatile String lastMethod = "Void";
public PregeneratorJob(PregenTask task, PregeneratorMethod method, Engine engine) {
instance.updateAndGet(old -> {
@@ -84,26 +89,32 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
info = new String[]{"Initializing..."};
this.task = task;
this.pregenerator = new IrisPregenerator(task, method, this);
max = new Position2(0, 0);
max = new Position2(Integer.MIN_VALUE, Integer.MIN_VALUE);
min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
service = Executors.newVirtualThreadPerTaskExecutor();
if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) {
open();
}
worker = new Thread(() -> {
J.sleep(1000);
computeBounds();
this.pregenerator.start();
}, "Iris Pregenerator");
worker.setPriority(Thread.MIN_PRIORITY);
worker.setDaemon(true);
worker.setUncaughtExceptionHandler((thread, ex) -> IrisLogging.reportError(ex));
worker.start();
}
private void computeBounds() {
task.iterateAllChunks((xx, zz) -> {
min.setX(Math.min(xx, min.getX()));
min.setZ(Math.min(zz, min.getZ()));
max.setX(Math.max(xx, max.getX()));
max.setZ(Math.max(zz, max.getZ()));
});
if (IrisSettings.get().getGui().isUseServerLaunchedGuis() && task.isGui()) {
open();
}
Thread t = new Thread(() -> {
J.sleep(1000);
this.pregenerator.start();
}, "Iris Pregenerator");
t.setPriority(Thread.MIN_PRIORITY);
t.start();
service = Executors.newVirtualThreadPerTaskExecutor();
}
public static boolean shutdownInstance() {
@@ -116,6 +127,23 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return true;
}
public static boolean shutdownAndWait(long timeoutMs) {
PregeneratorJob inst = instance.get();
if (inst == null) {
return false;
}
inst.pregenerator.close();
inst.worker.interrupt();
try {
inst.worker.join(timeoutMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
instance.compareAndSet(inst, null);
return true;
}
public static PregeneratorJob getInstance() {
return instance.get();
}
@@ -153,13 +181,36 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
return inst == null ? -1L : Math.max(0L, inst.lastChunksRemaining);
}
public boolean targetsWorld(World world) {
if (world == null || engine == null || engine.getWorld() == null) {
return false;
public record PregenProgress(double percent, long generated, long totalChunks, double chunksPerSecond, long chunksRemaining, long eta, long elapsed, String method, boolean paused, long failed, String worldName) {
}
public static PregenProgress progressSnapshot() {
PregeneratorJob inst = instance.get();
if (inst == null) {
return null;
}
String targetName = engine.getWorld().name();
return targetName != null && targetName.equalsIgnoreCase(world.getName());
double percent = inst.lastTotalChunks <= 0 ? 0D : ((double) inst.lastGenerated / (double) inst.lastTotalChunks) * 100D;
return new PregenProgress(
percent,
inst.lastGenerated,
inst.lastTotalChunks,
Math.max(0D, inst.lastChunksPerSecond),
Math.max(0L, inst.lastChunksRemaining),
inst.lastEta,
inst.lastElapsed,
inst.lastMethod,
inst.paused(),
inst.pregenerator.getFailedChunks(),
inst.worldName());
}
public String worldName() {
if (engine == null || engine.getWorld() == null) {
return null;
}
return engine.getWorld().name();
}
public boolean targetsWorldName(String worldName) {
@@ -252,6 +303,11 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
lastChunksPerSecond = chunksPerSecond;
lastChunksRemaining = chunksRemaining;
lastGenerated = generated;
lastTotalChunks = totalChunks;
lastEta = eta;
lastElapsed = elapsed;
lastMethod = method;
info = new String[]{
(paused() ? "PAUSED" : (saving ? "Saving... " : "Generating")) + " " + Form.f(generated) + " of " + Form.f(totalChunks) + " (" + Form.pc(percent, 0) + " Complete)",
@@ -1,5 +1,6 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.nms.INMS;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.WorldCreator;
@@ -31,6 +32,8 @@ final class BukkitPublicBackend implements WorldLifecycleBackend {
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
}
INMS.get().ensureServerLevelInjection();
try {
World world = creator.createWorld();
return CompletableFuture.completedFuture(world);
@@ -164,6 +164,9 @@ public interface INMSBinding {
throw new UnsupportedOperationException("Active NMS binding does not support runtime LevelStem creation.");
}
default void ensureServerLevelInjection() {
}
int countCustomBiomes();
default boolean supportsDataPacks() {
@@ -188,6 +191,13 @@ public interface INMSBinding {
return false;
}
default boolean saveAndUnloadChunk(World world, int x, int z) {
return false;
}
default void flushChunkIO(World world) {
}
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
@@ -0,0 +1,160 @@
/*
* Iris is a World Generator for Minecraft 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.pack;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.misc.WebCache;
import art.arcane.volmlib.util.io.IO;
import org.zeroturnaround.zip.ZipUtil;
import org.zeroturnaround.zip.commons.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import java.util.function.Consumer;
public final class PackDownloader {
private PackDownloader() {
}
public static String download(File packsFolder, String repo, String branch, boolean forceOverwrite, boolean directUrl, Consumer<String> feedback) throws IOException {
String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch;
feedback.accept("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
File zip = WebCache.getNonCachedFile("pack-" + repo, url);
File temp = WebCache.getTemp();
File work = new File(temp, "dl-" + UUID.randomUUID());
if (zip == null || !zip.exists()) {
feedback.accept("Failed to find pack at " + url);
feedback.accept("Make sure you specified the correct repo and branch!");
feedback.accept("For example: /iris download overworld branch=stable");
return null;
}
feedback.accept("Unpacking " + repo);
try {
ZipUtil.unpack(zip, work);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
feedback.accept(
"""
Issue when unpacking. Please check/do the following:
1. Do you have a functioning internet connection?
2. Did the download corrupt?
3. Try deleting the */plugins/iris/packs folder and re-download.
4. Download the pack from the GitHub repo: https://github.com/IrisDimensions/overworld
5. Contact support (if all other options do not help)"""
);
}
File dir = null;
File[] zipFiles = work.listFiles();
if (zipFiles == null) {
feedback.accept("No files were extracted from the zip file.");
return null;
}
try {
dir = zipFiles.length > 1 ? work : zipFiles[0].isDirectory() ? zipFiles[0] : null;
} catch (NullPointerException e) {
IrisLogging.reportError(e);
feedback.accept("Error when finding home directory. Are there any non-text characters in the file name?");
return null;
}
if (dir == null) {
feedback.accept("Invalid Format. Missing root folder or too many folders!");
return null;
}
IrisData data = IrisData.get(dir);
String[] dimensions = data.getDimensionLoader().getPossibleKeys();
if (dimensions == null || dimensions.length == 0) {
feedback.accept("No dimension file found in the extracted zip file.");
feedback.accept("Check it is there on GitHub and report this to staff!");
return null;
}
if (dimensions.length != 1) {
feedback.accept("Dimensions folder must have 1 file in it");
return null;
}
IrisDimension d = data.getDimensionLoader().load(dimensions[0]);
data.close();
if (d == null) {
feedback.accept("Invalid dimension (folder) in dimensions folder");
return null;
}
String key = d.getLoadKey();
feedback.accept("Importing " + d.getName() + " (" + key + ")");
File packEntry = new File(packsFolder, key);
if (forceOverwrite) {
IO.delete(packEntry);
}
if (IrisData.loadAnyDimension(key, null) != null) {
feedback.accept("Another dimension in the packs folder is already using the key " + key + " IMPORT FAILED!");
return null;
}
File[] existingEntries = packEntry.listFiles();
if (packEntry.exists() && existingEntries != null && existingEntries.length > 0) {
feedback.accept("Another pack is using the key " + key + ". IMPORT FAILED!");
return null;
}
FileUtils.copyDirectory(dir, packEntry);
IrisData.getLoaded(packEntry)
.ifPresent(IrisData::hotloaded);
feedback.accept("Successfully Aquired " + d.getName());
validateDownloaded(packEntry, feedback);
return key;
}
private static void validateDownloaded(File packEntry, Consumer<String> feedback) {
try {
PackValidationResult result = PackValidator.validate(packEntry);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
feedback.accept("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
feedback.accept(" - " + reason);
}
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
feedback.accept("Pack '" + result.getPackName() + "' validated ("
+ result.getRemovedUnusedFiles().size() + " unused file(s) quarantined to .iris-trash/, "
+ result.getWarnings().size() + " warning(s)).");
} else {
feedback.accept("Pack '" + result.getPackName() + "' validated.");
}
} catch (Throwable e) {
IrisLogging.reportError("Pack validation failed for '" + packEntry.getName() + "'", e);
}
}
}
@@ -1,268 +0,0 @@
package art.arcane.iris.core.pregenerator;
import com.google.gson.Gson;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RollingSequence;
import art.arcane.volmlib.util.math.Spiraler;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import lombok.Data;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldUnloadEvent;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
public class DeepSearchPregenerator extends Thread implements Listener {
@Getter
private static DeepSearchPregenerator instance;
private final DeepSearchJob job;
private final File destination;
private final int maxPosition;
private World world;
private final ChronoLatch latch;
private static AtomicInteger foundChunks;
private final AtomicInteger foundLast;
private final AtomicInteger foundTotalChunks;
private final AtomicLong startTime;
private final RollingSequence chunksPerSecond;
private final RollingSequence chunksPerMinute;
private final AtomicInteger chunkCachePos;
private final AtomicInteger chunkCacheSize;
private int pos;
private final AtomicInteger foundCacheLast;
private final AtomicInteger foundCache;
private LinkedHashMap<Integer, Position2> chunkCache;
private KList<Position2> chunkQueue;
private final ReentrantLock cacheLock;
private static final Map<String, DeepSearchJob> jobs = new HashMap<>();
public DeepSearchPregenerator(DeepSearchJob job, File destination) {
this.job = job;
this.chunkCacheSize = new AtomicInteger(); // todo
this.chunkCachePos = new AtomicInteger(1000);
this.foundCacheLast = new AtomicInteger();
this.foundCache = new AtomicInteger();
this.cacheLock = new ReentrantLock();
this.destination = destination;
this.chunkCache = new LinkedHashMap<>();
this.maxPosition = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
}).count();
this.world = Bukkit.getWorld(job.getWorld().getUID());
this.chunkQueue = new KList<>();
this.latch = new ChronoLatch(3000);
this.startTime = new AtomicLong(M.ms());
this.chunksPerSecond = new RollingSequence(10);
this.chunksPerMinute = new RollingSequence(10);
foundChunks = new AtomicInteger(0);
this.foundLast = new AtomicInteger(0);
this.foundTotalChunks = new AtomicInteger((int) Math.ceil(Math.pow((2.0 * job.getRadiusBlocks()) / 16, 2)));
this.pos = 0;
jobs.put(job.getWorld().getName(), job);
DeepSearchPregenerator.instance = this;
}
@EventHandler
public void on(WorldUnloadEvent e) {
if (e.getWorld().equals(world)) {
interrupt();
}
}
public void run() {
while (!interrupted()) {
tick();
}
try {
saveNow();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void tick() {
DeepSearchJob job = jobs.get(world.getName());
// chunkCache(); //todo finish this
if (latch.flip() && !job.paused) {
if (cacheLock.isLocked()) {
IrisLogging.info("DeepFinder: Caching: " + chunkCachePos.get() + " Of " + chunkCacheSize.get());
} else {
long eta = computeETA();
save();
int secondGenerated = foundChunks.get() - foundLast.get();
foundLast.set(foundChunks.get());
secondGenerated = secondGenerated / 3;
chunksPerSecond.put(secondGenerated);
chunksPerMinute.put(secondGenerated * 60);
IrisLogging.info("DeepFinder: " + C.IRIS + world.getName() + C.RESET + " Searching: " + Form.f(foundChunks.get()) + " of " + Form.f(foundTotalChunks.get()) + " " + Form.f((int) chunksPerSecond.getAverage()) + "/s ETA: " + Form.duration((double) eta, 2));
}
}
if (foundChunks.get() >= foundTotalChunks.get()) {
IrisLogging.info("Completed DeepSearch!");
interrupt();
}
}
private long computeETA() {
return (long) ((foundTotalChunks.get() - foundChunks.get()) / chunksPerSecond.getAverage()) * 1000;
// todo broken
}
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private void queueSystem(Position2 chunk) {
if (chunkQueue.isEmpty()) {
for (int limit = 512; limit != 0; limit--) {
pos = job.getPosition() + 1;
chunkQueue.add(getChunk(pos));
}
} else {
//MCAUtil.read();
}
}
private void findInChunk(World world, int x, int z) throws IOException {
int xx = x * 16;
int zz = z * 16;
Engine engine = IrisToolbelt.access(world).getEngine();
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
int height = engine.getHeight(xx + i, zz + j);
if (height > 300) {
File found = new File("plugins", "iris" + File.separator + "found.txt");
found.getParentFile().mkdirs();
IrisBiome biome = engine.getBiome(xx, engine.getHeight(), zz);
IrisLogging.info("Found at! " + xx + ", " + zz + " Biome ID: " + biome.getName());
try (FileWriter writer = new FileWriter(found, true)) {
writer.write("Biome at: X: " + xx + " Z: " + zz + " Biome ID: " + biome.getName() + "\n");
}
return;
}
}
}
}
public Position2 getChunk(int position) {
int p = -1;
AtomicInteger xx = new AtomicInteger();
AtomicInteger zz = new AtomicInteger();
Spiraler s = new Spiraler(job.getRadiusBlocks() * 2, job.getRadiusBlocks() * 2, (x, z) -> {
xx.set(x);
zz.set(z);
});
while (s.hasNext() && p++ < position) {
s.next();
}
return new Position2(xx.get(), zz.get());
}
public void save() {
J.a(() -> {
try {
saveNow();
} catch (Throwable e) {
e.printStackTrace();
}
});
}
public static void setPausedDeep(World world) {
DeepSearchJob job = jobs.get(world.getName());
if (isPausedDeep(world)){
job.paused = false;
} else {
job.paused = true;
}
if ( job.paused) {
IrisLogging.info(C.BLUE + "DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Paused");
} else {
IrisLogging.info(C.BLUE + "DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Resumed");
}
}
public static boolean isPausedDeep(World world) {
DeepSearchJob job = jobs.get(world.getName());
return job != null && job.isPaused();
}
public void shutdownInstance(World world) throws IOException {
IrisLogging.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " Shutting down..");
DeepSearchJob job = jobs.get(world.getName());
File worldDirectory = new File(Bukkit.getWorldContainer(), world.getName());
File deepFile = new File(worldDirectory, "DeepSearch.json");
if (job == null) {
IrisLogging.error("No DeepSearch job found for world: " + world.getName());
return;
}
try {
if (!job.isPaused()) {
job.setPaused(true);
}
save();
jobs.remove(world.getName());
J.a(() -> {
while (deepFile.exists()) {
deepFile.delete();
J.sleep(1000);
}
IrisLogging.info("DeepSearch: " + C.IRIS + world.getName() + C.BLUE + " File deleted and instance closed.");
}, 20);
} catch (Exception e) {
IrisLogging.error("Failed to shutdown DeepSearch for " + world.getName());
e.printStackTrace();
} finally {
saveNow();
interrupt();
}
}
public void saveNow() throws IOException {
IO.writeAll(this.destination, new Gson().toJson(job));
}
@Data
@lombok.Builder
public static class DeepSearchJob {
private World world;
@lombok.Builder.Default
private int radiusBlocks = 5000;
@lombok.Builder.Default
private int position = 0;
@lombok.Builder.Default
boolean paused = false;
}
}
@@ -67,6 +67,8 @@ public class IrisPregenerator {
private final KSet<Position2> net;
private final ChronoLatch cl;
private final ChronoLatch saveLatch;
private final ChronoLatch heapReclaimLatch;
private final AtomicLong failed;
private final IrisPackBenchmarking benchmarking;
public IrisPregenerator(PregenTask task, PregeneratorMethod generator, PregenListener listener) {
@@ -74,6 +76,8 @@ public class IrisPregenerator {
this.listener = listenify(listener);
cl = new ChronoLatch(10000);
saveLatch = new ChronoLatch(IrisSettings.get().getPregen().getSaveIntervalMs());
heapReclaimLatch = new ChronoLatch(1000);
failed = new AtomicLong(0);
generatedRegions = new KSet<>();
this.shutdown = new AtomicBoolean(false);
this.paused = new AtomicBoolean(false);
@@ -95,7 +99,6 @@ public class IrisPregenerator {
cachedLast = new AtomicLong(0);
cachedLastMinute = new AtomicLong(0);
totalChunks = new AtomicLong(0);
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
startTime = new AtomicLong(M.ms());
ticker = new Looper() {
@Override
@@ -175,13 +178,27 @@ public class IrisPregenerator {
public void start() {
init();
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
startTime.set(M.ms());
ticker.start();
checkRegions();
PrecisionStopwatch p = PrecisionStopwatch.start();
task.iterateRegions((x, z) -> visitRegion(x, z, true));
task.iterateRegions((x, z) -> visitRegion(x, z, false));
IrisLogging.info("Pregen took " + Form.duration((long) p.getMilliseconds()));
shutdown();
try {
int[] regionBounds = task.regionBounds();
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
task.iterateRegions((x, z) -> visitRegion(x, z, true));
task.iterateRegions((x, z) -> visitRegion(x, z, false));
long failedCount = failed.get();
if (failedCount > 0) {
IrisLogging.warn("Pregen finished with " + Form.f(failedCount) + " failed chunk(s); failures are not cached, rerun to fill them");
}
IrisLogging.info("Pregen took " + Form.duration((long) p.getMilliseconds()));
} catch (Throwable e) {
IrisLogging.reportError(e);
IrisLogging.error("Pregen aborted after " + Form.duration((long) p.getMilliseconds()) + " due to " + e.getClass().getSimpleName() + ": " + e.getMessage());
} finally {
shutdown();
}
if (benchmarking == null) {
IrisLogging.info(C.IRIS + "Pregen stopped.");
} else {
@@ -266,12 +283,20 @@ public class IrisPregenerator {
hit = true;
listener.onRegionGenerating(x, z);
task.iterateChunks(x, z, (xx, zz) -> {
while (paused.get() && !shutdown.get()) {
while ((paused.get() || MantleHeapPressure.overHighWater()) && !shutdown.get()) {
if (!paused.get()) {
reclaimHeapPressure();
}
J.sleep(50);
}
if (shutdown.get()) {
return;
}
generator.generateChunk(xx, zz, listener);
});
generator.onRegionSubmitted(x, z);
}
if (hit) {
@@ -280,9 +305,13 @@ public class IrisPregenerator {
if (saveLatch.flip()) {
listener.onSaving();
generator.save();
Mantle mantle = getMantle();
if (mantle != null) {
mantle.trim(0, 0);
try {
Mantle mantle = getMantle();
if (mantle != null) {
mantle.trim(0, 0);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
@@ -291,6 +320,26 @@ public class IrisPregenerator {
}
}
private void reclaimHeapPressure() {
if (!heapReclaimLatch.flip()) {
return;
}
try {
Mantle mantle = getMantle();
if (mantle != null) {
mantle.trim(0, 0);
mantle.unloadTectonicPlate(0);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
if (MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
}
private void checkRegion(int x, int z) {
if (generatedRegions.contains(new Position2(x, z))) {
return;
@@ -299,6 +348,10 @@ public class IrisPregenerator {
generator.supportsRegions(x, z, listener);
}
public long getFailedChunks() {
return failed.get();
}
public void pause() {
paused.set(true);
}
@@ -326,6 +379,12 @@ public class IrisPregenerator {
if (c) cached.addAndGet(1);
}
@Override
public void onChunkFailed(int x, int z) {
failed.addAndGet(1);
listener.onChunkFailed(x, z);
}
@Override
public void onRegionGenerated(int x, int z) {
listener.onRegionGenerated(x, z);
@@ -0,0 +1,82 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.pregenerator;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public final class MantleHeapPressure {
private static final double HIGH_WATER = 0.92D;
private static final double LOW_WATER = 0.82D;
private static final double PANIC_WATER = 0.96D;
private static final long PANIC_GC_INTERVAL_MS = 30_000L;
private static final AtomicBoolean engaged = new AtomicBoolean(false);
private static final AtomicLong lastPanicGcAt = new AtomicLong(0L);
private MantleHeapPressure() {
}
public static double usedFraction() {
MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
long max = heap.getMax();
if (max <= 0L) {
Runtime runtime = Runtime.getRuntime();
long runtimeMax = runtime.maxMemory();
if (runtimeMax <= 0L) {
return 0.0D;
}
long used = runtime.totalMemory() - runtime.freeMemory();
return (double) used / (double) runtimeMax;
}
return (double) heap.getUsed() / (double) max;
}
public static boolean overHighWater() {
double fraction = usedFraction();
if (engaged.get()) {
if (fraction <= LOW_WATER) {
engaged.set(false);
return false;
}
return true;
}
if (fraction >= HIGH_WATER) {
engaged.set(true);
return true;
}
return false;
}
public static boolean overPanicWater() {
return usedFraction() >= PANIC_WATER;
}
public static void requestPanicReclaim() {
long now = System.currentTimeMillis();
long last = lastPanicGcAt.get();
if (now - last < PANIC_GC_INTERVAL_MS) {
return;
}
if (lastPanicGcAt.compareAndSet(last, now)) {
System.gc();
}
}
}
@@ -29,6 +29,9 @@ public interface PregenListener {
void onChunkGenerated(int x, int z, boolean cached);
default void onChunkFailed(int x, int z) {
}
void onRegionGenerated(int x, int z);
void onRegionGenerating(int x, int z);
@@ -0,0 +1,148 @@
/*
* Iris is a World Generator for Minecraft 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.pregenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import java.util.function.Supplier;
public final class PregenMantleBackpressure {
private final Supplier<Mantle> mantleSupplier;
private final int maxResidentTectonicPlates;
private final int waitMs;
private final long timeoutMs;
private final Runnable onBudgetTimeout;
private final Supplier<String> diagnostics;
public PregenMantleBackpressure(Supplier<Mantle> mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier<String> diagnostics) {
this.mantleSupplier = mantleSupplier;
this.maxResidentTectonicPlates = maxResidentTectonicPlates;
this.waitMs = waitMs;
this.timeoutMs = timeoutMs;
this.onBudgetTimeout = onBudgetTimeout;
this.diagnostics = diagnostics;
}
public void apply() {
enforceMantleBudget();
awaitHeapHeadroom();
}
public void enforceMantleBudget() {
int cap = maxResidentTectonicPlates;
if (cap <= 0) {
return;
}
Mantle mantle = resolveMantle();
if (mantle == null) {
return;
}
int hardCap = cap * 2;
if (mantle.getLoadedRegionCount() <= hardCap) {
return;
}
long waitStart = M.ms();
long lastLog = 0L;
while (mantle.getLoadedRegionCount() > hardCap) {
int freed;
int resident;
try {
mantle.trim(0L, 0);
freed = mantle.unloadTectonicPlate(0);
resident = mantle.getLoadedRegionCount();
} catch (Throwable e) {
IrisLogging.reportError(e);
break;
}
if (resident <= hardCap) {
break;
}
long elapsed = M.ms() - waitStart;
if (elapsed >= timeoutMs) {
IrisLogging.warn("Pregen mantle backpressure exceeded " + timeoutMs + "ms with " + resident
+ " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. "
+ "Raise pregen.maxResidentTectonicPlates if this persists. " + diagnostics.get());
onBudgetTimeout.run();
return;
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
IrisLogging.warn("Pregen mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
}
try {
Thread.sleep(waitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
public void awaitHeapHeadroom() {
Mantle mantle = resolveMantle();
long lastLog = 0L;
while (MantleHeapPressure.overHighWater()) {
try {
if (mantle != null && mantle.getLoadedRegionCount() > maxResidentTectonicPlates) {
mantle.trim(0L, 0);
mantle.unloadTectonicPlate(0);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
if (MantleHeapPressure.overPanicWater()) {
MantleHeapPressure.requestPanicReclaim();
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
IrisLogging.warn("Pregen heap pressure: pausing generation at "
+ Math.round(MantleHeapPressure.usedFraction() * 100.0D) + "% heap; evicting tectonic plates and waiting for headroom"
+ (mantle != null ? " (" + mantle.getLoadedRegionCount() + " plates resident)" : "") + ".");
}
try {
Thread.sleep(waitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
private Mantle resolveMantle() {
try {
return mantleSupplier.get();
} catch (Throwable ignored) {
return null;
}
}
}
@@ -0,0 +1,64 @@
/*
* Iris is a World Generator for Minecraft 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.pregenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import java.util.concurrent.atomic.AtomicBoolean;
public final class PregenPerformanceProfile {
private static final AtomicBoolean JVM_HINT_LOGGED = new AtomicBoolean(false);
private PregenPerformanceProfile() {
}
public static boolean apply() {
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
performance.setNoiseCacheSize(targetNoiseCacheSize);
changed = true;
}
if (!fastCacheEnabledBefore) {
System.setProperty("iris.cache.fast", "true");
changed = true;
}
if (JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) {
IrisLogging.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true");
}
return changed;
}
public static void apply(Engine engine) {
boolean changed = apply();
if (changed && engine != null) {
engine.hotloadComplex();
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
}
}
}
@@ -19,7 +19,6 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.Spiraled;
@@ -29,12 +28,20 @@ import lombok.Data;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Builder
@Data
public class PregenTask {
private static final KMap<Long, int[]> ORDERS = new KMap<>();
private static final int MAX_CACHED_ORDERS = 512;
private static final LinkedHashMap<Long, int[]> ORDERS = new LinkedHashMap<>(64, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Long, int[]> eldest) {
return size() > MAX_CACHED_ORDERS;
}
};
@Builder.Default
private final boolean gui = false;
@@ -71,7 +78,18 @@ public class PregenTask {
private static int[] orderForPull(int pullX, int pullZ) {
long key = orderKey(pullX, pullZ);
return ORDERS.computeIfAbsent(key, PregenTask::computeOrder);
synchronized (ORDERS) {
int[] cached = ORDERS.get(key);
if (cached != null) {
return cached;
}
}
int[] computed = computeOrder(key);
synchronized (ORDERS) {
ORDERS.put(key, computed);
}
return computed;
}
private static int[] computeOrder(long key) {
@@ -119,6 +137,11 @@ public class PregenTask {
}));
}
public int[] regionBounds() {
Bound bound = bounds.region();
return new int[]{bound.minX(), bound.minZ(), bound.maxX(), bound.maxZ()};
}
@FunctionalInterface
public interface InterleavedChunkConsumer {
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
@@ -80,5 +80,11 @@ public interface PregeneratorMethod {
*/
void generateChunk(int x, int z, PregenListener listener);
default void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
}
default void onRegionSubmitted(int regionX, int regionZ) {
}
Mantle getMantle();
}
@@ -196,6 +196,16 @@ public class PregenCacheImpl implements PregenCache {
boolean test(Region region);
}
private enum CacheResult {
NOOP,
SET,
COMPLETED
}
private interface RegionCacheOp {
CacheResult apply(Region region);
}
private static class Plate {
private final int x;
private final int z;
@@ -216,7 +226,7 @@ public class PregenCacheImpl implements PregenCache {
this.lastAccess = System.currentTimeMillis();
}
private boolean cache(int x, int z, RegionPredicate predicate) {
private boolean cache(int x, int z, RegionCacheOp op) {
lastAccess = System.currentTimeMillis();
if (count == SIZE) {
return false;
@@ -229,7 +239,13 @@ public class PregenCacheImpl implements PregenCache {
regions[index] = region;
}
if (!predicate.test(region)) {
CacheResult result = op.apply(region);
if (result == CacheResult.NOOP) {
return false;
}
dirty = true;
if (result != CacheResult.COMPLETED) {
return false;
}
@@ -237,7 +253,6 @@ public class PregenCacheImpl implements PregenCache {
if (count == SIZE) {
regions = null;
}
dirty = true;
return true;
}
@@ -282,23 +297,23 @@ public class PregenCacheImpl implements PregenCache {
this.words = words;
}
private boolean cache() {
private CacheResult cache() {
if (count == SIZE) {
return false;
return CacheResult.NOOP;
}
count = SIZE;
words = null;
return true;
return CacheResult.COMPLETED;
}
private boolean cache(int x, int z) {
private CacheResult cache(int x, int z) {
if (count == SIZE) {
return false;
return CacheResult.NOOP;
}
long[] value = words;
if (value == null) {
return false;
return CacheResult.NOOP;
}
int index = PowerOfTwoCoordinates.packLocal32(x, z);
@@ -306,17 +321,17 @@ public class PregenCacheImpl implements PregenCache {
long bit = 1L << (index & 63);
boolean current = (value[wordIndex] & bit) != 0L;
if (current) {
return false;
return CacheResult.NOOP;
}
count++;
if (count == SIZE) {
words = null;
return true;
return CacheResult.COMPLETED;
}
value[wordIndex] = value[wordIndex] | bit;
return false;
return CacheResult.SET;
}
private boolean isCached() {
@@ -70,6 +70,16 @@ public class AsyncOrMedievalPregenMethod implements PregeneratorMethod {
method.generateChunk(x, z, listener);
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
method.onRegionSubmitted(regionX, regionZ);
}
@Override
public Mantle getMantle() {
return method.getMantle();
@@ -23,9 +23,13 @@ import art.arcane.iris.core.IrisPaperLikeBackendMode;
import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenMantleBackpressure;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import art.arcane.iris.util.common.parallel.MultiBurst;
@@ -37,15 +41,16 @@ import org.bukkit.World;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -53,8 +58,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private static final AtomicInteger THREAD_COUNT = new AtomicInteger();
private static final int ADAPTIVE_TIMEOUT_STEP = 3;
private static final int ADAPTIVE_RECOVERY_INTERVAL = 8;
private static final long CHUNK_CLEANUP_INTERVAL_MS = 10_000L;
private static final long CHUNK_CLEANUP_MIN_AGE_MS = 5_000L;
private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
private final IrisPaperLikeBackendMode paperLikeBackendMode;
@@ -73,12 +76,20 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final int timeoutSeconds;
private final int timeoutWarnIntervalMs;
private final boolean urgent;
private final Map<Chunk, Long> lastUse;
private final ConcurrentLinkedQueue<ChunkUse> chunkUseQueue;
private final ConcurrentHashMap<Long, AtomicInteger> regionPending;
private final ConcurrentHashMap<Long, Queue<Chunk>> regionChunks;
private final KSet<Long> drainedRegions;
private final KSet<Long> evictedRegions;
private volatile int evictionWindowRegions;
private volatile int boundsMinRegionX;
private volatile int boundsMinRegionZ;
private volatile int boundsMaxRegionX;
private volatile int boundsMaxRegionZ;
private final AtomicInteger adaptiveInFlightLimit;
private final int adaptiveMinInFlightLimit;
private final AtomicInteger timeoutStreak = new AtomicInteger();
private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L);
private final AtomicLong lastFailedReleaseLogAt = new AtomicLong(0L);
private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger();
private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L);
private final AtomicInteger inFlight = new AtomicInteger();
@@ -86,14 +97,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private final AtomicLong completed = new AtomicLong();
private final AtomicLong failed = new AtomicLong();
private final AtomicLong lastProgressAt = new AtomicLong(M.ms());
private final AtomicLong lastChunkCleanup = new AtomicLong(M.ms());
private final AtomicBoolean chunkCleanupRunning = new AtomicBoolean(false);
private final Object permitMonitor = new Object();
private volatile Engine metricsEngine;
private volatile Mantle cachedMantle;
private final int maxResidentTectonicPlates;
private final int mantleBackpressureWaitMs;
private final long mantleBackpressureTimeoutMs;
private final PregenMantleBackpressure backpressure;
public AsyncPregenMethod(World world, int unusedThreads) {
if (!PaperLib.isPaper()) {
@@ -140,13 +147,25 @@ public class AsyncPregenMethod implements PregeneratorMethod {
this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds();
this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs();
this.urgent = false;
this.lastUse = new ConcurrentHashMap<>();
this.chunkUseQueue = new ConcurrentLinkedQueue<>();
this.regionPending = new ConcurrentHashMap<>();
this.regionChunks = new ConcurrentHashMap<>();
this.drainedRegions = new KSet<>();
this.evictedRegions = new KSet<>();
this.evictionWindowRegions = -1;
this.boundsMinRegionX = Integer.MIN_VALUE;
this.boundsMinRegionZ = Integer.MIN_VALUE;
this.boundsMaxRegionX = Integer.MAX_VALUE;
this.boundsMaxRegionZ = Integer.MAX_VALUE;
this.adaptiveInFlightLimit = new AtomicInteger(this.threads);
this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4)));
this.maxResidentTectonicPlates = pregen.getMaxResidentTectonicPlates();
this.mantleBackpressureWaitMs = pregen.getMantleBackpressureWaitMs();
this.mantleBackpressureTimeoutMs = pregen.getMantleBackpressureTimeoutMs();
int pregenWorldHeight = world.getMaxHeight() - world.getMinHeight();
this.backpressure = new PregenMantleBackpressure(
this::resolveMantle,
pregen.getEffectiveResidentTectonicPlates(pregenWorldHeight),
pregen.getMantleBackpressureWaitMs(),
pregen.getMantleBackpressureTimeoutMs(),
this::lowerAdaptiveInFlightLimit,
this::metricsSnapshot);
}
private IrisPaperLikeBackendMode resolvePaperLikeBackendMode(IrisSettings.IrisSettingsPregen pregen) {
@@ -173,124 +192,242 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return -1;
}
private void unloadAndSaveAllChunks() {
if (foliaRuntime) {
lastUse.clear();
chunkUseQueue.clear();
private static long rkey(int rx, int rz) {
return (((long) rx) << 32) | (rz & 0xFFFFFFFFL);
}
private int evictionWindow() {
int cached = evictionWindowRegions;
if (cached > 0) {
return cached;
}
Engine engine = resolveMetricsEngine();
if (engine == null) {
return 2;
}
try {
int radius = engine.getMantle().getRadius();
int resolved = radius > 0 ? Math.max(1, (int) Math.ceil(radius / 32.0)) : 2;
evictionWindowRegions = resolved;
return resolved;
} catch (Throwable ignored) {
return 2;
}
}
private void onChunkCompleted(int x, int z, Chunk chunk) {
if (chunk == null) {
return;
}
if (lastUse.isEmpty()) {
try {
long rk = rkey(x >> 5, z >> 5);
regionChunks.computeIfAbsent(rk, k -> new ConcurrentLinkedQueue<>()).add(chunk);
AtomicInteger pending = regionPending.get(rk);
if (pending != null && pending.decrementAndGet() == 0) {
onRegionDrained(rk);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void onChunkFailedToLoad(int x, int z) {
try {
long rk = rkey(x >> 5, z >> 5);
AtomicInteger pending = regionPending.get(rk);
if (pending != null && pending.decrementAndGet() == 0) {
onRegionDrained(rk);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
long now = M.ms();
long last = lastFailedReleaseLogAt.get();
if (now - last >= timeoutWarnIntervalMs && lastFailedReleaseLogAt.compareAndSet(last, now)) {
IrisLogging.warn("Released region slot for failed or timed out chunk at " + x + "," + z + ". " + metricsSnapshot());
}
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
boundsMinRegionX = minRegionX;
boundsMinRegionZ = minRegionZ;
boundsMaxRegionX = maxRegionX;
boundsMaxRegionZ = maxRegionZ;
}
private boolean inBounds(int rx, int rz) {
return rx >= boundsMinRegionX && rx <= boundsMaxRegionX && rz >= boundsMinRegionZ && rz <= boundsMaxRegionZ;
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
try {
long rk = rkey(regionX, regionZ);
AtomicInteger pending = regionPending.get(rk);
if (pending == null || pending.decrementAndGet() == 0) {
onRegionDrained(rk);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void onRegionDrained(long rk) {
if (!drainedRegions.add(rk)) {
return;
}
int w = evictionWindow();
int rx = (int) (rk >> 32);
int rz = (int) rk;
for (int dx = -w; dx <= w; dx++) {
for (int dz = -w; dz <= w; dz++) {
int cx = rx + dx;
int cz = rz + dz;
long candidate = rkey(cx, cz);
if (evictedRegions.contains(candidate)) {
continue;
}
if (!drainedRegions.contains(candidate)) {
continue;
}
if (allNeighborsDrained(cx, cz, w)) {
evictRegion(candidate);
}
}
}
}
private boolean allNeighborsDrained(int rx, int rz, int w) {
for (int dx = -w; dx <= w; dx++) {
for (int dz = -w; dz <= w; dz++) {
int nx = rx + dx;
int nz = rz + dz;
if (!inBounds(nx, nz)) {
continue;
}
if (!drainedRegions.contains(rkey(nx, nz))) {
return false;
}
}
}
return true;
}
private void evictRegion(long c) {
if (!evictedRegions.add(c)) {
return;
}
regionPending.remove(c);
Queue<Chunk> chunks = regionChunks.remove(c);
if (chunks == null || chunks.isEmpty()) {
return;
}
try {
if (foliaRuntime) {
Chunk anchor = null;
for (Chunk chunk : chunks) {
if (chunk == null) {
continue;
}
if (anchor == null) {
anchor = chunk;
}
int cx = chunk.getX();
int cz = chunk.getZ();
if (!J.runRegion(world, cx, cz, () -> unloadChunkSafely(cx, cz))) {
unloadChunkSafely(cx, cz);
}
}
if (anchor != null) {
int ax = anchor.getX();
int az = anchor.getZ();
if (!J.runRegion(world, ax, az, () -> INMS.get().flushChunkIO(world))) {
INMS.get().flushChunkIO(world);
}
}
return;
}
J.s(() -> {
for (Chunk chunk : chunks) {
if (chunk != null) {
unloadChunkSafely(chunk.getX(), chunk.getZ());
}
}
INMS.get().flushChunkIO(world);
});
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void unloadChunkSafely(int cx, int cz) {
try {
world.removePluginChunkTicket(cx, cz, BukkitPlatform.plugin());
} catch (Throwable ignored) {
}
try {
if (!INMS.get().saveAndUnloadChunk(world, cx, cz)) {
world.unloadChunk(cx, cz, true);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
private void flushAllRemainingChunks() {
List<Long> keys = new ArrayList<>(regionChunks.keySet());
if (foliaRuntime) {
for (Long rk : keys) {
evictRegion(rk);
}
return;
}
try {
J.sfut(() -> {
if (world == null) {
IrisLogging.warn("World was null somehow...");
return;
for (Long rk : keys) {
if (!evictedRegions.add(rk)) {
continue;
}
regionPending.remove(rk);
Queue<Chunk> chunks = regionChunks.remove(rk);
if (chunks == null) {
continue;
}
for (Chunk chunk : chunks) {
if (chunk != null) {
unloadChunkSafely(chunk.getX(), chunk.getZ());
}
}
}
long minTime = M.ms() - 10_000;
AtomicBoolean unloaded = new AtomicBoolean(false);
lastUse.entrySet().removeIf(i -> {
final Chunk chunk = i.getKey();
final Long lastUseTime = i.getValue();
if (!chunk.isLoaded() || lastUseTime == null)
return true;
if (lastUseTime < minTime) {
chunk.unload();
unloaded.set(true);
return true;
}
return false;
});
if (unloaded.get()) {
world.save();
}
if (lastUse.isEmpty()) {
chunkUseQueue.clear();
}
world.save();
INMS.get().flushChunkIO(world);
}).get();
} catch (Throwable e) {
e.printStackTrace();
IrisLogging.reportError(e);
}
}
private void periodicChunkCleanup() {
long now = M.ms();
long lastCleanup = lastChunkCleanup.get();
if (now - lastCleanup < CHUNK_CLEANUP_INTERVAL_MS) {
return;
}
if (!lastChunkCleanup.compareAndSet(lastCleanup, now)) {
return;
}
if (foliaRuntime) {
int sizeBefore = lastUse.size();
if (sizeBefore > 0) {
lastUse.clear();
chunkUseQueue.clear();
IrisLogging.info("Periodic chunk cleanup: cleared " + sizeBefore + " Folia chunk references");
}
return;
}
int sizeBefore = lastUse.size();
if (sizeBefore == 0) {
return;
}
if (!chunkCleanupRunning.compareAndSet(false, true)) {
return;
}
long minTime = now - CHUNK_CLEANUP_MIN_AGE_MS;
J.a(() -> {
try {
int removedCount = cleanupQueuedChunkUses(minTime);
if (removedCount > 0) {
IrisLogging.info("Periodic chunk cleanup: removed " + removedCount + "/" + sizeBefore + " stale chunk references");
}
} finally {
chunkCleanupRunning.set(false);
}
});
}
private int cleanupQueuedChunkUses(long minTime) {
int removed = 0;
while (true) {
ChunkUse chunkUse = chunkUseQueue.peek();
if (chunkUse == null) {
return removed;
}
Long latestUse = lastUse.get(chunkUse.chunk());
if (latestUse != null && latestUse > chunkUse.lastUseTime()) {
chunkUseQueue.poll();
continue;
}
if (latestUse != null && latestUse >= minTime) {
return removed;
}
chunkUseQueue.poll();
if (latestUse != null && lastUse.remove(chunkUse.chunk(), latestUse)) {
removed++;
}
}
}
private void recordChunkUse(Chunk chunk) {
long now = M.ms();
lastUse.put(chunk, now);
chunkUseQueue.offer(new ChunkUse(chunk, now));
}
private Chunk onChunkFutureFailure(int x, int z, Throwable throwable) {
Throwable root = throwable;
while (root.getCause() != null) {
@@ -527,56 +664,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return resolved;
}
private void enforceMantleBudget() {
int cap = maxResidentTectonicPlates;
if (cap <= 0) {
return;
}
Mantle mantle = resolveMantle();
if (mantle == null) {
return;
}
int hardCap = cap * 2;
if (mantle.getLoadedRegionCount() <= hardCap) {
return;
}
long waitStart = M.ms();
long lastLog = 0L;
while (mantle.getLoadedRegionCount() > hardCap) {
mantle.trim(0L, 0);
int freed = mantle.unloadTectonicPlate(0);
int resident = mantle.getLoadedRegionCount();
if (resident <= hardCap) {
break;
}
long elapsed = M.ms() - waitStart;
if (elapsed >= mantleBackpressureTimeoutMs) {
IrisLogging.warn("Pregen mantle backpressure exceeded " + mantleBackpressureTimeoutMs + "ms with " + resident
+ " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. "
+ "Raise pregen.maxResidentTectonicPlates if this persists. " + metricsSnapshot());
return;
}
long logNow = M.ms();
if (logNow - lastLog >= 5_000L) {
lastLog = logNow;
IrisLogging.warn("Pregen mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
}
try {
Thread.sleep(mantleBackpressureWaitMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
@Override
public void init() {
IrisLogging.info("Async pregen init: world=" + world.getName()
@@ -591,7 +678,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
+ ", recommendedCap=" + recommendedRuntimeConcurrencyCap
+ ", urgent=" + urgent
+ ", timeout=" + timeoutSeconds + "s");
unloadAndSaveAllChunks();
if (workerPoolThreads > 0) {
increaseWorkerThreads();
}
@@ -610,14 +696,13 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override
public void close() {
semaphore.acquireUninterruptibly(threads);
unloadAndSaveAllChunks();
flushAllRemainingChunks();
executor.shutdown();
resetWorkerThreads();
}
@Override
public void save() {
unloadAndSaveAllChunks();
}
@Override
@@ -633,8 +718,8 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override
public void generateChunk(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z);
periodicChunkCleanup();
enforceMantleBudget();
backpressure.enforceMantleBudget();
backpressure.awaitHeapHeadroom();
try {
long waitStart = M.ms();
synchronized (permitMonitor) {
@@ -659,6 +744,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
return;
}
regionPending.computeIfAbsent(rkey(x >> 5, z >> 5), k -> new AtomicInteger(1)).incrementAndGet();
markSubmitted();
executor.generate(x, z, listener);
}
@@ -833,6 +919,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
.whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable)))) {
markFinished(false);
semaphore.release();
listener.onChunkFailed(x, z);
IrisLogging.warn("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot());
}
}
@@ -842,17 +929,21 @@ public class AsyncPregenMethod implements PregeneratorMethod {
try {
if (throwable != null) {
onChunkFutureFailure(x, z, throwable);
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
if (chunk == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
recordChunkUse(chunk);
onChunkCompleted(x, z, chunk);
success = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
@@ -877,13 +968,15 @@ public class AsyncPregenMethod implements PregeneratorMethod {
.get();
if (i == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
recordChunkUse(i);
onChunkCompleted(x, z, i);
success = true;
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
@@ -913,13 +1006,15 @@ public class AsyncPregenMethod implements PregeneratorMethod {
boolean success = false;
try {
if (i == null) {
onChunkFailedToLoad(x, z);
listener.onChunkFailed(x, z);
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
recordChunkUse(i);
onChunkCompleted(x, z, i);
success = true;
} finally {
markFinished(success);
@@ -929,9 +1024,6 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
}
private record ChunkUse(Chunk chunk, long lastUseTime) {
}
private record ChunkAsyncMethodSelection(Method urgentMethod, Method standardMethod, String mode) {
}
}
@@ -1,27 +1,22 @@
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.service.GlobalCacheSVC;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class CachedPregenMethod implements PregeneratorMethod {
private final PregeneratorMethod method;
private final PregenCache cache;
private final PregenTask task;
private volatile PregenListener wrappedSource;
private volatile PregenListener wrappedListener;
public CachedPregenMethod(PregeneratorMethod method, String worldName) {
public CachedPregenMethod(PregeneratorMethod method, PregenCache cache, PregenTask task) {
this.method = method;
PregenCache cache = IrisServices.get(GlobalCacheSVC.class).get(worldName);
if (cache == null) {
IrisLogging.debug("Could not find existing cache for " + worldName + " creating fallback");
cache = GlobalCacheSVC.createDefault(worldName);
}
this.cache = cache;
this.cache = cache.sync();
this.task = task;
}
@Override
@@ -31,6 +26,7 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public void close() {
cache.write();
method.close();
cache.write();
}
@@ -48,6 +44,9 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public String getMethod(int x, int z) {
if (cache.isRegionCached(x, z)) {
return "Cached";
}
return method.getMethod(x, z);
}
@@ -55,14 +54,10 @@ public class CachedPregenMethod implements PregeneratorMethod {
public void generateRegion(int x, int z, PregenListener listener) {
if (cache.isRegionCached(x, z)) {
listener.onRegionGenerated(x, z);
int rX = x << 5, rZ = z << 5;
for (int cX = 0; cX < 32; cX++) {
for (int cZ = 0; cZ < 32; cZ++) {
listener.onChunkGenerated(rX + cX, rZ + cZ, true);
listener.onChunkCleaned(rX + cX, rZ + cZ);
}
}
task.iterateChunks(x, z, (cX, cZ) -> {
listener.onChunkGenerated(cX, cZ, true);
listener.onChunkCleaned(cX, cZ);
});
return;
}
method.generateRegion(x, z, listener);
@@ -76,8 +71,113 @@ public class CachedPregenMethod implements PregeneratorMethod {
listener.onChunkCleaned(x, z);
return;
}
method.generateChunk(x, z, listener);
cache.cacheChunk(x, z);
method.generateChunk(x, z, cachingListener(listener));
}
private PregenListener cachingListener(PregenListener listener) {
PregenListener source = wrappedSource;
PregenListener wrapped = wrappedListener;
if (source == listener && wrapped != null) {
return wrapped;
}
PregenListener created = new PregenListener() {
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
listener.onTick(chunksPerSecond, chunksPerMinute, regionsPerMinute, percent, generated, totalChunks, chunksRemaining, eta, elapsed, method, cached);
}
@Override
public void onChunkGenerating(int x, int z) {
listener.onChunkGenerating(x, z);
}
@Override
public void onChunkGenerated(int x, int z, boolean cachedChunk) {
if (!cachedChunk) {
cache.cacheChunk(x, z);
}
listener.onChunkGenerated(x, z, cachedChunk);
}
@Override
public void onChunkFailed(int x, int z) {
listener.onChunkFailed(x, z);
}
@Override
public void onRegionGenerated(int x, int z) {
listener.onRegionGenerated(x, z);
}
@Override
public void onRegionGenerating(int x, int z) {
listener.onRegionGenerating(x, z);
}
@Override
public void onChunkCleaned(int x, int z) {
listener.onChunkCleaned(x, z);
}
@Override
public void onRegionSkipped(int x, int z) {
listener.onRegionSkipped(x, z);
}
@Override
public void onNetworkStarted(int x, int z) {
listener.onNetworkStarted(x, z);
}
@Override
public void onNetworkFailed(int x, int z) {
listener.onNetworkFailed(x, z);
}
@Override
public void onNetworkReclaim(int revert) {
listener.onNetworkReclaim(revert);
}
@Override
public void onNetworkGeneratedChunk(int x, int z) {
listener.onNetworkGeneratedChunk(x, z);
}
@Override
public void onNetworkDownloaded(int x, int z) {
listener.onNetworkDownloaded(x, z);
}
@Override
public void onClose() {
listener.onClose();
}
@Override
public void onSaving() {
listener.onSaving();
}
@Override
public void onChunkExistsInRegionGen(int x, int z) {
listener.onChunkExistsInRegionGen(x, z);
}
};
wrappedSource = listener;
wrappedListener = created;
return created;
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
method.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
method.onRegionSubmitted(regionX, regionZ);
}
@Override
@@ -67,6 +67,16 @@ public class HybridPregenMethod implements PregeneratorMethod {
inWorld.generateChunk(x, z, listener);
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
inWorld.onRegionBounds(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onRegionSubmitted(int regionX, int regionZ) {
inWorld.onRegionSubmitted(regionX, regionZ);
}
@Override
public Mantle getMantle() {
return inWorld.getMantle();
@@ -0,0 +1,69 @@
/*
* Iris is a World Generator for Minecraft 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.project;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.stream.Stream;
public final class IrisProjectCopier {
private IrisProjectCopier() {
}
public static void copyProject(File sourcePack, File targetPack, String sourceKey, String targetKey) throws IOException {
Path source = sourcePack.toPath();
try (Stream<Path> walk = Files.walk(source)) {
for (Path path : walk.sorted(Comparator.naturalOrder()).toList()) {
String relative = source.relativize(path).toString();
if (relative.isEmpty() || relative.equals(".git") || relative.startsWith(".git" + File.separator) || relative.endsWith(".code-workspace")) {
continue;
}
Path destination = targetPack.toPath().resolve(relative);
if (Files.isDirectory(path)) {
Files.createDirectories(destination);
} else {
Files.createDirectories(destination.getParent());
Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
}
File oldDimension = new File(targetPack, "dimensions/" + sourceKey + ".json");
File newDimension = new File(targetPack, "dimensions/" + targetKey + ".json");
if (oldDimension.isFile() && !oldDimension.equals(newDimension)) {
Files.copy(oldDimension.toPath(), newDimension.toPath(), StandardCopyOption.REPLACE_EXISTING);
Files.delete(oldDimension.toPath());
}
if (newDimension.isFile()) {
JSONObject json = new JSONObject(IO.readAll(newDimension));
if (json.has("name")) {
json.put("name", Form.capitalizeWords(targetKey.replaceAll("\\Q-\\E", " ")));
IO.writeAll(newDimension, json.toString(4));
}
}
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.core.runtime;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.math.ChunkSpiral;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.Form;
@@ -29,8 +30,6 @@ import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -61,19 +60,7 @@ public final class ChunkJobReporter {
}
public static List<int[]> orderedTargets(int centerChunkX, int centerChunkZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerChunkX + dx, centerChunkZ + dz});
}
}
targets.sort(Comparator.comparingInt(t -> {
int ox = t[0] - centerChunkX;
int oz = t[1] - centerChunkZ;
return ox * ox + oz * oz;
}));
return targets;
return ChunkSpiral.centerOut(centerChunkX, centerChunkZ, radius);
}
public void start() {
@@ -0,0 +1,484 @@
/*
* 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.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.ChunkSpiral;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
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 GoldenHashEngine {
public enum Mode {
AUTO,
CAPTURE,
VERIFY
}
public interface ChunkSnapshot {
int minY();
int maxY();
PlatformBlockState block(int x, int y, int z);
PlatformBiome biome(int x, int y, int z);
}
public interface ChunkSource {
ChunkSnapshot generate(int chunkX, int chunkZ) throws Exception;
}
public interface Feedback {
void ok(String message);
void warn(String message);
void fail(String message);
}
public interface Progress {
default void stage(String stage) {
}
default void total(int total) {
}
default void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) {
}
default void chunkFailed(int chunkX, int chunkZ, Throwable error) {
}
}
public record Request(String worldName, long seed, String mcVersion, int minY, int maxY, int centerChunkX,
int centerChunkZ, int radius, int threads, Mode mode, boolean resetMantle, boolean deep,
String nullBiomeKey) {
}
private static final AtomicInteger ACTIVE_SCANS = new AtomicInteger(0);
private static final String FORMAT = "iris-goldenhash v1";
private static final int BIOME_STEP = 4;
private static final int MAX_REPORTED_MISMATCHES = 10;
private final Engine engine;
private final Request request;
private final ChunkSource source;
private final Feedback feedback;
private final Progress progress;
private final int radius;
private final int threads;
private final File goldenFile;
public GoldenHashEngine(Engine engine, Request request, File goldenDir, ChunkSource source, Feedback feedback, Progress progress) {
this.engine = engine;
this.request = request;
this.source = source;
this.feedback = feedback;
this.progress = progress;
this.radius = Math.max(0, request.radius());
this.threads = Math.max(1, request.threads());
goldenDir.mkdirs();
this.goldenFile = new File(goldenDir, engine.getDimension().getLoadKey()
+ "-s" + request.seed()
+ "-c" + request.centerChunkX() + "x" + request.centerChunkZ()
+ "-r" + this.radius + ".hashes");
}
public static boolean isActive() {
return ACTIVE_SCANS.get() > 0;
}
public File getGoldenFile() {
return goldenFile;
}
public boolean run() {
ACTIVE_SCANS.incrementAndGet();
try {
boolean exists = goldenFile.exists();
if (request.mode() == Mode.VERIFY && !exists) {
feedback.fail("No golden capture at " + goldenFile.getAbsolutePath() + "; run a capture first.");
return false;
}
if (request.resetMantle()) {
progress.stage("Resetting mantle");
resetMantleFull();
}
List<int[]> targets = ChunkSpiral.centerOut(request.centerChunkX(), request.centerChunkZ(), radius);
progress.total(targets.size());
progress.stage("Generating");
Map<Long, String> lines = scan(targets);
if (lines.size() != targets.size()) {
feedback.fail("GoldenHash aborted: " + (targets.size() - lines.size()) + " chunk(s) failed to generate. No golden file written.");
return false;
}
progress.stage("Comparing");
if (request.mode() == Mode.CAPTURE || (request.mode() == Mode.AUTO && !exists)) {
capture(lines);
return true;
}
return verify(lines);
} catch (Throwable e) {
IrisLogging.reportError(e);
feedback.fail("GoldenHash failed: " + e);
return false;
} finally {
ACTIVE_SCANS.decrementAndGet();
}
}
private void resetMantleFull() {
try {
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();
}
}
}
feedback.ok("Mantle reset (" + folder.getAbsolutePath() + ")");
} catch (Throwable e) {
IrisLogging.reportError(e);
feedback.warn("Mantle reset failed (" + e.getClass().getSimpleName() + "); continuing with existing mantle state.");
}
}
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
Map<Long, String> lines = new ConcurrentHashMap<>();
Semaphore inFlight = new Semaphore(threads);
CountDownLatch done = new CountDownLatch(targets.size());
AtomicInteger completed = new AtomicInteger();
int total = targets.size();
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
boolean ok = false;
try {
ChunkSnapshot snapshot = source.generate(chunkX, chunkZ);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, snapshot));
if (request.deep()) {
writeDeepDump(chunkX, chunkZ, snapshot);
}
ok = true;
} catch (Throwable e) {
IrisLogging.reportError(e);
progress.chunkFailed(chunkX, chunkZ, e);
} finally {
progress.chunkDone(chunkX, chunkZ, ok, completed.incrementAndGet(), total);
inFlight.release();
done.countDown();
}
});
}
done.await();
return lines;
}
private String hashChunk(int chunkX, int chunkZ, ChunkSnapshot snapshot) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
int minY = snapshot.minY();
int maxY = snapshot.maxY();
IdentityHashMap<PlatformBlockState, byte[]> blockCache = new IdentityHashMap<>();
Map<PlatformBiome, byte[]> biomeCache = new HashMap<>();
byte[] nullBiome = (request.nullBiomeKey() + "\n").getBytes(StandardCharsets.UTF_8);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = minY; y < maxY; y++) {
PlatformBlockState data = snapshot.block(x, y, z);
byte[] bytes = blockCache.computeIfAbsent(data, (PlatformBlockState d) -> (d.key() + "\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) {
PlatformBiome biome = snapshot.biome(x, y, z);
byte[] bytes = biome == null
? nullBiome
: biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\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=" + request.worldName());
out.add("#dim=" + engine.getDimension().getLoadKey());
out.add("#seed=" + request.seed());
out.add("#mc=" + request.mcVersion());
out.add("#minY=" + request.minY() + " maxY=" + request.maxY());
out.add("#center=" + request.centerChunkX() + "," + request.centerChunkZ());
out.add("#radius=" + radius);
out.addAll(body);
out.add("#combined=" + combined);
Files.write(goldenFile.toPath(), out, StandardCharsets.UTF_8);
feedback.ok("Golden captured: " + body.size() + " chunks combined=" + shortHash(combined));
feedback.ok(goldenFile.getAbsolutePath());
IrisLogging.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(request.seed());
String expectedDim = engine.getDimension().getLoadKey();
if (!expectedSeed.equals(meta.get("seed")) || !expectedDim.equals(meta.get("dim"))) {
feedback.fail("Golden file is for dim=" + meta.get("dim") + " seed=" + meta.get("seed")
+ " but this world is dim=" + expectedDim + " seed=" + expectedSeed + ". Aborting.");
return false;
}
if (!request.mcVersion().equals(meta.get("mc"))) {
feedback.warn("Golden was captured on mc=" + meta.get("mc") + ", running mc=" + request.mcVersion() + ". 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()) {
feedback.ok("GOLDEN MATCH: " + body.size() + "/" + goldenChunks.size() + " chunks, combined=" + shortHash(combined));
IrisLogging.info("goldenhash MATCH: " + goldenFile.getName() + " combined=" + combined);
return true;
}
feedback.fail("GOLDEN MISMATCH: " + mismatches.size() + "/" + body.size() + " chunks differ.");
for (int i = 0; i < Math.min(MAX_REPORTED_MISMATCHES, mismatches.size()); i++) {
feedback.fail(" chunk " + mismatches.get(i));
}
if (mismatches.size() > MAX_REPORTED_MISMATCHES) {
feedback.fail(" ... 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);
feedback.warn("Current hashes written to " + current.getName());
IrisLogging.info("goldenhash MISMATCH: " + mismatches.size() + "/" + body.size() + " -> " + current.getAbsolutePath());
progress.stage("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]);
ChunkSnapshot first = source.generate(chunkX, chunkZ);
ChunkSnapshot second = source.generate(chunkX, chunkZ);
int minY = first.minY();
int maxY = first.maxY();
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.block(x, y, z).key();
String b = second.block(x, y, z).key();
if (!a.equals(b)) {
diffs.add(x + " " + y + " " + z + " | " + a + " | " + b);
}
}
}
}
List<String> mantleDiffs = new ArrayList<>();
String mantleStatus;
try {
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);
}
}
ChunkSnapshot reset = source.generate(chunkX, chunkZ);
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.block(x, y, z).key();
String b = reset.block(x, y, z).key();
if (!a.equals(b)) {
mantleDiffs.add(x + " " + y + " " + z + " | scan: " + a + " | mantle-reset: " + b);
}
}
}
}
mantleStatus = mantleDiffs.isEmpty() ? "STABLE (mantle rebuild reproduces scan output)" : "DIVERGED (" + mantleDiffs.size() + "+ diffs - mantle build is state/order dependent)";
} catch (Throwable t) {
mantleDiffs.clear();
mantleStatus = "SKIPPED (" + t.getClass().getSimpleName() + ")";
}
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: " + mantleStatus);
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.block(x, y, z).key();
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);
String repeatPart = diffs.isEmpty() ? "Repeat-gen STABLE" : "Repeat-gen UNSTABLE (" + diffs.size() + "+ block diffs)";
String mantlePart = "mantle-reset " + mantleStatus;
if (diffs.isEmpty() && mantleDiffs.isEmpty() && mantleStatus.startsWith("STABLE")) {
feedback.warn(repeatPart + ", " + mantlePart + " -> " + diag.getName());
} else {
feedback.fail(repeatPart + ", " + mantlePart + " -> " + diag.getName());
}
IrisLogging.info("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath());
} catch (Throwable e) {
IrisLogging.reportError(e);
feedback.fail("Diagnosis failed: " + e.getMessage());
}
}
private void writeDeepDump(int chunkX, int chunkZ, ChunkSnapshot snapshot) throws IOException {
File dir = new File(goldenFile.getParentFile(), goldenFile.getName() + (goldenFile.exists() ? ".deep-verify" : ".deep"));
dir.mkdirs();
int minY = snapshot.minY();
int maxY = snapshot.maxY();
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 = snapshot.block(x, y, z).key();
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 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 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);
}
}
}
@@ -18,76 +18,47 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
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.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
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 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;
private final GoldenHashEngine hashEngine;
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 = IrisPlatforms.get().dataFile("golden", engine.getDimension().getLoadKey()
+ "-s" + world.getSeed()
+ "-c" + centerChunkX + "x" + centerChunkZ
+ "-r" + this.radius + ".hashes");
this.reporter = new ChunkJobReporter(sender, "GoldenHash", world);
GoldenHashEngine.Request request = new GoldenHashEngine.Request(
world.getName(),
world.getSeed(),
Bukkit.getBukkitVersion(),
world.getMinHeight(),
world.getMaxHeight(),
centerChunkX,
centerChunkZ,
radius,
threads,
GoldenHashEngine.Mode.AUTO,
resetMantle,
deep,
"null");
this.hashEngine = new GoldenHashEngine(engine, request, IrisPlatforms.get().dataFolder("golden"), this::snapshot, feedback(), progress());
}
public static boolean isScanActive() {
return GoldenHashEngine.isActive();
}
public void start() {
@@ -98,334 +69,75 @@ public final class GoldenHashScanner {
}
private void run() {
boolean error = false;
ACTIVE_SCANS.incrementAndGet();
boolean ok = false;
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) {
IrisLogging.reportError(e);
error = true;
ok = hashEngine.run();
} finally {
ACTIVE_SCANS.decrementAndGet();
reporter.finish(error);
reporter.finish(!ok);
}
}
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) {
IrisLogging.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<PlatformBlockState, byte[]> blockCache = new IdentityHashMap<>();
Map<PlatformBiome, 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++) {
PlatformBlockState data = buffer.getBlockData(x, y, z);
byte[] bytes = blockCache.computeIfAbsent(data, (PlatformBlockState d) -> (d.key() + "\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) {
PlatformBiome biome = buffer.getBiome(x, y, z);
byte[] bytes = biome == null
? NULL_BIOME
: biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\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());
IrisLogging.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));
IrisLogging.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());
IrisLogging.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).key();
String b = second.getBlockData(x, y, z).key();
if (!a.equals(b)) {
diffs.add(x + " " + y + " " + z + " | " + a + " | " + b);
}
}
}
private GoldenHashEngine.ChunkSnapshot snapshot(int chunkX, int chunkZ) throws Exception {
TerrainChunk buffer = TerrainChunk.create(world);
engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
return new GoldenHashEngine.ChunkSnapshot() {
@Override
public int minY() {
return buffer.getMinHeight();
}
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).key();
String b = reset.getBlockData(x, y, z).key();
if (!a.equals(b)) {
mantleDiffs.add(x + " " + y + " " + z + " | scan: " + a + " | mantle-reset: " + b);
}
}
}
@Override
public int maxY() {
return buffer.getMaxHeight();
}
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).key();
if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
report.add(x + " " + y + " " + z + " " + state);
}
}
}
@Override
public PlatformBlockState block(int x, int y, int z) {
return buffer.getBlockData(x, y, z);
}
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());
IrisLogging.info("goldenhash diag: chunk=" + chunkX + "," + chunkZ + " repeatStable=" + diffs.isEmpty() + " -> " + diag.getAbsolutePath());
} catch (Throwable e) {
IrisLogging.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).key();
if (!state.equals("minecraft:air") && !state.equals("minecraft:cave_air") && !state.equals("minecraft:void_air")) {
out.add(x + " " + y + " " + z + " " + state);
}
}
@Override
public PlatformBiome biome(int x, int y, int z) {
return buffer.getBiome(x, y, z);
}
}
Files.write(new File(dir, chunkX + "_" + chunkZ + ".txt").toPath(), out, StandardCharsets.UTF_8);
};
}
private static String shortHash(String hex) {
return hex.substring(0, 12);
private GoldenHashEngine.Feedback feedback() {
return new GoldenHashEngine.Feedback() {
@Override
public void ok(String message) {
sender.sendMessage(C.GREEN + message);
}
@Override
public void warn(String message) {
sender.sendMessage(C.YELLOW + message);
}
@Override
public void fail(String message) {
sender.sendMessage(C.RED + message);
}
};
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
}
private GoldenHashEngine.Progress progress() {
return new GoldenHashEngine.Progress() {
@Override
public void stage(String stage) {
reporter.setStage(stage);
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
@Override
public void total(int total) {
reporter.setTotal(total);
}
@Override
public void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) {
reporter.countApplied(ok);
}
};
}
}
@@ -1,16 +1,13 @@
package art.arcane.iris.core.safeguard;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.splash.IrisSplashComposer;
import art.arcane.iris.core.splash.IrisSplashRenderer;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
import org.bukkit.Bukkit;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public enum Mode {
@@ -54,36 +51,44 @@ public enum Mode {
}
public void splash() {
String padd = Form.repeat(" ", 4);
String padd2 = Form.repeat(" ", 4);
String version = BukkitPlatform.plugin().getDescription().getVersion();
String releaseTrain = getReleaseTrain(version);
String serverVersion = getServerVersion();
String startupDate = getStartupDate();
int javaVersion = getJavaVersion();
String[] splash = IrisSplashRenderer.render(this::splashTone);
String[] info = IrisSplashComposer.composeInfo(version, getServerVersion(), infoStyle());
IrisLogging.info(IrisSplashComposer.compose(splash, info));
}
String[] info = new String[]{
"",
padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1.1.6]",
padd2 + C.GRAY + " Version: " + color + version,
padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)",
padd2 + C.GRAY + " Server: " + color + serverVersion,
padd2 + C.GRAY + " Java: " + color + javaVersion + C.GRAY + " | Date: " + color + startupDate,
padd2 + C.GRAY + " Commit: " + color + BuildConstants.COMMIT + C.GRAY + "/" + color + BuildConstants.ENVIRONMENT,
"",
"",
"",
""
private IrisSplashComposer.InfoStyle infoStyle() {
return new IrisSplashComposer.InfoStyle() {
@Override
public String linePrefix() {
return " ".repeat(4);
}
@Override
public String title(String text) {
return color + text;
}
@Override
public String subtitle(String text) {
return C.AQUA + text;
}
@Override
public String tag(String text) {
return C.RED + text;
}
@Override
public String label(String text) {
return C.GRAY + text;
}
@Override
public String value(String text) {
return color + text;
}
};
StringBuilder builder = new StringBuilder("\n\n");
for (int i = 0; i < splash.length; i++) {
builder.append(padd).append(splash[i]).append(info[i]).append("\n");
}
IrisLogging.info(builder.toString());
}
private String splashTone(char glyph) {
@@ -108,34 +113,4 @@ public enum Mode {
}
return version;
}
private int getJavaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf('.');
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
private String getStartupDate() {
return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
}
private String getReleaseTrain(String version) {
String value = version;
int suffixIndex = value.indexOf('-');
if (suffixIndex >= 0) {
value = value.substring(0, suffixIndex);
}
String[] split = value.split("\\.");
if (split.length >= 2) {
return split[0] + "." + split[1];
}
return value;
}
}
@@ -26,9 +26,11 @@ import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.IrisPack;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.project.IrisProjectCopier;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.AtomicCache;
@@ -36,7 +38,6 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONException;
import art.arcane.volmlib.util.json.JSONObject;
@@ -52,7 +53,6 @@ import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
@@ -259,99 +259,12 @@ public class StudioSVC implements IrisService {
}
public void download(VolmitSender sender, String repo, String branch, boolean forceOverwrite, boolean directUrl) throws JsonSyntaxException, IOException {
String url = directUrl ? branch : "https://codeload.github.com/" + repo + "/zip/refs/heads/" + branch;
sender.sendMessage("Downloading " + url + " "); //The extra space stops a bug in adventure API from repeating the last letter of the URL
File zip = art.arcane.iris.util.common.misc.WebCache.getNonCachedFile("pack-" + repo, url);
File temp = art.arcane.iris.util.common.misc.WebCache.getTemp();
File work = new File(temp, "dl-" + UUID.randomUUID());
File packs = getWorkspaceFolder();
String key = PackDownloader.download(getWorkspaceFolder(), repo, branch, forceOverwrite, directUrl, sender::sendMessage);
if (zip == null || !zip.exists()) {
sender.sendMessage("Failed to find pack at " + url);
sender.sendMessage("Make sure you specified the correct repo and branch!");
sender.sendMessage("For example: /iris download overworld branch=stable");
return;
}
sender.sendMessage("Unpacking " + repo);
try {
ZipUtil.unpack(zip, work);
} catch (Throwable e) {
IrisLogging.reportError(e);
e.printStackTrace();
sender.sendMessage(
"""
Issue when unpacking. Please check/do the following:
1. Do you have a functioning internet connection?
2. Did the download corrupt?
3. Try deleting the */plugins/iris/packs folder and re-download.
4. Download the pack from the GitHub repo: https://github.com/IrisDimensions/overworld
5. Contact support (if all other options do not help)"""
);
}
File dir = null;
File[] zipFiles = work.listFiles();
if (zipFiles == null) {
sender.sendMessage("No files were extracted from the zip file.");
if (key == null) {
return;
}
try {
dir = zipFiles.length > 1 ? work : zipFiles[0].isDirectory() ? zipFiles[0] : null;
} catch (NullPointerException e) {
IrisLogging.reportError(e);
sender.sendMessage("Error when finding home directory. Are there any non-text characters in the file name?");
return;
}
if (dir == null) {
sender.sendMessage("Invalid Format. Missing root folder or too many folders!");
return;
}
IrisData data = IrisData.get(dir);
String[] dimensions = data.getDimensionLoader().getPossibleKeys();
if (dimensions == null || dimensions.length == 0) {
sender.sendMessage("No dimension file found in the extracted zip file.");
sender.sendMessage("Check it is there on GitHub and report this to staff!");
} else if (dimensions.length != 1) {
sender.sendMessage("Dimensions folder must have 1 file in it");
return;
}
IrisDimension d = data.getDimensionLoader().load(dimensions[0]);
data.close();
if (d == null) {
sender.sendMessage("Invalid dimension (folder) in dimensions folder");
return;
}
String key = d.getLoadKey();
sender.sendMessage("Importing " + d.getName() + " (" + key + ")");
File packEntry = new File(packs, key);
if (forceOverwrite) {
IO.delete(packEntry);
}
if (IrisData.loadAnyDimension(key, null) != null) {
sender.sendMessage("Another dimension in the packs folder is already using the key " + key + " IMPORT FAILED!");
return;
}
if (packEntry.exists() && packEntry.listFiles().length > 0) {
sender.sendMessage("Another pack is using the key " + key + ". IMPORT FAILED!");
return;
}
FileUtils.copyDirectory(dir, packEntry);
IrisData.getLoaded(packEntry)
.ifPresent(IrisData::hotloaded);
sender.sendMessage("Successfully Aquired " + d.getName());
ServerConfigurator.installDataPacks(true);
}
@@ -557,32 +470,7 @@ public class StudioSVC implements IrisService {
}
try {
FileUtils.copyDirectory(importPack, newPack, pathname -> !pathname.getAbsolutePath().contains(".git"), false);
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
new File(importPack, existingPack + ".code-workspace").delete();
File dimFile = new File(importPack, "dimensions/" + existingPack + ".json");
File newDimFile = new File(newPack, "dimensions/" + newName + ".json");
try {
FileUtils.copyFile(dimFile, newDimFile);
} catch (IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
}
new File(newPack, "dimensions/" + existingPack + ".json").delete();
try {
JSONObject json = new JSONObject(IO.readAll(newDimFile));
if (json.has("name")) {
json.put("name", Form.capitalizeWords(newName.replaceAll("\\Q-\\E", " ")));
IO.writeAll(newDimFile, json.toString(4));
}
IrisProjectCopier.copyProject(importPack, newPack, existingPack, newName);
} catch (JSONException | IOException e) {
IrisLogging.reportError(e);
e.printStackTrace();
@@ -0,0 +1,116 @@
package art.arcane.iris.core.splash;
import art.arcane.iris.BuildConstants;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public final class IrisSplashComposer {
public static final String RELEASE_TAG = "RC.1.1.6";
private static final String SPLASH_PADDING = " ".repeat(4);
private IrisSplashComposer() {
}
public static String[] composeInfo(String version, String serverLine, InfoStyle style) {
String releaseTrain = releaseTrain(version);
String prefix = style.linePrefix();
return new String[]{
"",
prefix + style.title(" Iris, ") + style.subtitle("Dimension Engine ") + style.tag("[" + releaseTrain + " " + RELEASE_TAG + "]"),
prefix + style.label(" Version: ") + style.value(version),
prefix + style.label(" By: ") + style.value("Volmit Software (Arcane Arts)"),
prefix + style.label(" Server: ") + style.value(serverLine),
prefix + style.label(" Java: ") + style.value(String.valueOf(javaVersion())) + style.label(" | Date: ") + style.value(startupDate()),
prefix + style.label(" Commit: ") + style.value(BuildConstants.COMMIT) + style.label("/") + style.value(BuildConstants.ENVIRONMENT),
"",
"",
"",
""
};
}
public static String compose(String[] splash, String[] info) {
StringBuilder builder = new StringBuilder("\n\n");
for (int i = 0; i < splash.length; i++) {
builder.append(SPLASH_PADDING).append(splash[i]).append(info[i]).append('\n');
}
return builder.toString();
}
public static List<String> composePackLines(File packFolder, IrisSplashPackScanner.SplashPackErrorReporter reporter) {
List<IrisSplashPackScanner.SplashPackMetadata> packs = IrisSplashPackScanner.collect(packFolder, reporter);
if (packs.isEmpty()) {
return List.of();
}
List<String> lines = new ArrayList<>(packs.size() + 1);
lines.add("Custom Dimensions: " + packs.size());
for (IrisSplashPackScanner.SplashPackMetadata pack : packs) {
lines.add(" " + pack.name() + " v" + pack.version());
}
return lines;
}
public static int javaVersion() {
String version = System.getProperty("java.version");
if (version.startsWith("1.")) {
version = version.substring(2, 3);
} else {
int dot = version.indexOf('.');
if (dot != -1) {
version = version.substring(0, dot);
}
}
return Integer.parseInt(version);
}
public static String releaseTrain(String version) {
String value = version == null ? "unknown" : version;
int suffixIndex = value.indexOf('-');
if (suffixIndex >= 0) {
value = value.substring(0, suffixIndex);
}
String[] split = value.split("\\.");
if (split.length >= 2) {
return split[0] + "." + split[1];
}
return value;
}
private static String startupDate() {
return LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
}
public interface InfoStyle {
InfoStyle PLAIN = new InfoStyle() {
};
default String linePrefix() {
return "";
}
default String title(String text) {
return text;
}
default String subtitle(String text) {
return text;
}
default String tag(String text) {
return text;
}
default String label(String text) {
return text;
}
default String value(String text) {
return text;
}
}
}
@@ -26,11 +26,14 @@ import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod;
import art.arcane.iris.core.pregenerator.methods.HybridPregenMethod;
import art.arcane.iris.core.service.GlobalCacheSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
@@ -53,7 +56,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -66,7 +68,6 @@ public class IrisToolbelt {
private static final Map<String, AtomicInteger> worldMaintenanceDepth = new ConcurrentHashMap<>();
private static final Map<String, AtomicInteger> worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>();
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
private static final AtomicBoolean PREGEN_PROFILE_JVM_HINT_LOGGED = new AtomicBoolean(false);
/**
* Will find / download / search for the dimension or return null
@@ -247,39 +248,24 @@ public class IrisToolbelt {
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA;
}
return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine);
return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld().name()), task) : method, engine);
}
private static PregenCache resolvePregenCache(String worldName) {
PregenCache cache = IrisServices.get(GlobalCacheSVC.class).get(worldName);
if (cache == null) {
IrisLogging.debug("Could not find existing cache for " + worldName + " creating fallback");
cache = GlobalCacheSVC.createDefault(worldName);
}
return cache;
}
public static boolean applyPregenPerformanceProfile() {
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
performance.setNoiseCacheSize(targetNoiseCacheSize);
changed = true;
}
if (!fastCacheEnabledBefore) {
System.setProperty("iris.cache.fast", "true");
changed = true;
}
if (PREGEN_PROFILE_JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) {
IrisLogging.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true");
}
return changed;
return PregenPerformanceProfile.apply();
}
public static void applyPregenPerformanceProfile(Engine engine) {
boolean changed = applyPregenPerformanceProfile();
if (changed && engine != null) {
engine.hotloadComplex();
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
}
PregenPerformanceProfile.apply(engine);
}
/**
@@ -432,15 +418,26 @@ public class IrisToolbelt {
return;
}
String name = world.getName();
int depth = worldMaintenanceDepth.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet();
beginWorldMaintenance(world.getName(), reason, bypassMantleStages);
}
public static void beginWorldMaintenance(String worldName, String reason) {
beginWorldMaintenance(worldName, reason, false);
}
public static void beginWorldMaintenance(String worldName, String reason, boolean bypassMantleStages) {
if (worldName == null) {
return;
}
int depth = worldMaintenanceDepth.computeIfAbsent(worldName, k -> new AtomicInteger()).incrementAndGet();
if (bypassMantleStages) {
worldMaintenanceMantleBypassDepth.computeIfAbsent(name, k -> new AtomicInteger()).incrementAndGet();
worldMaintenanceMantleBypassDepth.computeIfAbsent(worldName, k -> new AtomicInteger()).incrementAndGet();
}
if (IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("World maintenance enter: " + name + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
IrisLogging.info("World maintenance enter: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
} else {
IrisLogging.debug("World maintenance enter: " + name + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
IrisLogging.debug("World maintenance enter: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantle=" + bypassMantleStages);
}
}
@@ -449,50 +446,65 @@ public class IrisToolbelt {
return;
}
String name = world.getName();
AtomicInteger depthCounter = worldMaintenanceDepth.get(name);
endWorldMaintenance(world.getName(), reason);
}
public static void endWorldMaintenance(String worldName, String reason) {
if (worldName == null) {
return;
}
AtomicInteger depthCounter = worldMaintenanceDepth.get(worldName);
if (depthCounter == null) {
return;
}
int depth = depthCounter.decrementAndGet();
if (depth <= 0) {
worldMaintenanceDepth.remove(name, depthCounter);
worldMaintenanceDepth.remove(worldName, depthCounter);
depth = 0;
}
AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(name);
AtomicInteger bypassCounter = worldMaintenanceMantleBypassDepth.get(worldName);
int bypassDepth = 0;
if (bypassCounter != null) {
bypassDepth = bypassCounter.decrementAndGet();
if (bypassDepth <= 0) {
worldMaintenanceMantleBypassDepth.remove(name, bypassCounter);
worldMaintenanceMantleBypassDepth.remove(worldName, bypassCounter);
bypassDepth = 0;
}
}
if (IrisSettings.get().getGeneral().isDebug()) {
IrisLogging.info("World maintenance exit: " + name + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
IrisLogging.info("World maintenance exit: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
} else {
IrisLogging.debug("World maintenance exit: " + name + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
IrisLogging.debug("World maintenance exit: " + worldName + " reason=" + reason + " depth=" + depth + " bypassMantleDepth=" + bypassDepth);
}
}
public static boolean isWorldMaintenanceActive(World world) {
if (world == null) {
return world != null && isWorldMaintenanceActive(world.getName());
}
public static boolean isWorldMaintenanceActive(String worldName) {
if (worldName == null) {
return false;
}
AtomicInteger counter = worldMaintenanceDepth.get(world.getName());
AtomicInteger counter = worldMaintenanceDepth.get(worldName);
return counter != null && counter.get() > 0;
}
public static boolean isWorldMaintenanceBypassingMantleStages(World world) {
if (world == null) {
return world != null && isWorldMaintenanceBypassingMantleStages(world.getName());
}
public static boolean isWorldMaintenanceBypassingMantleStages(String worldName) {
if (worldName == null) {
return false;
}
AtomicInteger counter = worldMaintenanceMantleBypassDepth.get(world.getName());
AtomicInteger counter = worldMaintenanceMantleBypassDepth.get(worldName);
return counter != null && counter.get() > 0;
}
@@ -218,7 +218,9 @@ public class IrisEngine implements Engine {
currentMode.close();
}
J.a(() -> new IrisProject(getData().getDataFolder()).updateWorkspace());
if (getWorld().hasRealWorld()) {
J.a(() -> new IrisProject(getData().getDataFolder()).updateWorkspace());
}
}
private void setupEngine() {
@@ -406,11 +408,13 @@ public class IrisEngine implements Engine {
getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey()));
prehotload();
setupEngine();
J.a(() -> {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false);
}
});
if (getWorld().hasRealWorld()) {
J.a(() -> {
synchronized (ServerConfigurator.class) {
ServerConfigurator.installDataPacks(false);
}
});
}
}
@Override
@@ -608,7 +612,7 @@ public class IrisEngine implements Engine {
private boolean isPregeneratorActiveForThisWorld() {
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob != null && getWorld().hasRealWorld() && pregeneratorJob.targetsWorld(getWorld().realWorld());
return pregeneratorJob != null && pregeneratorJob.targetsWorldName(getWorld().name());
}
private void savePrefetchOnce() {
@@ -458,7 +458,7 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return false;
}
return job.targetsWorld(world);
return job.targetsWorldName(world.getName());
}
private Chunk[] getLoadedChunksSnapshot(World world) {
@@ -105,11 +105,13 @@ public class IrisDecorantActuator extends EngineAssignedActuator<PlatformBlockSt
output, biome, height, getEngine().getHeight());
}
getSurfaceDecorator().decorate(i, j, realX, realZ, output, biome, height, getEngine().getHeight() - height);
if (height >= 0 && height < output.getHeight()) {
getSurfaceDecorator().decorate(i, j, realX, realZ, output, biome, height, getEngine().getHeight() - height);
}
if (cave != null && cave.getDecorators().isNotEmpty()) {
for (int k = height; k > 0; k--) {
for (int k = Math.min(height, output.getHeight() - 1); k > 0; k--) {
solid = PREDICATE_SOLID.test(output.get(i, k, j));
if (solid) {
@@ -109,7 +109,7 @@ final class DecoratorCore {
String half = IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
if (height + 2 < data.getHeight() && (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z)))) {
data.set(x, height + 2, z, bd.withProperty("half", topHalfValue(half)));
}
} catch (Throwable e) {
@@ -118,7 +118,7 @@ final class DecoratorCore {
bd = bd.withProperty("half", bottomHalfValue(half));
}
if (B.isAir(data.get(x, height + 1, z))) {
if (height + 1 < data.getHeight() && B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
}
}
@@ -135,6 +135,10 @@ final class DecoratorCore {
int x, int z, int realX, int height, int realZ,
Hunk<PlatformBlockState> data, RNG rng, IrisData irisData,
boolean underwater, boolean caveSkipFluid, EngineMantle mantle) {
if (height < 0 || height >= data.getHeight()) {
return;
}
PlatformBlockState bdx = data.get(x, height, z);
PlatformBlockState bd = decorator.pickBlockData(rng, irisData, realX, realZ);
@@ -166,7 +170,7 @@ final class DecoratorCore {
String half = bd == null ? null : IrisProceduralBlocks.propertyValue(bd, "half");
if (half != null) {
try {
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
if (height + 2 < data.getHeight() && (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z)))) {
data.set(x, height + 2, z, bd.withProperty("half", topHalfValue(half)));
}
} catch (Throwable e) {
@@ -175,7 +179,7 @@ final class DecoratorCore {
bd = bd.withProperty("half", bottomHalfValue(half));
}
if (B.isAir(data.get(x, height + 1, z))) {
if (height + 1 < data.getHeight() && B.isAir(data.get(x, height + 1, z))) {
data.set(x, height + 1, z, fixFacesForHunk(bd, data, x, z, realX, height + 1, realZ, mantle));
}
}
@@ -196,6 +200,10 @@ final class DecoratorCore {
static void placeStackUp(IrisDecorator decorator, int x, int z, int realX, int realZ,
int height, int max, Hunk<PlatformBlockState> data,
RNG rng, IrisData irisData, PlaceOpts opts) {
if (height < 0 || height >= data.getHeight()) {
return;
}
int effectiveMax = max;
if (opts.underwater && height < opts.fluidHeight) {
effectiveMax = opts.fluidHeight;
@@ -232,6 +240,10 @@ final class DecoratorCore {
break;
}
if (height + 1 + i >= data.getHeight()) {
break;
}
if (opts.caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) {
break;
}
@@ -58,7 +58,6 @@ import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.stream.ProceduralStream;
import org.bukkit.World;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
@@ -660,15 +659,24 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
}
default void cleanupMantleChunk(int x, int z) {
World world = getWorld().realWorld();
if (world != null && IrisToolbelt.isWorldMaintenanceActive(world)) {
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
return;
}
if (shouldSkipMaintenanceCleanup()) {
return;
}
if (IrisSettings.get().getPerformance().isTrimMantleInStudio() || !isStudio()) {
getMantle().cleanupChunk(x, z);
}
}
private boolean shouldSkipMaintenanceCleanup() {
IrisWorld world = getWorld();
if (world == null) {
return false;
}
String worldName = world.name();
if (!IrisToolbelt.isWorldMaintenanceActive(worldName)) {
return false;
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return pregeneratorJob == null || !pregeneratorJob.targetsWorldName(worldName);
}
}
@@ -98,7 +98,7 @@ public interface EngineMode extends Staged {
}
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorld(getEngine().getWorld().realWorld());
boolean pregeneratorTargetsWorld = pregeneratorJob != null && pregeneratorJob.targetsWorldName(getEngine().getWorld().name());
return shouldDisableContextCacheForMaintenance(maintenanceActive, pregeneratorTargetsWorld);
}
}
@@ -57,9 +57,11 @@ public class WorldObjectPlacer implements IObjectPlacer {
@Override
public void set(int x, int y, int z, PlatformBlockState state) {
BlockData d = (BlockData) state.nativeHandle();
Block block = world.getBlockAt(x, y + world.getMinHeight(), z);
int worldY = y + world.getMinHeight();
if (worldY < world.getMinHeight() || worldY >= world.getMaxHeight()) return;
Block block = world.getBlockAt(x, worldY, z);
if (y <= world.getMinHeight() || block.getType() == Material.BEDROCK) return;
if (block.getType() == Material.BEDROCK) return;
InventorySlotType slot = null;
if (BukkitBlockResolution.isStorageChest(d)) {
slot = InventorySlotType.STORAGE;
@@ -128,7 +130,9 @@ public class WorldObjectPlacer implements IObjectPlacer {
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
tile.toBukkitTry(world.getBlockAt(xx, yy + world.getMinHeight(), zz));
int worldY = yy + world.getMinHeight();
if (worldY < world.getMinHeight() || worldY >= world.getMaxHeight()) return;
tile.toBukkitTry(world.getBlockAt(xx, worldY, zz));
}
@Override
@@ -19,12 +19,14 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.math.RNG;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.attribute.Attributable;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
@@ -33,7 +35,9 @@ import org.bukkit.inventory.EquipmentSlotGroup;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Snippet("attribute-modifier")
@Accessors(chain = true)
@@ -42,17 +46,19 @@ import java.util.UUID;
@Desc("Represents an attribute modifier for an item or an entity. This allows you to create modifications to basic game attributes such as MAX_HEALTH or ARMOR_VALUE.")
@Data
public class IrisAttributeModifier {
private static final Set<String> WARNED_ATTRIBUTES = ConcurrentHashMap.newKeySet();
@Required
@Desc("The Attribute type. This type is pulled from the game attributes. Zombie & Horse attributes will not work on non-zombie/horse entities.\nUsing an attribute on an item will have affects when held, or worn. There is no way to specify further granularity as the game picks this depending on the item type.")
private Attribute attribute = null;
@Desc("The Attribute type key such as max_health or minecraft:armor. This type is pulled from the game attributes. Zombie & Horse attributes will not work on non-zombie/horse entities.\nUsing an attribute on an item will have affects when held, or worn. There is no way to specify further granularity as the game picks this depending on the item type.")
private String attribute = null;
@MinNumber(2)
@Required
@Desc("The Attribute Name is used internally only for the game. This value should be unique to all other attributes applied to this item/entity. It is not shown in game.")
private String name = "";
@Desc("The application operation (add number is default). Add Number adds to the default value. \nAdd scalar_1 will multiply by 1 for example if the health is 20 and you multiply_scalar_1 by 0.5, the health will result in 30, not 10. Use negative values to achieve that.")
private Operation operation = Operation.ADD_NUMBER;
@Desc("The application operation: ADD_NUMBER (default), ADD_SCALAR or MULTIPLY_SCALAR_1. Add Number adds to the default value. \nAdd scalar_1 will multiply by 1 for example if the health is 20 and you multiply_scalar_1 by 0.5, the health will result in 30, not 10. Use negative values to achieve that.")
private String operation = "ADD_NUMBER";
@Desc("Minimum amount for this modifier. Iris randomly chooses an amount, this is the minimum it can choose randomly for this attribute.")
private double minAmount = 1;
@@ -67,13 +73,21 @@ public class IrisAttributeModifier {
public void apply(RNG rng, ItemMeta meta) {
if (rng.nextDouble() < getChance()) {
meta.addAttributeModifier(getAttribute(), createModifier(rng));
Attribute resolved = resolveAttribute();
if (resolved == null) {
return;
}
meta.addAttributeModifier(resolved, createModifier(rng));
}
}
public void apply(RNG rng, Attributable meta) {
if (rng.nextDouble() < getChance()) {
meta.getAttribute(getAttribute()).addModifier(createModifier(rng));
Attribute resolved = resolveAttribute();
if (resolved == null) {
return;
}
meta.getAttribute(resolved).addModifier(createModifier(rng));
}
}
@@ -81,9 +95,44 @@ public class IrisAttributeModifier {
return rng.d(getMinAmount(), getMaxAmount());
}
private Attribute resolveAttribute() {
if (attribute == null || attribute.isBlank()) {
return null;
}
String value = attribute.trim().toLowerCase(Locale.ROOT);
Attribute resolved;
if (value.contains(":")) {
NamespacedKey key = NamespacedKey.fromString(value);
resolved = key == null ? null : Registry.ATTRIBUTE.get(key);
} else {
resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value));
if (resolved == null && value.startsWith("generic_")) {
resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value.substring("generic_".length())));
}
if (resolved == null && value.startsWith("generic.")) {
resolved = Registry.ATTRIBUTE.get(NamespacedKey.minecraft(value.substring("generic.".length())));
}
}
if (resolved == null && WARNED_ATTRIBUTES.add(value)) {
IrisLogging.warn("Unknown attribute '" + attribute + "' in attribute-modifier '" + name + "'");
}
return resolved;
}
private Operation resolveOperation() {
if (operation == null || operation.isBlank()) {
return Operation.ADD_NUMBER;
}
try {
return Operation.valueOf(operation.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
return Operation.ADD_NUMBER;
}
}
private AttributeModifier createModifier(RNG rng) {
NamespacedKey key = NamespacedKey.minecraft(generateModifierKey());
return new AttributeModifier(key, getAmount(rng), getOperation(), EquipmentSlotGroup.ANY);
return new AttributeModifier(key, getAmount(rng), resolveOperation(), EquipmentSlotGroup.ANY);
}
private String generateModifierKey() {
@@ -293,6 +293,19 @@ public class IrisBiome extends IrisRegistrant implements IRare {
return resolved == null ? getDerivative() : resolved;
}
public String getVanillaDerivativeKey() {
String resolved = namespacedBiomeKey(vanillaDerivative);
return resolved == null ? namespacedBiomeKey(derivative) : resolved;
}
private static String namespacedBiomeKey(String key) {
if (key == null || key.isBlank()) {
return null;
}
String trimmed = key.trim();
return trimmed.indexOf(':') >= 0 ? trimmed : "minecraft:" + trimmed;
}
private KList<Biome> getBiomeScatterResolved() {
return biomeScatterResolved.aquire(() -> resolveBiomeKeys(biomeScatter));
}
@@ -38,8 +38,8 @@ import org.bukkit.TreeType;
public class IrisTree {
@Required
@Desc("The types of trees overwritten by this object")
@ArrayType(min = 1, type = TreeType.class)
private KList<TreeType> treeTypes;
@ArrayType(min = 1, type = String.class)
private KList<String> treeTypes = new KList<>();
@Desc("If enabled, overrides any TreeType")
private boolean anyTree = false;
@@ -71,6 +71,17 @@ public class IrisTree {
}
private boolean matchesType(TreeType type) {
return getTreeTypes().contains(type);
if (type == null) {
return false;
}
String name = type.name();
for (String i : getTreeTypes()) {
if (i != null && i.equalsIgnoreCase(name)) {
return true;
}
}
return false;
}
}
@@ -503,7 +503,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
World realWorld = this.world.realWorld();
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorld(realWorld);
return realWorld != null && pregeneratorJob != null && pregeneratorJob.targetsWorldName(realWorld.getName());
}
@Override
@@ -1,55 +0,0 @@
/*
* 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.platform.bukkit;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.spi.ChunkWriteTarget;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
/**
* Bukkit adapter for the hot-path chunk write surface backed by TerrainChunk.
*/
public final class BukkitChunkWriteTarget implements ChunkWriteTarget {
private final TerrainChunk terrain;
public BukkitChunkWriteTarget(TerrainChunk terrain) {
this.terrain = terrain;
}
@Override
public void setBlock(int x, int y, int z, PlatformBlockState state) {
terrain.setBlock(x, y, z, state);
}
@Override
public void setBiome(int x, int y, int z, PlatformBiome biome) {
terrain.setBiome(x, y, z, biome);
}
@Override
public int minHeight() {
return terrain.getMinHeight();
}
@Override
public int maxHeight() {
return terrain.getMaxHeight();
}
}
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformCapabilities;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
@@ -32,7 +31,6 @@ import art.arcane.iris.spi.PlatformWorld;
import art.arcane.iris.util.common.misc.Bindings;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.Vector3d;
import org.bukkit.Bukkit;
@@ -45,7 +43,6 @@ import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.inventory.ItemStack;
@@ -90,7 +87,6 @@ public final class BukkitPlatform implements IrisPlatform {
private final BukkitRegistries registries = new BukkitRegistries();
private final BukkitScheduler scheduler = new BukkitScheduler();
private final PlatformCapabilities capabilities = new BukkitCapabilities();
private final BukkitStructureHooks structureHooks = new BukkitStructureHooks();
private final BukkitBiomeWriter biomeWriter = new BukkitBiomeWriter();
@@ -244,11 +240,6 @@ public final class BukkitPlatform implements IrisPlatform {
return scheduler;
}
@Override
public PlatformCapabilities capabilities() {
return capabilities;
}
@Override
public PlatformStructureHooks structureHooks() {
return structureHooks;
@@ -311,18 +302,6 @@ public final class BukkitPlatform implements IrisPlatform {
return entity != null;
}
@Override
public boolean giveItem(Object player, String itemKey, int amount) {
if (!(player instanceof Player bukkitPlayer) || itemKey == null || amount <= 0) {
return false;
}
Material material = Material.matchMaterial(itemKey);
if (material == null) {
return false;
}
return bukkitPlayer.getInventory().addItem(new ItemStack(material, amount)).isEmpty();
}
@Override
public void log(LogLevel level, String message) {
bridge().logSink().accept(level, message);
@@ -337,26 +316,4 @@ public final class BukkitPlatform implements IrisPlatform {
public void reportError(Throwable error) {
bridge().errorSink().accept(error);
}
private static final class BukkitCapabilities implements PlatformCapabilities {
@Override
public boolean customBiomes() {
return INMS.get().supportsCustomBiomes();
}
@Override
public boolean dataPacks() {
return INMS.get().supportsDataPacks();
}
@Override
public boolean structurePlacement() {
return true;
}
@Override
public boolean regionizedThreading() {
return J.isFolia();
}
}
}
@@ -0,0 +1,44 @@
/*
* 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.util.common.math;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class ChunkSpiral {
private ChunkSpiral() {
}
public static List<int[]> centerOut(int centerX, int centerZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerX + dx, centerZ + dz});
}
}
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerX;
int oz = t[1] - centerZ;
return ox * ox + oz * oz;
}));
return targets;
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.util.common.scheduling;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.engine.framework.PreservationRegistry;
@@ -121,6 +122,13 @@ public class J {
}
public static void aBukkit(Runnable a) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().scheduler().async(a);
}
return;
}
if (!isPluginEnabled()) {
return;
}
@@ -328,6 +336,13 @@ public class J {
}
public static void s(Runnable r) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().scheduler().global(r);
}
return;
}
if (!isPluginEnabled()) {
return;
}
@@ -355,7 +370,7 @@ public class J {
public static CompletableFuture sfut(Runnable r) {
CompletableFuture f = new CompletableFuture();
if (!isPluginEnabled()) {
if (!canSchedule()) {
return null;
}
@@ -370,7 +385,7 @@ public class J {
public static <T> CompletableFuture<T> sfut(Supplier<T> r) {
CompletableFuture<T> f = new CompletableFuture<>();
if (!isPluginEnabled()) {
if (!canSchedule()) {
return null;
}
@@ -388,7 +403,7 @@ public class J {
public static CompletableFuture sfut(Runnable r, int delay) {
CompletableFuture f = new CompletableFuture();
if (!isPluginEnabled()) {
if (!canSchedule()) {
return null;
}
@@ -410,6 +425,13 @@ public class J {
}
public static void s(Runnable r, int delay) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
IrisPlatforms.get().scheduler().laterGlobal(r, delay);
}
return;
}
if (!isPluginEnabled()) {
return;
}
@@ -451,7 +473,7 @@ public class J {
}
public static int sr(Runnable r, int interval) {
if (!isPluginEnabled()) {
if (!canSchedule()) {
return -1;
}
@@ -461,13 +483,13 @@ public class J {
Runnable[] loop = new Runnable[1];
loop[0] = () -> {
if (state.cancelled || !isPluginEnabled()) {
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
return;
}
r.run();
if (state.cancelled || !isPluginEnabled()) {
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
return;
}
@@ -496,6 +518,17 @@ public class J {
}
public static void a(Runnable r, int delay) {
if (!BUKKIT_PRESENT) {
if (IrisPlatforms.isBound()) {
if (delay <= 0) {
IrisPlatforms.get().scheduler().async(r);
} else {
IrisPlatforms.get().scheduler().laterGlobal(() -> IrisPlatforms.get().scheduler().async(r), delay);
}
}
return;
}
if (!isPluginEnabled()) {
return;
}
@@ -521,7 +554,7 @@ public class J {
}
public static int ar(Runnable r, int interval) {
if (!isPluginEnabled()) {
if (!canSchedule()) {
return -1;
}
@@ -531,13 +564,13 @@ public class J {
Runnable[] loop = new Runnable[1];
loop[0] = () -> {
if (state.cancelled || !isPluginEnabled()) {
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
return;
}
r.run();
if (state.cancelled || !isPluginEnabled()) {
if (state.cancelled || !canSchedule()) {
REPEATING_CANCELLERS.remove(taskId);
return;
}
@@ -582,6 +615,10 @@ public class J {
return BUKKIT_PRESENT && BukkitPlatform.hasPlugin() && Bukkit.getPluginManager().isPluginEnabled(BukkitPlatform.plugin());
}
private static boolean canSchedule() {
return BUKKIT_PRESENT ? isPluginEnabled() : IrisPlatforms.isBound();
}
private static long ticksToMilliseconds(int ticks) {
return Math.max(0L, ticks) * TICK_MS;
}
@@ -55,6 +55,10 @@ public class ChunkDataHunkHolder extends AtomicHunk<PlatformBlockState> {
@Override
public PlatformBlockState getRaw(int x, int y, int z) {
if (y < 0 || y >= getHeight()) {
return States.AIR;
}
PlatformBlockState b = super.getRaw(x, y, z);
return b != null ? b : States.AIR;
@@ -255,6 +255,14 @@ public class FastNoiseDouble {
m_seed = seed;
}
public double getFrequency() {
return m_frequency;
}
public double getFractalBounding() {
return m_fractalBounding;
}
// Sets frequency for all noise types
// Default: 0.01
public void setFrequency(double frequency) {
@@ -0,0 +1,8 @@
package art.arcane.iris.util.simd;
public interface NoiseKernels2D {
String describe();
void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length);
}
@@ -0,0 +1,99 @@
package art.arcane.iris.util.simd;
public final class ScalarNoiseKernels2D implements NoiseKernels2D {
static final double F2 = 0.5D * (Math.sqrt(3.0D) - 1.0D);
static final double G2 = (3.0D - Math.sqrt(3.0D)) / 6.0D;
static final long X_PRIME = 1619L;
static final long Y_PRIME = 31337L;
static final double[] GRAD_2D = {-1D, -1D, 1D, -1D, -1D, 1D, 1D, 1D, 0D, -1D, -1D, 0D, 0D, 1D, 1D, 0D};
@Override
public String describe() {
return "scalar";
}
static long fastFloor(double f) {
return f >= 0D ? (long) f : (long) f - 1L;
}
static double gradCoord2D(long seed, long x, long y, double xd, double yd) {
long hash = seed;
hash ^= X_PRIME * x;
hash ^= Y_PRIME * y;
hash = hash * hash * hash * 60493L;
hash = (hash >> 13) ^ hash;
int gradientIndex = ((int) hash & 7) << 1;
return (xd * GRAD_2D[gradientIndex]) + (yd * GRAD_2D[gradientIndex + 1]);
}
static double singleSimplex(long seed, double x, double y) {
double t = (x + y) * F2;
long i = fastFloor(x + t);
long j = fastFloor(y + t);
t = (i + j) * G2;
double x0 = x - (i - t);
double y0 = y - (j - t);
long i1;
long j1;
if (x0 > y0) {
i1 = 1L;
j1 = 0L;
} else {
i1 = 0L;
j1 = 1L;
}
double x1 = x0 - i1 + G2;
double y1 = y0 - j1 + G2;
double x2 = x0 - 1D + (2D * G2);
double y2 = y0 - 1D + (2D * G2);
double n0;
double n1;
double n2;
double a = 0.5D - x0 * x0 - y0 * y0;
if (a < 0D) {
n0 = 0D;
} else {
a *= a;
n0 = a * a * gradCoord2D(seed, i, j, x0, y0);
}
double b = 0.5D - x1 * x1 - y1 * y1;
if (b < 0D) {
n1 = 0D;
} else {
b *= b;
n1 = b * b * gradCoord2D(seed, i + i1, j + j1, x1, y1);
}
double c = 0.5D - x2 * x2 - y2 * y2;
if (c < 0D) {
n2 = 0D;
} else {
c *= c;
n2 = c * c * gradCoord2D(seed, i + 1L, j + 1L, x2, y2);
}
return 50D * (n0 + n1 + n2);
}
static double simplexFractalFBMScalar(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double xIn, double yIn) {
double x = xIn * frequency;
double y = yIn * frequency;
long s = seed;
double sum = singleSimplex(s, x, y);
double amp = 1D;
for (int o = 1; o < octaves; o++) {
x *= lacunarity;
y *= lacunarity;
amp *= gain;
sum += singleSimplex(++s, x, y) * amp;
}
return sum * fractalBounding;
}
@Override
public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length) {
for (int k = 0; k < length; k++) {
out[k] = simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs[k], zs[k]);
}
}
}
@@ -54,6 +54,36 @@ public final class SimdSupport {
return vector == null ? new ScalarSimdKernels() : vector;
}
private static final NoiseKernels2D NOISE_KERNELS_2D = selectNoiseKernels2D();
public static NoiseKernels2D noiseKernels2D() {
return NOISE_KERNELS_2D;
}
public static NoiseKernels2D createVectorNoiseKernels2D() {
if (!MODULE_PRESENT) {
return null;
}
try {
Class<?> cls = Class.forName("art.arcane.iris.util.simd.VectorNoiseKernels2D");
boolean profitable = (boolean) cls.getMethod("profitable").invoke(null);
if (!profitable) {
return null;
}
return (NoiseKernels2D) cls.getDeclaredConstructor().newInstance();
} catch (Throwable e) {
return null;
}
}
private static NoiseKernels2D selectNoiseKernels2D() {
if (!simdEnabledInSettings()) {
return new ScalarNoiseKernels2D();
}
NoiseKernels2D vector = createVectorNoiseKernels2D();
return vector == null ? new ScalarNoiseKernels2D() : vector;
}
private static boolean simdEnabledInSettings() {
try {
return IrisSettings.get().getPerformance().isSimdKernels();
@@ -0,0 +1,129 @@
package art.arcane.iris.util.simd;
import jdk.incubator.vector.DoubleVector;
import jdk.incubator.vector.LongVector;
import jdk.incubator.vector.VectorMask;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
public final class VectorNoiseKernels2D implements NoiseKernels2D {
private static final VectorSpecies<Double> DS = DoubleVector.SPECIES_PREFERRED;
private static final VectorSpecies<Long> LS = LongVector.SPECIES_PREFERRED;
private static final boolean ALIGNED = DS.length() == LS.length();
private static final int MIN_PROFITABLE_LANES = 4;
private static final boolean PROFITABLE = ALIGNED && DS.length() >= MIN_PROFITABLE_LANES;
private static final double[] GRAD_X = {-1D, 1D, -1D, 1D, 0D, -1D, 0D, 1D};
private static final double[] GRAD_Y = {-1D, -1D, 1D, 1D, -1D, 0D, 1D, 0D};
private static final double F2 = ScalarNoiseKernels2D.F2;
private static final double G2 = ScalarNoiseKernels2D.G2;
private static final long X_PRIME = ScalarNoiseKernels2D.X_PRIME;
private static final long Y_PRIME = ScalarNoiseKernels2D.Y_PRIME;
private static final ThreadLocal<long[]> LONG_SCRATCH = ThreadLocal.withInitial(() -> new long[LS.length()]);
public static boolean lanesAligned() {
return ALIGNED;
}
public static boolean profitable() {
return PROFITABLE;
}
@Override
public String describe() {
return DS.length() + "x64 lanes, " + DS.vectorShape();
}
private static LongVector floorToLong(DoubleVector f) {
LongVector truncated = (LongVector) f.convertShape(VectorOperators.D2L, LS, 0);
VectorMask<Long> negative = f.compare(VectorOperators.LT, 0D).cast(LS);
return truncated.sub(1L, negative);
}
private static DoubleVector toDouble(LongVector v) {
return (DoubleVector) v.convertShape(VectorOperators.L2D, DS, 0);
}
private static DoubleVector gradCoord(long seed, LongVector i, LongVector j, DoubleVector xd, DoubleVector yd,
int[] idxScratch) {
LongVector hash = LongVector.broadcast(LS, seed)
.lanewise(VectorOperators.XOR, i.mul(X_PRIME))
.lanewise(VectorOperators.XOR, j.mul(Y_PRIME));
hash = hash.mul(hash).mul(hash).mul(60493L);
LongVector shifted = hash.lanewise(VectorOperators.ASHR, 13);
hash = shifted.lanewise(VectorOperators.XOR, hash);
LongVector idx = hash.lanewise(VectorOperators.AND, 7L);
long[] tmp = LONG_SCRATCH.get();
idx.intoArray(tmp, 0);
for (int l = 0; l < idxScratch.length; l++) {
idxScratch[l] = (int) tmp[l];
}
DoubleVector gx = DoubleVector.fromArray(DS, GRAD_X, 0, idxScratch, 0);
DoubleVector gy = DoubleVector.fromArray(DS, GRAD_Y, 0, idxScratch, 0);
return xd.mul(gx).add(yd.mul(gy));
}
private static DoubleVector corner(DoubleVector xk, DoubleVector yk, DoubleVector grad) {
DoubleVector t = DoubleVector.broadcast(DS, 0.5D).sub(xk.mul(xk)).sub(yk.mul(yk));
VectorMask<Double> negative = t.compare(VectorOperators.LT, 0D);
DoubleVector t2 = t.mul(t);
DoubleVector t4 = t2.mul(t2);
return t4.mul(grad).blend(0D, negative);
}
private static DoubleVector singleSimplexVector(long seed, DoubleVector x, DoubleVector y, int[] idxScratch) {
DoubleVector t = x.add(y).mul(F2);
LongVector i = floorToLong(x.add(t));
LongVector j = floorToLong(y.add(t));
DoubleVector skew = toDouble(i.add(j)).mul(G2);
DoubleVector x0 = x.sub(toDouble(i).sub(skew));
DoubleVector y0 = y.sub(toDouble(j).sub(skew));
VectorMask<Double> xGreater = x0.compare(VectorOperators.GT, y0);
VectorMask<Long> xGreaterL = xGreater.cast(LS);
LongVector i1 = LongVector.zero(LS).blend(1L, xGreaterL);
LongVector j1 = LongVector.broadcast(LS, 1L).blend(0L, xGreaterL);
DoubleVector x1 = x0.sub(toDouble(i1)).add(G2);
DoubleVector y1 = y0.sub(toDouble(j1)).add(G2);
DoubleVector x2 = x0.sub(1D).add(2D * G2);
DoubleVector y2 = y0.sub(1D).add(2D * G2);
DoubleVector n0 = corner(x0, y0, gradCoord(seed, i, j, x0, y0, idxScratch));
DoubleVector n1 = corner(x1, y1, gradCoord(seed, i.add(i1), j.add(j1), x1, y1, idxScratch));
DoubleVector n2 = corner(x2, y2, gradCoord(seed, i.add(1L), j.add(1L), x2, y2, idxScratch));
return n0.add(n1).add(n2).mul(50D);
}
@Override
public void simplexFractalFBM(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int length) {
if (!ALIGNED) {
tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, 0, length);
return;
}
int lanes = DS.length();
int bound = DS.loopBound(length);
int[] idxScratch = new int[lanes];
int k = 0;
for (; k < bound; k += lanes) {
DoubleVector x = DoubleVector.fromArray(DS, xs, k).mul(frequency);
DoubleVector y = DoubleVector.fromArray(DS, zs, k).mul(frequency);
long s = seed;
DoubleVector sum = singleSimplexVector(s, x, y, idxScratch);
double amp = 1D;
for (int o = 1; o < octaves; o++) {
x = x.mul(lacunarity);
y = y.mul(lacunarity);
amp *= gain;
sum = sum.add(singleSimplexVector(++s, x, y, idxScratch).mul(amp));
}
sum.mul(fractalBounding).intoArray(out, k);
}
tailScalar(seed, octaves, frequency, lacunarity, gain, fractalBounding, xs, zs, out, k, length);
}
private static void tailScalar(long seed, int octaves, double frequency, double lacunarity, double gain,
double fractalBounding, double[] xs, double[] zs, double[] out, int from, int length) {
for (int k = from; k < length; k++) {
out[k] = ScalarNoiseKernels2D.simplexFractalFBMScalar(seed, octaves, frequency, lacunarity, gain,
fractalBounding, xs[k], zs[k]);
}
}
}
@@ -0,0 +1,70 @@
package art.arcane.iris.core.pregenerator.cache;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PregenCacheImplPersistenceTest {
private File directory;
@Before
public void setUp() throws Exception {
directory = Files.createTempDirectory("iris-pregen-cache-persist").toFile();
}
@Test
public void partialRegionChunkPersistsAcrossReload() {
PregenCache cache = PregenCache.create(directory);
cache.cacheChunk(5, 7);
cache.write();
PregenCache reloaded = PregenCache.create(directory);
assertTrue(reloaded.isChunkCached(5, 7));
assertFalse(reloaded.isChunkCached(5, 8));
assertFalse(reloaded.isRegionCached(0, 0));
}
@Test
public void partialRegionNegativeCoordinatesPersistAcrossReload() {
PregenCache cache = PregenCache.create(directory);
cache.cacheChunk(-3, -1);
cache.write();
PregenCache reloaded = PregenCache.create(directory);
assertTrue(reloaded.isChunkCached(-3, -1));
assertFalse(reloaded.isChunkCached(-3, -2));
}
@Test
public void fullRegionPersistsAcrossReload() {
PregenCache cache = PregenCache.create(directory);
cache.cacheRegion(2, 3);
cache.write();
PregenCache reloaded = PregenCache.create(directory);
assertTrue(reloaded.isRegionCached(2, 3));
assertTrue(reloaded.isChunkCached((2 << 5) + 11, (3 << 5) + 19));
assertFalse(reloaded.isRegionCached(2, 4));
}
@Test
public void regionCompletedChunkByChunkPersistsAsRegion() {
PregenCache cache = PregenCache.create(directory);
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
cache.cacheChunk(x, z);
}
}
cache.write();
PregenCache reloaded = PregenCache.create(directory);
assertTrue(reloaded.isRegionCached(0, 0));
assertTrue(reloaded.isChunkCached(31, 31));
assertFalse(reloaded.isChunkCached(32, 0));
}
}
@@ -0,0 +1,213 @@
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class CachedPregenMethodCompletionTest {
private File directory;
private PregenCache cache;
private CapturingMethod underlying;
private PregenTask task;
private CachedPregenMethod method;
private RecordingListener listener;
@Before
public void setUp() throws Exception {
directory = Files.createTempDirectory("iris-pregen-cache-test").toFile();
cache = PregenCache.create(directory);
underlying = new CapturingMethod();
task = PregenTask.builder()
.center(new Position2(0, 0))
.radiusX(256)
.radiusZ(256)
.build();
method = new CachedPregenMethod(underlying, cache, task);
listener = new RecordingListener();
}
@Test
public void generateChunkDoesNotCacheOnSubmitOnlyOnCompletion() {
method.generateChunk(3, 4, listener);
assertEquals(1, underlying.generateChunkCalls.get());
assertFalse(cache.isChunkCached(3, 4));
underlying.capturedListener.get().onChunkGenerated(3, 4, false);
assertTrue(cache.isChunkCached(3, 4));
assertEquals(1, listener.generated.get());
}
@Test
public void generateChunkFailureIsNotCached() {
method.generateChunk(6, 9, listener);
underlying.capturedListener.get().onChunkFailed(6, 9);
assertFalse(cache.isChunkCached(6, 9));
assertEquals(0, listener.generated.get());
assertEquals(1, listener.failed.get());
}
@Test
public void generateChunkSkipsUnderlyingMethodWhenCached() {
method.generateChunk(1, 2, listener);
underlying.capturedListener.get().onChunkGenerated(1, 2, false);
assertEquals(1, underlying.generateChunkCalls.get());
method.generateChunk(1, 2, listener);
assertEquals(1, underlying.generateChunkCalls.get());
assertEquals(2, listener.generated.get());
assertEquals(1, listener.generatedCached.get());
}
@Test
public void generateRegionReplayRespectsTaskBounds() {
cache.cacheRegion(0, 0);
List<long[]> expected = new ArrayList<>();
task.iterateChunks(0, 0, (x, z) -> expected.add(new long[]{x, z}));
assertTrue(expected.size() < 1024);
method.generateRegion(0, 0, listener);
assertEquals(0, underlying.generateRegionCalls.get());
assertEquals(expected.size(), listener.generated.get());
assertEquals(expected.size(), listener.generatedCached.get());
}
private static final class CapturingMethod implements PregeneratorMethod {
private final AtomicInteger generateChunkCalls = new AtomicInteger();
private final AtomicInteger generateRegionCalls = new AtomicInteger();
private final AtomicReference<PregenListener> capturedListener = new AtomicReference<>();
@Override
public void init() {
}
@Override
public void close() {
}
@Override
public void save() {
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public String getMethod(int x, int z) {
return "capture";
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
generateRegionCalls.incrementAndGet();
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
generateChunkCalls.incrementAndGet();
capturedListener.set(listener);
}
@Override
public Mantle getMantle() {
return null;
}
}
private static final class RecordingListener implements PregenListener {
private final AtomicInteger generated = new AtomicInteger();
private final AtomicInteger generatedCached = new AtomicInteger();
private final AtomicInteger failed = new AtomicInteger();
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
}
@Override
public void onChunkGenerating(int x, int z) {
}
@Override
public void onChunkGenerated(int x, int z, boolean cached) {
generated.incrementAndGet();
if (cached) {
generatedCached.incrementAndGet();
}
}
@Override
public void onChunkFailed(int x, int z) {
failed.incrementAndGet();
}
@Override
public void onRegionGenerated(int x, int z) {
}
@Override
public void onRegionGenerating(int x, int z) {
}
@Override
public void onChunkCleaned(int x, int z) {
}
@Override
public void onRegionSkipped(int x, int z) {
}
@Override
public void onNetworkStarted(int x, int z) {
}
@Override
public void onNetworkFailed(int x, int z) {
}
@Override
public void onNetworkReclaim(int revert) {
}
@Override
public void onNetworkGeneratedChunk(int x, int z) {
}
@Override
public void onNetworkDownloaded(int x, int z) {
}
@Override
public void onClose() {
}
@Override
public void onSaving() {
}
@Override
public void onChunkExistsInRegionGen(int x, int z) {
}
}
}
@@ -0,0 +1,214 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class GoldenHashEngineTest {
private static final int MIN_Y = 0;
private static final int MAX_Y = 8;
private IrisSettings previousSettings;
private File goldenDir;
private Engine engine;
private PlatformBlockState stone;
private PlatformBlockState dirt;
private PlatformBiome plains;
@Before
public void setUp() throws Exception {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
goldenDir = Files.createTempDirectory("iris-goldenhash-test").toFile();
engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
when(dimension.getLoadKey()).thenReturn("testdim");
when(engine.getDimension()).thenReturn(dimension);
stone = mock(PlatformBlockState.class);
when(stone.key()).thenReturn("minecraft:stone");
dirt = mock(PlatformBlockState.class);
when(dirt.key()).thenReturn("minecraft:dirt");
plains = mock(PlatformBiome.class);
when(plains.key()).thenReturn("minecraft:plains");
}
@After
public void tearDown() {
IrisSettings.settings = previousSettings;
}
@Test
public void capturePersistsGoldenFileWithStableFormat() throws Exception {
RecordingFeedback feedback = new RecordingFeedback();
GoldenHashEngine hashEngine = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1), feedback, progress());
assertTrue(hashEngine.run());
File goldenFile = new File(goldenDir, "testdim-s1234-c0x0-r0.hashes");
assertTrue(goldenFile.exists());
List<String> lines = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8);
assertEquals("#iris-goldenhash v1", lines.get(0));
assertEquals("#world=testworld", lines.get(1));
assertEquals("#dim=testdim", lines.get(2));
assertEquals("#seed=1234", lines.get(3));
assertEquals("#mc=test-1.0", lines.get(4));
assertEquals("#minY=0 maxY=8", lines.get(5));
assertEquals("#center=0,0", lines.get(6));
assertEquals("#radius=0", lines.get(7));
String expectedBody = referenceLine(0, 0, 3, 2, 1);
assertEquals(expectedBody, lines.get(8));
assertEquals("#combined=" + referenceCombined(expectedBody), lines.get(9));
}
@Test
public void verifyMatchesGoldenCapture() {
RecordingFeedback captureFeedback = new RecordingFeedback();
GoldenHashEngine capture = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1), captureFeedback, progress());
assertTrue(capture.run());
RecordingFeedback verifyFeedback = new RecordingFeedback();
GoldenHashEngine verify = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1), verifyFeedback, progress());
assertTrue(verify.run());
assertTrue(verifyFeedback.ok.stream().anyMatch((String line) -> line.startsWith("GOLDEN MATCH: 1/1")));
assertTrue(verifyFeedback.fail.isEmpty());
}
@Test
public void verifyFlagsMismatchAndWritesDiagnostics() {
RecordingFeedback captureFeedback = new RecordingFeedback();
GoldenHashEngine capture = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, source(dirt, 3, 2, 1), captureFeedback, progress());
assertTrue(capture.run());
RecordingFeedback verifyFeedback = new RecordingFeedback();
GoldenHashEngine verify = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.VERIFY), goldenDir, source(dirt, 7, 4, 9), verifyFeedback, progress());
assertFalse(verify.run());
assertTrue(verifyFeedback.fail.stream().anyMatch((String line) -> line.startsWith("GOLDEN MISMATCH: 1/1")));
File goldenFile = new File(goldenDir, "testdim-s1234-c0x0-r0.hashes");
assertTrue(new File(goldenDir, goldenFile.getName() + ".new").exists());
assertTrue(new File(goldenDir, goldenFile.getName() + ".diag-c0x0.txt").exists());
}
@Test
public void verifyModeWithoutCaptureFails() {
RecordingFeedback feedback = new RecordingFeedback();
GoldenHashEngine verify = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.VERIFY), goldenDir, source(dirt, 3, 2, 1), feedback, progress());
assertFalse(verify.run());
assertTrue(feedback.fail.stream().anyMatch((String line) -> line.startsWith("No golden capture at ")));
}
@Test
public void abortsWithoutWritingWhenChunkGenerationFails() {
RecordingFeedback feedback = new RecordingFeedback();
GoldenHashEngine.ChunkSource broken = (int chunkX, int chunkZ) -> {
throw new IllegalStateException("boom");
};
GoldenHashEngine hashEngine = new GoldenHashEngine(engine, request(GoldenHashEngine.Mode.AUTO), goldenDir, broken, feedback, progress());
assertFalse(hashEngine.run());
assertFalse(new File(goldenDir, "testdim-s1234-c0x0-r0.hashes").exists());
assertTrue(feedback.fail.stream().anyMatch((String line) -> line.startsWith("GoldenHash aborted: 1 chunk(s)")));
}
private GoldenHashEngine.Request request(GoldenHashEngine.Mode mode) {
return new GoldenHashEngine.Request("testworld", 1234L, "test-1.0", MIN_Y, MAX_Y, 0, 0, 0, 1, mode, false, false, "null");
}
private GoldenHashEngine.Progress progress() {
return new GoldenHashEngine.Progress() {
};
}
private GoldenHashEngine.ChunkSource source(PlatformBlockState special, int sx, int sy, int sz) {
return (int chunkX, int chunkZ) -> new GoldenHashEngine.ChunkSnapshot() {
@Override
public int minY() {
return MIN_Y;
}
@Override
public int maxY() {
return MAX_Y;
}
@Override
public PlatformBlockState block(int x, int y, int z) {
return x == sx && y == sy && z == sz ? special : stone;
}
@Override
public PlatformBiome biome(int x, int y, int z) {
return plains;
}
};
}
private String referenceLine(int chunkX, int chunkZ, int sx, int sy, int sz) throws Exception {
MessageDigest blockDigest = MessageDigest.getInstance("SHA-256");
MessageDigest biomeDigest = MessageDigest.getInstance("SHA-256");
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = MIN_Y; y < MAX_Y; y++) {
String key = x == sx && y == sy && z == sz ? "minecraft:dirt" : "minecraft:stone";
blockDigest.update((key + "\n").getBytes(StandardCharsets.UTF_8));
}
}
}
for (int x = 0; x < 16; x += 4) {
for (int z = 0; z < 16; z += 4) {
for (int y = MIN_Y; y < MAX_Y; y += 4) {
biomeDigest.update("minecraft:plains\n".getBytes(StandardCharsets.UTF_8));
}
}
}
return chunkX + " " + chunkZ + " "
+ HexFormat.of().formatHex(blockDigest.digest()) + " "
+ HexFormat.of().formatHex(biomeDigest.digest());
}
private String referenceCombined(String bodyLine) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update((bodyLine + "\n").getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest.digest());
}
private static final class RecordingFeedback implements GoldenHashEngine.Feedback {
private final List<String> ok = new ArrayList<>();
private final List<String> warn = new ArrayList<>();
private final List<String> fail = new ArrayList<>();
@Override
public void ok(String message) {
ok.add(message);
}
@Override
public void warn(String message) {
warn.add(message);
}
@Override
public void fail(String message) {
fail.add(message);
}
}
}
@@ -0,0 +1,55 @@
package art.arcane.iris.util.common.math;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class ChunkSpiralTest {
@Test
public void centerOutMatchesLegacyOrdering() {
int centerX = 5;
int centerZ = -3;
int radius = 3;
List<int[]> expected = legacyOrderedTargets(centerX, centerZ, radius);
List<int[]> actual = ChunkSpiral.centerOut(centerX, centerZ, radius);
assertEquals(expected.size(), actual.size());
for (int i = 0; i < expected.size(); i++) {
assertArrayEquals(expected.get(i), actual.get(i));
}
}
@Test
public void centerOutStartsAtCenterAndCoversSquare() {
List<int[]> targets = ChunkSpiral.centerOut(10, 20, 2);
assertEquals(25, targets.size());
assertArrayEquals(new int[]{10, 20}, targets.get(0));
}
@Test
public void centerOutRadiusZeroIsSingleChunk() {
List<int[]> targets = ChunkSpiral.centerOut(-7, 9, 0);
assertEquals(1, targets.size());
assertArrayEquals(new int[]{-7, 9}, targets.get(0));
}
private static List<int[]> legacyOrderedTargets(int centerChunkX, int centerChunkZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerChunkX + dx, centerChunkZ + dz});
}
}
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerChunkX;
int oz = t[1] - centerChunkZ;
return ox * ox + oz * oz;
}));
return targets;
}
}
@@ -0,0 +1,120 @@
package art.arcane.iris.util.simd;
import art.arcane.iris.util.project.noise.FastNoiseDouble;
import art.arcane.iris.util.project.noise.FastNoiseDouble.FractalType;
import art.arcane.iris.util.project.noise.FastNoiseDouble.NoiseType;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class NoiseKernels2DParityTest {
private static FastNoiseDouble oracle(long seed, int octaves) {
FastNoiseDouble n = new FastNoiseDouble(seed);
n.setNoiseType(NoiseType.SimplexFractal);
n.setFractalType(FractalType.FBM);
n.setFractalOctaves(octaves);
return n;
}
private static void assertKernelMatchesOracle(NoiseKernels2D kernel) {
RNG rng = new RNG(99L);
for (int octaves = 1; octaves <= 8; octaves++) {
FastNoiseDouble n = oracle(1337L + octaves, octaves);
int length = 256;
double[] xs = new double[length];
double[] zs = new double[length];
double[] expected = new double[length];
double[] out = new double[length];
for (int k = 0; k < length; k++) {
double x = (rng.nextDouble() - 0.5D) * 1_000_000D;
double z = (rng.nextDouble() - 0.5D) * 1_000_000D;
xs[k] = x;
zs[k] = z;
expected[k] = n.GetSimplexFractal(x, z);
}
kernel.simplexFractalFBM(1337L + octaves, octaves, n.getFrequency(), 2.0D, 0.5D, n.getFractalBounding(), xs, zs, out, length);
for (int k = 0; k < length; k++) {
assertEquals("octaves=" + octaves + " idx=" + k, expected[k], out[k], 0D);
}
}
}
@Test
public void scalarKernelIsBitExactToOracle() {
assertKernelMatchesOracle(new ScalarNoiseKernels2D());
}
@Test
public void vectorKernelSingleOctaveIsBitExactToOracle() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
FastNoiseDouble n = oracle(202020L, 1);
RNG rng = new RNG(7L);
int length = 333;
double[] xs = new double[length];
double[] zs = new double[length];
double[] expected = new double[length];
double[] out = new double[length];
for (int k = 0; k < length; k++) {
double x = (rng.nextDouble() - 0.5D) * 800_000D;
double z = (rng.nextDouble() - 0.5D) * 800_000D;
xs[k] = x;
zs[k] = z;
expected[k] = n.GetSimplexFractal(x, z);
}
new VectorNoiseKernels2D().simplexFractalFBM(202020L, 1, n.getFrequency(), 2.0D, 0.5D, n.getFractalBounding(), xs, zs, out, length);
for (int k = 0; k < length; k++) {
assertEquals("idx=" + k, expected[k], out[k], 0D);
}
}
@Test
public void vectorKernelAllOctavesAreBitExactToOracle() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
assertKernelMatchesOracle(new VectorNoiseKernels2D());
}
@Test
public void simdSupportReturnsBitExactNoiseKernel() {
NoiseKernels2D kernel = SimdSupport.noiseKernels2D();
org.junit.Assert.assertNotNull(kernel);
assertKernelMatchesOracle(kernel);
}
@Test
public void simdSupportNeverSelectsVectorBelowProfitableLanes() {
if (VectorNoiseKernels2D.profitable()) {
org.junit.Assert.assertTrue(SimdSupport.createVectorNoiseKernels2D() instanceof VectorNoiseKernels2D);
} else {
org.junit.Assert.assertTrue(SimdSupport.noiseKernels2D() instanceof ScalarNoiseKernels2D);
}
}
@Test
public void vectorMatchesScalarAcrossEdgeCases() {
org.junit.Assume.assumeTrue(VectorNoiseKernels2D.lanesAligned());
NoiseKernels2D scalar = new ScalarNoiseKernels2D();
NoiseKernels2D vector = new VectorNoiseKernels2D();
RNG rng = new RNG(31337L);
int[] lengths = {1, 3, 7, 8, 9, 255, 256, 257};
double[] edgeCoords = {-4.0D, -3.0D, -1.0D, -0.0D, 0.0D, 1.0D, 4.0D, 1_000_000.0D, -1_000_000.0D};
for (int octaves = 1; octaves <= 8; octaves++) {
for (int length : lengths) {
double[] xs = new double[length];
double[] zs = new double[length];
double[] a = new double[length];
double[] b = new double[length];
for (int k = 0; k < length; k++) {
boolean edge = (k % 3) == 0;
xs[k] = edge ? edgeCoords[k % edgeCoords.length] : (rng.nextDouble() - 0.5D) * 2_000_000D;
zs[k] = edge ? edgeCoords[(k + 1) % edgeCoords.length] : (rng.nextDouble() - 0.5D) * 2_000_000D;
}
scalar.simplexFractalFBM(77L, octaves, 0.01D, 2.0D, 0.5D, 0.6667D, xs, zs, a, length);
vector.simplexFractalFBM(77L, octaves, 0.01D, 2.0D, 0.5D, 0.6667D, xs, zs, b, length);
for (int k = 0; k < length; k++) {
assertEquals("oct=" + octaves + " len=" + length + " idx=" + k, a[k], b[k], 0D);
}
}
}
}
}