Bulk changes and fixes

namely the chunk issue, and objects not wantingto place on cave tops.
This commit is contained in:
Brian Neumann-Fopiano
2026-04-17 22:28:35 -04:00
parent b82472d521
commit e6a8351e57
43 changed files with 3575 additions and 2180 deletions
@@ -32,6 +32,10 @@ import art.arcane.iris.core.link.IrisPapiExpansion;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.EnginePanic;
@@ -537,6 +541,7 @@ public class Iris extends VolmitPlugin implements Listener {
IO.delete(new File("iris"));
compat = IrisCompat.configured(getDataFile("compat.json"));
ServerConfigurator.configure();
validateAllPacks();
IrisSafeguard.execute();
getSender().setTag(getTag());
IrisSafeguard.splash();
@@ -1008,6 +1013,16 @@ public class Iris extends VolmitPlugin implements Listener {
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
PackValidationResult validation = PackValidationRegistry.get(id);
if (validation != null && !validation.isLoadable()) {
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + id + "':");
for (String reason : validation.getBlockingErrors()) {
Iris.error(" - " + reason);
}
throw new BrokenPackException(id, validation.getBlockingErrors());
}
IrisDimension dim = loadDimension(worldName, id);
if (dim == null) {
throw new RuntimeException("Can't find dimension " + id + "!");
@@ -1039,6 +1054,38 @@ public class Iris extends VolmitPlugin implements Listener {
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
}
public static void validateAllPacks() {
File packsRoot = Iris.instance.getDataFolder("packs");
File[] packDirs = packsRoot.listFiles(File::isDirectory);
if (packDirs == null || packDirs.length == 0) {
return;
}
PackValidationRegistry.clear();
for (File packDir : packDirs) {
try {
PackValidationResult result = PackValidator.validate(packDir);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
Iris.error("Pack '" + result.getPackName() + "' FAILED validation - world/studio creation will be refused. Reasons:");
for (String reason : result.getBlockingErrors()) {
Iris.error(" - " + reason);
}
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
Iris.info("Pack '" + result.getPackName() + "' validated ("
+ result.getRemovedUnusedFiles().size() + " unused file(s) quarantined to .iris-trash/, "
+ result.getWarnings().size() + " warning(s)).");
for (String warning : result.getWarnings()) {
Iris.warn(" [" + result.getPackName() + "] " + warning);
}
} else {
Iris.success("Pack '" + result.getPackName() + "' validated.");
}
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packDir.getName() + "'", e);
}
}
}
@Nullable
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
File pack = new File(Bukkit.getWorldContainer(), String.join(File.separator, worldName, "iris", "pack"));
File diff suppressed because it is too large Load Diff
@@ -185,6 +185,13 @@ public class ServerConfigurator {
DimensionHeight height = new DimensionHeight(fixer);
KList<File> baseFolders = getDatapacksFolder();
KList<File> folders = collectInstallDatapackFolders(baseFolders, extraWorldDatapackFoldersByPack);
if (fullInstall) {
if (anyDimensionHasVanillaStructures()) {
VanillaDatapackDumper.dumpIfNeeded(baseFolders);
} else {
VanillaDatapackDumper.removeIfPresent(baseFolders);
}
}
if (includeExternal) {
installExternalDataPacks(baseFolders, extraWorldDatapackFoldersByPack);
}
@@ -254,6 +261,9 @@ public class ServerConfigurator {
if (summary.getLegacyWorldCopyRemovals() > 0) {
Iris.verbose("Removed " + summary.getLegacyWorldCopyRemovals() + " legacy managed world datapack copies.");
}
if (summary.getSkippedExistingRequests() > 0) {
Iris.verbose("Reused " + summary.getSkippedExistingRequests() + " already-installed external datapack(s) (no download/projection).");
}
int loadedDatapackCount = Math.max(0, summary.getRequests() - summary.getOptionalFailures() - summary.getRequiredFailures());
Iris.info("Loaded Datapacks into Iris: " + loadedDatapackCount + "!");
if (summary.getRequiredFailures() > 0) {
@@ -261,6 +271,28 @@ public class ServerConfigurator {
}
}
private static boolean anyDimensionHasVanillaStructures() {
try (Stream<IrisData> stream = allPacks()) {
return stream.anyMatch(data -> {
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
if (loader == null) {
return false;
}
String[] keys = loader.getPossibleKeys();
if (keys == null || keys.length == 0) {
return false;
}
for (String key : keys) {
IrisDimension dim = loader.load(key);
if (dim != null && dim.isVanillaStructures()) {
return true;
}
}
return false;
});
}
}
private static boolean shouldDeferInstallUntilWorldsReady() {
String forcedMainWorld = IrisSettings.get().getGeneral().forceMainWorld;
if (forcedMainWorld != null && !forcedMainWorld.isBlank()) {
@@ -370,17 +402,9 @@ public class ServerConfigurator {
targetPack,
environment,
definition.isRequired(),
definition.isReplaceVanilla(),
definition.isSupportSmartBore(),
definition.getReplaceTargets(),
definition.getStructureAliases(),
definition.getStructureSetAliases(),
definition.getTemplateAliases(),
definition.getStructurePatches(),
definition.isReplace(),
Set.of(),
scopeKey,
!definition.isReplaceVanilla(),
Set.of()
scopeKey
);
dedupeMerges += mergeDeduplicatedRequest(deduplicated, request);
unscopedRequests++;
@@ -389,8 +413,7 @@ public class ServerConfigurator {
+ ", dimension=" + dimension.getLoadKey()
+ ", scope=dimension-root"
+ ", forcedBiomes=0"
+ ", replaceVanilla=" + definition.isReplaceVanilla()
+ ", alongsideMode=" + (!definition.isReplaceVanilla())
+ ", replace=" + definition.isReplace()
+ ", required=" + definition.isRequired());
continue;
}
@@ -403,16 +426,8 @@ public class ServerConfigurator {
environment,
group.required(),
group.replaceVanilla(),
definition.isSupportSmartBore(),
definition.getReplaceTargets(),
definition.getStructureAliases(),
definition.getStructureSetAliases(),
definition.getTemplateAliases(),
definition.getStructurePatches(),
group.forcedBiomeKeys(),
group.scopeKey(),
!group.replaceVanilla(),
Set.of()
group.scopeKey()
);
dedupeMerges += mergeDeduplicatedRequest(deduplicated, request);
scopedRequests++;
@@ -421,8 +436,7 @@ public class ServerConfigurator {
+ ", dimension=" + dimension.getLoadKey()
+ ", scope=" + group.source()
+ ", forcedBiomes=" + group.forcedBiomeKeys().size()
+ ", replaceVanilla=" + group.replaceVanilla()
+ ", alongsideMode=" + (!group.replaceVanilla())
+ ", replace=" + group.replaceVanilla()
+ ", required=" + group.required());
}
}
@@ -507,9 +521,9 @@ public class ServerConfigurator {
continue;
}
boolean replaceVanilla = binding.getReplaceVanillaOverride() == null
? definition.isReplaceVanilla()
: binding.getReplaceVanillaOverride();
boolean replaceVanilla = binding.getReplaceOverride() == null
? definition.isReplace()
: binding.getReplaceOverride();
boolean required = binding.getRequiredOverride() == null
? definition.isRequired()
: binding.getRequiredOverride();
@@ -551,9 +565,9 @@ public class ServerConfigurator {
continue;
}
boolean replaceVanilla = binding.getReplaceVanillaOverride() == null
? definition.isReplaceVanilla()
: binding.getReplaceVanillaOverride();
boolean replaceVanilla = binding.getReplaceOverride() == null
? definition.isReplace()
: binding.getReplaceOverride();
boolean required = binding.getRequiredOverride() == null
? definition.isRequired()
: binding.getRequiredOverride();
@@ -0,0 +1,153 @@
package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public final class VanillaDatapackDumper {
private static final String DUMP_ZIP_NAME = "00-iris-vanilla-worldgen.zip";
private static final String MARKER_FILE = "vanilla-datapack-version.txt";
private static final String PACK_DESCRIPTION = "Iris extracted vanilla worldgen datapack.";
private VanillaDatapackDumper() {
}
public static void dumpIfNeeded(KList<File> datapackFolders) {
if (datapackFolders == null || datapackFolders.isEmpty()) {
return;
}
String currentVersion = resolveVersionKey();
if (currentVersion == null) {
Iris.warn("Unable to determine server version for vanilla datapack dump.");
return;
}
boolean needsDump = false;
for (File folder : datapackFolders) {
File zip = new File(folder, DUMP_ZIP_NAME);
File marker = new File(folder, MARKER_FILE);
if (!zip.exists() || !marker.exists() || !currentVersion.equals(readMarker(marker))) {
needsDump = true;
break;
}
}
if (!needsDump) {
Iris.verbose("Vanilla datapack is up to date, skipping dump.");
return;
}
Iris.info("Dumping vanilla worldgen datapack...");
Map<String, byte[]> entries = INMS.get().extractVanillaDatapack();
if (entries.isEmpty()) {
Iris.warn("Vanilla datapack extraction returned no entries. Skipping dump.");
return;
}
byte[] zipBytes = buildZip(entries);
if (zipBytes == null) {
Iris.error("Failed to build vanilla datapack ZIP.");
return;
}
int written = 0;
for (File folder : datapackFolders) {
folder.mkdirs();
File zip = new File(folder, DUMP_ZIP_NAME);
File marker = new File(folder, MARKER_FILE);
try {
Files.write(zip.toPath(), zipBytes);
Files.writeString(marker.toPath(), currentVersion, StandardCharsets.UTF_8);
written++;
} catch (IOException e) {
Iris.error("Failed to write vanilla datapack to " + folder.getAbsolutePath());
e.printStackTrace();
}
}
Iris.info("Vanilla datapack written to " + written + " world(s) with " + entries.size() + " entries.");
}
public static void removeIfPresent(KList<File> datapackFolders) {
if (datapackFolders == null || datapackFolders.isEmpty()) {
return;
}
int removed = 0;
for (File folder : datapackFolders) {
File zip = new File(folder, DUMP_ZIP_NAME);
File marker = new File(folder, MARKER_FILE);
if (zip.exists() && zip.delete()) {
removed++;
}
if (marker.exists()) {
marker.delete();
}
}
if (removed > 0) {
Iris.info("Removed vanilla datapack from " + removed + " world(s) (vanillaStructures disabled).");
}
}
private static byte[] buildZip(Map<String, byte[]> entries) {
try {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
zos.putNextEntry(new ZipEntry("pack.mcmeta"));
zos.write(buildPackMeta());
zos.closeEntry();
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
zos.putNextEntry(new ZipEntry(entry.getKey()));
zos.write(entry.getValue());
zos.closeEntry();
}
}
return baos.toByteArray();
} catch (IOException e) {
Iris.error("Failed to build vanilla datapack ZIP");
e.printStackTrace();
return null;
}
}
private static byte[] buildPackMeta() {
int packFormat = INMS.get().getDataVersion().getPackFormat();
JSONObject root = new JSONObject();
JSONObject pack = new JSONObject();
pack.put("description", PACK_DESCRIPTION);
pack.put("pack_format", packFormat);
root.put("pack", pack);
return root.toString(4).getBytes(StandardCharsets.UTF_8);
}
private static String resolveVersionKey() {
try {
DataVersion dv = INMS.get().getDataVersion();
return dv.getVersion() + ":" + dv.getPackFormat();
} catch (Exception e) {
return null;
}
}
private static String readMarker(File marker) {
try {
return Files.readString(marker.toPath(), StandardCharsets.UTF_8).trim();
} catch (IOException e) {
return null;
}
}
}
@@ -30,7 +30,6 @@ import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisExternalDatapack;
import art.arcane.iris.engine.object.IrisExternalDatapackReplaceTargets;
import art.arcane.iris.engine.platform.ChunkReplacementListener;
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
@@ -99,6 +98,7 @@ public class CommandIris implements DirectorExecutor {
private CommandEdit edit;
private CommandFind find;
private CommandDeveloper developer;
private CommandPack pack;
public static boolean worldCreation = false;
private static final AtomicReference<Thread> mainWorld = new AtomicReference<>();
String WorldEngine;
@@ -591,42 +591,7 @@ public class CommandIris implements DirectorExecutor {
}
private static Map<String, Set<String>> buildExternalLocateFallbackById(KList<IrisExternalDatapack> externalDatapacks) {
Map<String, Set<String>> mapped = new ConcurrentHashMap<>();
if (externalDatapacks == null || externalDatapacks.isEmpty()) {
return mapped;
}
for (IrisExternalDatapack externalDatapack : externalDatapacks) {
if (externalDatapack == null) {
continue;
}
String url = externalDatapack.getUrl() == null ? "" : externalDatapack.getUrl().trim();
String entryId = externalDatapack.getId() == null ? "" : externalDatapack.getId().trim();
String normalizedEntryId = normalizeLocateExternalToken(entryId.isBlank() ? url : entryId);
if (normalizedEntryId.isBlank()) {
continue;
}
IrisExternalDatapackReplaceTargets replaceTargets = externalDatapack.getReplaceTargets();
if (replaceTargets == null || replaceTargets.getStructures() == null || replaceTargets.getStructures().isEmpty()) {
continue;
}
LinkedHashSet<String> structures = new LinkedHashSet<>();
for (String structure : replaceTargets.getStructures()) {
String normalizedStructure = normalizeLocateStructureToken(structure);
if (!normalizedStructure.isBlank()) {
structures.add(normalizedStructure);
}
}
if (!structures.isEmpty()) {
mapped.put(normalizedEntryId, Set.copyOf(structures));
}
}
return mapped;
return new ConcurrentHashMap<>();
}
private static String normalizeLocateStructureToken(String structure) {
@@ -803,7 +768,7 @@ public class CommandIris implements DirectorExecutor {
public void download(
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "main")
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
String branch,
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
//boolean trim,
@@ -0,0 +1,164 @@
/*
* 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.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import java.io.File;
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
public class CommandPack implements DirectorExecutor {
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v", "check"})
public void validate(
@Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "")
String pack
) {
VolmitSender s = sender();
File packsRoot = Iris.instance.getDataFolder("packs");
if (!packsRoot.isDirectory()) {
s.sendMessage(C.RED + "packs/ folder not found.");
return;
}
if (pack == null || pack.isBlank()) {
File[] dirs = packsRoot.listFiles(File::isDirectory);
if (dirs == null || dirs.length == 0) {
s.sendMessage(C.YELLOW + "No packs to validate.");
return;
}
int broken = 0;
for (File dir : dirs) {
PackValidationResult result = runValidate(s, dir);
if (result != null && !result.isLoadable()) {
broken++;
}
}
s.sendMessage(C.GREEN + "Validation complete. Broken packs: " + broken + "/" + dirs.length);
return;
}
File target = new File(packsRoot, pack);
if (!target.isDirectory()) {
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
return;
}
runValidate(s, target);
}
@Director(description = "Restore most recent trashed files for a pack", aliases = {"r", "undelete"})
public void restore(
@Param(description = "The pack folder name to restore")
String pack
) {
VolmitSender s = sender();
if (pack == null || pack.isBlank()) {
s.sendMessage(C.RED + "You must specify a pack name.");
return;
}
File packFolder = new File(Iris.instance.getDataFolder("packs"), pack);
if (!packFolder.isDirectory()) {
s.sendMessage(C.RED + "Pack '" + pack + "' not found under packs/.");
return;
}
int restored = PackValidator.restoreTrash(packFolder);
if (restored == 0) {
s.sendMessage(C.YELLOW + "Nothing to restore for pack '" + pack + "'.");
return;
}
s.sendMessage(C.GREEN + "Restored " + restored + " file(s) from the most recent trash dump for pack '" + pack + "'.");
s.sendMessage(C.GRAY + "Re-run /iris pack validate " + pack + " to re-check.");
}
@Director(description = "Show cached validation status for a pack", aliases = {"s", "info"})
public void status(
@Param(description = "The pack folder name", defaultValue = "")
String pack
) {
VolmitSender s = sender();
if (pack == null || pack.isBlank()) {
if (PackValidationRegistry.snapshot().isEmpty()) {
s.sendMessage(C.YELLOW + "No validation results recorded. Run /iris pack validate first.");
return;
}
PackValidationRegistry.snapshot().forEach((name, result) -> {
String tag = result.isLoadable() ? (C.GREEN + "OK") : (C.RED + "BROKEN");
s.sendMessage(tag + C.RESET + " " + name
+ C.GRAY + " (blocking=" + result.getBlockingErrors().size()
+ ", warnings=" + result.getWarnings().size()
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
});
return;
}
PackValidationResult result = PackValidationRegistry.get(pack);
if (result == null) {
s.sendMessage(C.YELLOW + "No validation result for '" + pack + "'. Run /iris pack validate " + pack + ".");
return;
}
reportResult(s, result);
}
private PackValidationResult runValidate(VolmitSender s, File packFolder) {
try {
PackValidationResult result = PackValidator.validate(packFolder);
PackValidationRegistry.publish(result);
reportResult(s, result);
return result;
} catch (Throwable e) {
Iris.reportError("Pack validation failed for '" + packFolder.getName() + "'", e);
s.sendMessage(C.RED + "Validation of '" + packFolder.getName() + "' failed: " + e.getMessage());
return null;
}
}
private void reportResult(VolmitSender s, PackValidationResult result) {
if (result.isLoadable()) {
s.sendMessage(C.GREEN + "Pack '" + result.getPackName() + "' is loadable."
+ C.GRAY + " (warnings=" + result.getWarnings().size()
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
} else {
s.sendMessage(C.RED + "Pack '" + result.getPackName() + "' is BROKEN:");
for (String reason : result.getBlockingErrors()) {
s.sendMessage(C.RED + " - " + reason);
}
}
int wMax = Math.min(10, result.getWarnings().size());
for (int i = 0; i < wMax; i++) {
s.sendMessage(C.YELLOW + " ! " + result.getWarnings().get(i));
}
if (result.getWarnings().size() > wMax) {
s.sendMessage(C.GRAY + " ... and " + (result.getWarnings().size() - wMax) + " more warning(s).");
}
int tMax = Math.min(10, result.getRemovedUnusedFiles().size());
for (int i = 0; i < tMax; i++) {
s.sendMessage(C.GRAY + " ~ trashed " + result.getRemovedUnusedFiles().get(i));
}
if (result.getRemovedUnusedFiles().size() > tMax) {
s.sendMessage(C.GRAY + " ... and " + (result.getRemovedUnusedFiles().size() - tMax) + " more trashed file(s).");
}
}
}
@@ -103,7 +103,7 @@ public class CommandStudio implements DirectorExecutor {
public void download(
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
String pack,
@Param(name = "branch", description = "The branch to download from", defaultValue = "master")
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
String branch,
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
//boolean trim,
@@ -21,8 +21,12 @@ package art.arcane.iris.core.gui;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.NoiseStyle;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.RNG;
@@ -32,305 +36,519 @@ import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.locks.ReentrantLock;
import java.awt.image.DataBufferInt;
import java.util.*;
import java.util.List;
import java.util.function.Supplier;
public class NoiseExplorerGUI extends JPanel implements MouseWheelListener, Listener {
private static final long serialVersionUID = 2094606939770332040L;
private static final Color BG = new Color(24, 24, 28);
private static final Color SIDEBAR_BG = new Color(20, 20, 24);
private static final Color SIDEBAR_SELECTED = new Color(40, 50, 70);
private static final Color SIDEBAR_ITEM_COLOR = new Color(170, 170, 185);
private static final Color SEARCH_BG = new Color(30, 30, 38);
private static final Color SEARCH_FG = new Color(180, 180, 190);
private static final Color STATUS_BG = new Color(32, 32, 38, 230);
private static final Color STATUS_TEXT = new Color(180, 180, 190);
private static final Color ACCENT = new Color(90, 140, 255);
private static final Color SEPARATOR = new Color(40, 40, 50);
private static final Font STATUS_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 12);
private static final Font SIDEBAR_HEADER_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 11);
private static final Font SIDEBAR_ITEM_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
private static final Font SEARCH_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 13);
private static final int SIDEBAR_WIDTH = 240;
private static final int[] HSB_LUT = new int[256];
static JComboBox<String> combo;
@SuppressWarnings("CanBeFinal")
static boolean hd = false;
static double ascale = 10;
static double oxp = 0;
static double ozp = 0;
static double mxx = 0;
static double mzz = 0;
@SuppressWarnings("CanBeFinal")
static boolean down = false;
@SuppressWarnings("CanBeFinal")
RollingSequence r = new RollingSequence(20);
@SuppressWarnings("CanBeFinal")
boolean colorMode = IrisSettings.get().getGui().colorMode;
double scale = 1;
CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong()));
@SuppressWarnings("CanBeFinal")
MultiBurst gx = MultiBurst.burst;
ReentrantLock l = new ReentrantLock();
BufferedImage img;
int w = 0;
int h = 0;
Function2<Double, Double, Double> generator;
Supplier<Function2<Double, Double, Double>> loader;
double ox = 0; //Offset X
double oz = 0; //Offset Y
double mx = 0;
double mz = 0;
double lx = Double.MAX_VALUE; //MouseX
double lz = Double.MAX_VALUE; //MouseY
double t;
double tz;
private static final String[] CATEGORY_ORDER = {
"Pack Generators", "Simplex", "Perlin", "Cellular", "Iris", "Clover",
"Hexagon", "Vascular", "Globe", "Cubic", "Fractal", "Static",
"Nowhere", "Sierpinski", "Utility", "Other"
};
static {
for (int i = 0; i < 256; i++) {
float n = i / 255f;
HSB_LUT[i] = Color.HSBtoRGB(0.666f - n * 0.666f, 1f, 1f - n * 0.8f);
}
}
private final RollingSequence fpsHistory = new RollingSequence(60);
private final boolean colorMode = IrisSettings.get().getGui().colorMode;
private final MultiBurst gx = MultiBurst.burst;
private double scale = 1;
private double animScale = 10;
private double ox = 0;
private double oz = 0;
private double animOx = 0;
private double animOz = 0;
private double lastMouseX = Double.MAX_VALUE;
private double lastMouseZ = Double.MAX_VALUE;
private double time = 0;
private double animTime = 0;
private int imgWidth = 0;
private int imgHeight = 0;
private BufferedImage img;
private CNG cng = NoiseStyle.STATIC.create(new RNG(RNG.r.nextLong()));
private Function2<Double, Double, Double> generator;
private Supplier<Function2<Double, Double, Double>> loader;
private String currentName = "STATIC";
public NoiseExplorerGUI() {
Iris.instance.registerListener(this);
setBackground(BG);
addMouseWheelListener(this);
addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
Point cp = e.getPoint();
lx = (cp.getX());
lz = (cp.getY());
mx = lx;
mz = lz;
lastMouseX = cp.getX();
lastMouseZ = cp.getY();
}
@Override
public void mouseDragged(MouseEvent e) {
Point cp = e.getPoint();
ox += (lx - cp.getX()) * scale;
oz += (lz - cp.getY()) * scale;
lx = cp.getX();
lz = cp.getY();
ox += (lastMouseX - cp.getX()) * scale;
oz += (lastMouseZ - cp.getY()) * scale;
lastMouseX = cp.getX();
lastMouseZ = cp.getY();
}
});
}
private static void createAndShowGUI(Supplier<Function2<Double, Double, Double>> loader, String genName) {
JFrame frame = new JFrame("Noise Explorer: " + genName);
NoiseExplorerGUI nv = new NoiseExplorerGUI();
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
JLayeredPane pane = new JLayeredPane();
nv.setSize(new Dimension(1440, 820));
pane.add(nv, 1, 0);
nv.loader = loader;
nv.generator = loader.get();
frame.add(pane);
File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png");
if (file != null) {
try {
frame.setIconImage(ImageIO.read(file));
} catch (IOException e) {
Iris.reportError(e);
}
}
frame.setSize(1440, 820);
frame.setVisible(true);
}
private static void createAndShowGUI() {
JFrame frame = new JFrame("Noise Explorer");
NoiseExplorerGUI nv = new NoiseExplorerGUI();
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
KList<String> li = new KList<>(NoiseStyle.values()).toStringList().sort();
combo = new JComboBox<>(li.toArray(new String[0]));
combo.setSelectedItem("STATIC");
combo.setFocusable(false);
combo.addActionListener(e -> {
@SuppressWarnings("unchecked")
String b = (String) (((JComboBox<String>) e.getSource()).getSelectedItem());
NoiseStyle s = NoiseStyle.valueOf(b);
nv.cng = s.create(RNG.r.nextParallelRNG(RNG.r.imax()));
});
combo.setSize(500, 30);
JLayeredPane pane = new JLayeredPane();
nv.setSize(new Dimension(1440, 820));
pane.add(nv, 1, 0);
pane.add(combo, 2, 0);
frame.add(pane);
File file = Iris.getCached("Iris Icon", "https://raw.githubusercontent.com/VolmitSoftware/Iris/master/icon.png");
if (file != null) {
try {
frame.setIconImage(ImageIO.read(file));
} catch (IOException e) {
Iris.reportError(e);
}
}
frame.setSize(1440, 820);
frame.setVisible(true);
frame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
Iris.instance.unregisterListener(nv);
}
public static void launch() {
Engine engine = findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI nv = new NoiseExplorerGUI();
buildFrame("Noise Explorer", nv, engine, null, null);
});
}
public static void launch(Supplier<Function2<Double, Double, Double>> gen, String genName) {
EventQueue.invokeLater(() -> createAndShowGUI(gen, genName));
Engine engine = findActiveEngine();
EventQueue.invokeLater(() -> {
NoiseExplorerGUI nv = new NoiseExplorerGUI();
nv.loader = gen;
nv.generator = gen.get();
nv.currentName = genName;
buildFrame("Noise Explorer: " + genName, nv, engine, gen, genName);
});
}
public static void launch() {
EventQueue.invokeLater(NoiseExplorerGUI::createAndShowGUI);
private static Engine findActiveEngine() {
try {
for (World w : new ArrayList<>(Bukkit.getWorlds())) {
try {
PlatformChunkGenerator access = IrisToolbelt.access(w);
if (access != null && access.getEngine() != null && !access.getEngine().isClosed()) {
return access.getEngine();
}
} catch (Throwable ignored) {}
}
} catch (Throwable ignored) {}
return null;
}
private static JFrame buildFrame(String title, NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.getContentPane().setBackground(BG);
frame.setLayout(new BorderLayout());
JPanel sidebar = buildSidebar(nv, engine, customGen, customName);
frame.add(sidebar, BorderLayout.WEST);
frame.add(nv, BorderLayout.CENTER);
frame.setSize(1440, 820);
frame.setMinimumSize(new Dimension(640, 480));
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
Iris.instance.unregisterListener(nv);
}
});
return frame;
}
private static JPanel buildSidebar(NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
JPanel sidebar = new JPanel(new BorderLayout());
sidebar.setPreferredSize(new Dimension(SIDEBAR_WIDTH, 0));
sidebar.setBackground(SIDEBAR_BG);
sidebar.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, SEPARATOR));
JTextField search = new JTextField();
search.setBackground(SEARCH_BG);
search.setForeground(SEARCH_FG);
search.setCaretColor(SEARCH_FG);
search.setFont(SEARCH_FONT);
search.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 0, SEPARATOR),
BorderFactory.createEmptyBorder(8, 10, 8, 10)
));
search.putClientProperty("JTextField.placeholderText", "Search...");
LinkedHashMap<String, List<ListItem>> categories = buildCategoryMap(nv, engine, customGen, customName);
DefaultListModel<ListItem> model = new DefaultListModel<>();
populateModel(model, categories, "");
JList<ListItem> list = new JList<>(model);
list.setBackground(SIDEBAR_BG);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setCellRenderer(new SidebarCellRenderer());
list.setFixedCellHeight(-1);
list.addListSelectionListener(e -> {
if (e.getValueIsAdjusting()) return;
ListItem selected = list.getSelectedValue();
if (selected != null && !selected.header && selected.action != null) {
selected.action.run();
}
});
search.getDocument().addDocumentListener(new DocumentListener() {
private void filter() {
String text = search.getText().trim();
populateModel(model, categories, text);
}
public void insertUpdate(DocumentEvent e) { filter(); }
public void removeUpdate(DocumentEvent e) { filter(); }
public void changedUpdate(DocumentEvent e) { filter(); }
});
JScrollPane scrollPane = new JScrollPane(list);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
scrollPane.getVerticalScrollBar().setUnitIncrement(16);
scrollPane.getVerticalScrollBar().setBackground(SIDEBAR_BG);
sidebar.add(search, BorderLayout.NORTH);
sidebar.add(scrollPane, BorderLayout.CENTER);
return sidebar;
}
private static void populateModel(DefaultListModel<ListItem> model, LinkedHashMap<String, List<ListItem>> categories, String filter) {
model.clear();
String lower = filter.toLowerCase();
for (Map.Entry<String, List<ListItem>> entry : categories.entrySet()) {
List<ListItem> matching = new ArrayList<>();
for (ListItem item : entry.getValue()) {
if (lower.isEmpty() || item.text.toLowerCase().contains(lower) || item.rawName.toLowerCase().contains(lower)) {
matching.add(item);
}
}
if (!matching.isEmpty()) {
model.addElement(new ListItem(entry.getKey(), entry.getKey(), true, null));
for (ListItem item : matching) {
model.addElement(item);
}
}
}
}
private static LinkedHashMap<String, List<ListItem>> buildCategoryMap(NoiseExplorerGUI nv, Engine engine,
Supplier<Function2<Double, Double, Double>> customGen, String customName) {
LinkedHashMap<String, List<ListItem>> categories = new LinkedHashMap<>();
if (customGen != null && customName != null) {
List<ListItem> custom = new ArrayList<>();
custom.add(new ListItem(customName, customName, false, () -> {
nv.generator = customGen.get();
nv.loader = customGen;
nv.currentName = customName;
}));
categories.put("Custom", custom);
}
Map<String, List<NoiseStyle>> styleGroups = new LinkedHashMap<>();
for (NoiseStyle style : NoiseStyle.values()) {
String cat = categorize(style);
styleGroups.computeIfAbsent(cat, k -> new ArrayList<>()).add(style);
}
if (engine != null && !engine.isClosed()) {
List<ListItem> genItems = new ArrayList<>();
try {
IrisData data = engine.getData();
String[] keys = data.getGeneratorLoader().getPossibleKeys();
Arrays.sort(keys);
for (String key : keys) {
IrisGenerator gen = data.getGeneratorLoader().load(key);
if (gen != null) {
long seed = new RNG(12345).nextParallelRNG(3245).lmax();
genItems.add(new ListItem(formatName(key), key, false, () -> {
nv.generator = (x, z) -> gen.getHeight(x, z, seed);
nv.loader = null;
nv.currentName = key;
}));
}
}
} catch (Throwable ignored) {}
if (!genItems.isEmpty()) {
categories.put("Pack Generators", genItems);
}
}
for (String cat : CATEGORY_ORDER) {
if ("Pack Generators".equals(cat)) continue;
List<NoiseStyle> styles = styleGroups.get(cat);
if (styles != null && !styles.isEmpty()) {
List<ListItem> items = new ArrayList<>();
for (NoiseStyle style : styles) {
items.add(new ListItem(formatName(style.name()), style.name(), false, () -> {
nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax()));
nv.generator = null;
nv.loader = null;
nv.currentName = style.name();
}));
}
categories.put(cat, items);
}
}
for (Map.Entry<String, List<NoiseStyle>> entry : styleGroups.entrySet()) {
if (!categories.containsKey(entry.getKey())) {
List<ListItem> items = new ArrayList<>();
for (NoiseStyle style : entry.getValue()) {
items.add(new ListItem(formatName(style.name()), style.name(), false, () -> {
nv.cng = style.create(RNG.r.nextParallelRNG(RNG.r.imax()));
nv.generator = null;
nv.loader = null;
nv.currentName = style.name();
}));
}
categories.put(entry.getKey(), items);
}
}
return categories;
}
private static String categorize(NoiseStyle style) {
String n = style.name();
if (n.startsWith("STATIC")) return "Static";
if (n.startsWith("IRIS")) return "Iris";
if (n.startsWith("CLOVER")) return "Clover";
if (n.startsWith("VASCULAR")) return "Vascular";
if (n.equals("FLAT")) return "Utility";
if (n.startsWith("CELLULAR")) return "Cellular";
if (n.startsWith("HEX") || n.equals("HEXAGON")) return "Hexagon";
if (n.startsWith("SIERPINSKI")) return "Sierpinski";
if (n.startsWith("NOWHERE")) return "Nowhere";
if (n.startsWith("GLOB")) return "Globe";
if (n.startsWith("PERLIN")) return "Perlin";
if (n.startsWith("CUBIC") || (n.startsWith("FRACTAL") && n.contains("CUBIC"))) return "Cubic";
if (n.contains("SIMPLEX") && !n.startsWith("FRACTAL")) return "Simplex";
if (n.startsWith("FRACTAL")) return "Fractal";
return "Other";
}
private static String formatName(String enumName) {
String lower = enumName.toLowerCase().replace('_', ' ');
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
}
@EventHandler
public void on(IrisEngineHotloadEvent e) {
if (generator != null)
if (generator != null && loader != null) {
generator = loader.get();
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
int notches = e.getWheelRotation();
if (e.isControlDown()) {
t = t + ((0.0025 * t) * notches);
time = time + ((0.0025 * time) * notches);
return;
}
scale = scale + ((0.044 * scale) * notches);
scale = Math.max(scale, 0.00001);
}
private double lerp(double current, double target, double speed) {
double diff = target - current;
if (Math.abs(diff) < 0.001) return target;
return current + diff * speed;
}
@Override
public void paint(Graphics g) {
if (scale < ascale) {
ascale -= Math.abs(scale - ascale) * 0.16;
}
if (scale > ascale) {
ascale += Math.abs(ascale - scale) * 0.16;
}
if (t < tz) {
tz -= Math.abs(t - tz) * 0.29;
}
if (t > tz) {
tz += Math.abs(tz - t) * 0.29;
}
if (ox < oxp) {
oxp -= Math.abs(ox - oxp) * 0.16;
}
if (ox > oxp) {
oxp += Math.abs(oxp - ox) * 0.16;
}
if (oz < ozp) {
ozp -= Math.abs(oz - ozp) * 0.16;
}
if (oz > ozp) {
ozp += Math.abs(ozp - oz) * 0.16;
}
if (mx < mxx) {
mxx -= Math.abs(mx - mxx) * 0.16;
}
if (mx > mxx) {
mxx += Math.abs(mxx - mx) * 0.16;
}
if (mz < mzz) {
mzz -= Math.abs(mz - mzz) * 0.16;
}
if (mz > mzz) {
mzz += Math.abs(mzz - mz) * 0.16;
}
animScale = lerp(animScale, scale, 0.16);
animTime = lerp(animTime, time, 0.29);
animOx = lerp(animOx, ox, 0.16);
animOz = lerp(animOz, oz, 0.16);
PrecisionStopwatch p = PrecisionStopwatch.start();
int accuracy = hd ? 1 : M.clip((r.getAverage() / 12D), 2D, 128D).intValue();
accuracy = down ? accuracy * 4 : accuracy;
int v = 1000;
if (g instanceof Graphics2D gg) {
gg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
if (getParent().getWidth() != w || getParent().getHeight() != h) {
w = getParent().getWidth();
h = getParent().getHeight();
int pw = getWidth();
int ph = getHeight();
if (pw != imgWidth || ph != imgHeight || img == null) {
imgWidth = pw;
imgHeight = ph;
img = null;
}
if (img == null) {
img = new BufferedImage(w / accuracy, h / accuracy, BufferedImage.TYPE_INT_RGB);
int accuracy = M.clip((fpsHistory.getAverage() / 14D), 1D, 64D).intValue();
int rw = Math.max(1, pw / accuracy);
int rh = Math.max(1, ph / accuracy);
if (img == null || img.getWidth() != rw || img.getHeight() != rh) {
img = new BufferedImage(rw, rh, BufferedImage.TYPE_INT_RGB);
}
BurstExecutor e = gx.burst(w);
int[] pixels = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
for (int x = 0; x < w / accuracy; x++) {
BurstExecutor burst = gx.burst(rw);
for (int x = 0; x < rw; x++) {
int xx = x;
burst.queue(() -> {
for (int z = 0; z < rh; z++) {
double worldX = (xx * accuracy * animScale) + animOx;
double worldZ = (z * accuracy * animScale) + animOz;
double n = generator != null
? generator.apply(worldX, worldZ)
: cng.noise(worldX, worldZ);
n = Math.max(0, Math.min(1, n));
int finalAccuracy = accuracy;
e.queue(() -> {
for (int z = 0; z < h / finalAccuracy; z++) {
double n = generator != null ? generator.apply(((xx * finalAccuracy) * ascale) + oxp, ((z * finalAccuracy) * ascale) + ozp) : cng.noise(((xx * finalAccuracy) * ascale) + oxp, ((z * finalAccuracy) * ascale) + ozp);
n = n > 1 ? 1 : n < 0 ? 0 : n;
try {
//Color color = colorMode ? Color.getHSBColor((float) (n), 1f - (float) (n * n * n * n * n * n), 1f - (float) n) : Color.getHSBColor(0f, 0f, (float) n);
Color color = colorMode ? Color.getHSBColor((float) (0.666f - n * 0.666f), 1f, (float) (1f - n * 0.8f)) : Color.getHSBColor(0f, 0f, (float) n);
int rgb = color.getRGB();
img.setRGB(xx, z, rgb);
} catch (Throwable ignored) {
int rgb;
if (colorMode) {
rgb = HSB_LUT[(int) (n * 255)];
} else {
int v = (int) (n * 255);
rgb = (v << 16) | (v << 8) | v;
}
pixels[z * rw + xx] = rgb;
}
});
}
burst.complete();
e.complete();
gg.drawImage(img, 0, 0, getParent().getWidth() * accuracy, getParent().getHeight() * accuracy, (img, infoflags, x, y, width, height) -> true);
gg.setColor(BG);
gg.fillRect(0, 0, pw, ph);
gg.drawImage(img, 0, 0, pw, ph, null);
renderStatusBar(gg, pw, ph, p.getMilliseconds());
renderCrosshair(gg, pw, ph);
}
p.end();
time += 1D;
fpsHistory.put(p.getMilliseconds());
t += 1D;
r.put(p.getMilliseconds());
if (!isVisible()) {
if (!isVisible() || !getParent().isVisible()) {
return;
}
if (!getParent().isVisible()) {
return;
}
if (!getParent().getParent().isVisible()) {
return;
}
EventQueue.invokeLater(() ->
{
J.sleep((long) Math.max(0, 32 - r.getAverage()));
long sleepMs = Math.max(1, 16 - (long) p.getMilliseconds());
EventQueue.invokeLater(() -> {
J.sleep(sleepMs);
repaint();
});
}
static class HandScrollListener extends MouseAdapter {
private static final Point pp = new Point();
private void renderCrosshair(Graphics2D g, int w, int h) {
int cx = w / 2;
int cy = h / 2;
g.setColor(new Color(255, 255, 255, 40));
g.drawLine(cx - 8, cy, cx + 8, cy);
g.drawLine(cx, cy - 8, cx, cy + 8);
}
@Override
public void mouseDragged(MouseEvent e) {
JViewport vport = (JViewport) e.getSource();
JComponent label = (JComponent) vport.getView();
Point cp = e.getPoint();
Point vp = vport.getViewPosition();
vp.translate(pp.x - cp.x, pp.y - cp.y);
label.scrollRectToVisible(new Rectangle(vp, vport.getSize()));
private void renderStatusBar(Graphics2D g, int w, int h, double frameMs) {
int barHeight = 28;
int y = h - barHeight;
pp.setLocation(cp);
g.setColor(STATUS_BG);
g.fillRect(0, y, w, barHeight);
g.setColor(new Color(50, 50, 60));
g.drawLine(0, y, w, y);
g.setFont(STATUS_FONT);
g.setColor(STATUS_TEXT);
double worldX = (w / 2.0 * animScale) + animOx;
double worldZ = (h / 2.0 * animScale) + animOz;
double noiseVal = generator != null
? generator.apply(worldX, worldZ)
: cng.noise(worldX, worldZ);
noiseVal = Math.max(0, Math.min(1, noiseVal));
int fps = frameMs > 0 ? (int) (1000.0 / frameMs) : 0;
String status = String.format(" %s | X: %.1f Z: %.1f | Zoom: %.4f | Value: %.4f | %d FPS",
currentName, worldX, worldZ, animScale, noiseVal, fps);
g.drawString(status, 8, y + 18);
int barW = 60;
int barX = w - barW - 12;
int barY = y + 6;
int barH = barHeight - 12;
g.setColor(new Color(40, 40, 48));
g.fillRoundRect(barX, barY, barW, barH, 4, 4);
int fillW = (int) (noiseVal * (barW - 2));
g.setColor(ACCENT);
g.fillRoundRect(barX + 1, barY + 1, fillW, barH - 2, 3, 3);
}
private static final class ListItem {
final String text;
final String rawName;
final boolean header;
final Runnable action;
ListItem(String text, String rawName, boolean header, Runnable action) {
this.text = text;
this.rawName = rawName;
this.header = header;
this.action = action;
}
@Override
public void mousePressed(MouseEvent e) {
pp.setLocation(e.getPoint());
public String toString() {
return text;
}
}
}
private static final class SidebarCellRenderer extends DefaultListCellRenderer {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean selected, boolean focus) {
ListItem item = (ListItem) value;
super.getListCellRendererComponent(list, item.text, index, !item.header && selected, false);
setOpaque(true);
if (item.header) {
setFont(SIDEBAR_HEADER_FONT);
setForeground(ACCENT);
setBackground(SIDEBAR_BG);
setBorder(BorderFactory.createEmptyBorder(10, 10, 4, 10));
} else {
setFont(SIDEBAR_ITEM_FONT);
setForeground(selected ? Color.WHITE : SIDEBAR_ITEM_COLOR);
setBackground(selected ? SIDEBAR_SELECTED : SIDEBAR_BG);
setBorder(BorderFactory.createEmptyBorder(3, 20, 3, 10));
}
return this;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,84 +1,80 @@
/*
* 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.gui.components;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.function.BiFunction;
public class IrisRenderer {
private final Engine renderer;
public IrisRenderer(Engine renderer) {
this.renderer = renderer;
}
public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) {
BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB);
BiFunction<Double, Double, Integer> colorFunction = (d, dx) -> Color.black.getRGB();
switch (currentType) {
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD ->
colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case BIOME_LAND ->
colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case BIOME_SEA ->
colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case REGION ->
colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB();
case CAVE_LAND ->
colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case HEIGHT ->
colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 100, 100).getRGB();
case CONTINENT -> colorFunction = (x, z) -> {
IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z));
IrisBiomeGeneratorLink g = b.getGenerators().get(0);
Color c;
if (g.getMax() <= 0) {
// Max is below water level, so it is most likely an ocean biome
c = Color.BLUE;
} else if (g.getMin() < 0) {
// Min is below water level, but max is not, so it is most likely a shore biome
c = Color.YELLOW;
} else {
// Both min and max are above water level, so it is most likely a land biome
c = Color.GREEN;
}
return c.getRGB();
};
}
double x, z;
int i, j;
for (i = 0; i < resolution; i++) {
x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) (resolution));
for (j = 0; j < resolution; j++) {
z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) (resolution));
image.setRGB(i, j, colorFunction.apply(x, z));
}
}
return image;
}
}
/*
* 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.gui.components;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.function.BiFunction;
public class IrisRenderer {
private static final int BLUE = Color.BLUE.getRGB();
private static final int YELLOW = Color.YELLOW.getRGB();
private static final int GREEN = Color.GREEN.getRGB();
private final Engine renderer;
public IrisRenderer(Engine renderer) {
this.renderer = renderer;
}
public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) {
BufferedImage image = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_RGB);
int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
BiFunction<Double, Double, Integer> colorFunction = (d, dx) -> 0;
switch (currentType) {
case BIOME, DECORATOR_LOAD, OBJECT_LOAD, LAYER_LOAD ->
colorFunction = (x, z) -> renderer.getComplex().getTrueBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case BIOME_LAND ->
colorFunction = (x, z) -> renderer.getComplex().getLandBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case BIOME_SEA ->
colorFunction = (x, z) -> renderer.getComplex().getSeaBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case REGION ->
colorFunction = (x, z) -> renderer.getComplex().getRegionStream().get(x, z).getColor(renderer.getComplex(), currentType).getRGB();
case CAVE_LAND ->
colorFunction = (x, z) -> renderer.getComplex().getCaveBiomeStream().get(x, z).getColor(renderer, currentType).getRGB();
case HEIGHT ->
colorFunction = (x, z) -> Color.getHSBColor(renderer.getComplex().getHeightStream().get(x, z).floatValue(), 1f, 1f).getRGB();
case CONTINENT -> colorFunction = (x, z) -> {
IrisBiome b = renderer.getBiome((int) Math.round(x), renderer.getMaxHeight() - 1, (int) Math.round(z));
IrisBiomeGeneratorLink g = b.getGenerators().get(0);
if (g.getMax() <= 0) return BLUE;
if (g.getMin() < 0) return YELLOW;
return GREEN;
};
}
double x, z;
for (int i = 0; i < resolution; i++) {
x = IrisInterpolation.lerp(sx, sx + size, (double) i / (double) resolution);
for (int j = 0; j < resolution; j++) {
z = IrisInterpolation.lerp(sz, sz + size, (double) j / (double) resolution);
pixels[j * resolution + i] = colorFunction.apply(x, z);
}
}
return image;
}
}
@@ -47,6 +47,7 @@ import org.bukkit.inventory.ItemStack;
import java.awt.Color;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public interface INMSBinding {
@@ -172,6 +173,10 @@ public interface INMSBinding {
KMap<Identifier, StructurePlacement> collectStructures();
default Map<String, byte[]> extractVanillaDatapack() {
return Map.of();
}
private void validateDimensionTypes(WorldCreator c) {
if (c.generator() instanceof PlatformChunkGenerator gen
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) {
@@ -0,0 +1,56 @@
/*
* 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.pack;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class BrokenPackException extends RuntimeException {
private final String packName;
private final List<String> reasons;
public BrokenPackException(String packName, List<String> reasons) {
super(buildMessage(packName, reasons));
this.packName = packName;
this.reasons = reasons == null ? new ArrayList<>() : new ArrayList<>(reasons);
}
public String getPackName() {
return packName;
}
public List<String> getReasons() {
return Collections.unmodifiableList(reasons);
}
private static String buildMessage(String packName, List<String> reasons) {
StringBuilder sb = new StringBuilder();
sb.append("Iris pack '").append(packName).append("' is broken and cannot be used for world or studio creation.");
if (reasons != null) {
for (String reason : reasons) {
if (reason == null || reason.isBlank()) {
continue;
}
sb.append(System.lineSeparator()).append(" - ").append(reason);
}
}
return sb.toString();
}
}
@@ -45,7 +45,7 @@ public class IrisPackRepository {
private String repo = "overworld";
@Builder.Default
private String branch = "master";
private String branch = "stable";
@Builder.Default
private String tag = "";
@@ -94,7 +94,7 @@ public class IrisPackRepository {
return IrisPackRepository.builder()
.user("IrisDimensions")
.repo(g)
.branch(g.equals("overworld") ? "stable" : "master")
.branch("stable")
.build();
}
@@ -0,0 +1,64 @@
/*
* 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.pack;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class PackValidationRegistry {
private static final Map<String, PackValidationResult> RESULTS = new ConcurrentHashMap<>();
private PackValidationRegistry() {
}
public static void publish(PackValidationResult result) {
if (result == null || result.getPackName() == null || result.getPackName().isBlank()) {
return;
}
RESULTS.put(result.getPackName(), result);
}
public static PackValidationResult get(String packName) {
if (packName == null || packName.isBlank()) {
return null;
}
return RESULTS.get(packName);
}
public static boolean isBroken(String packName) {
PackValidationResult result = get(packName);
return result != null && !result.isLoadable();
}
public static Map<String, PackValidationResult> snapshot() {
return Collections.unmodifiableMap(RESULTS);
}
public static void remove(String packName) {
if (packName == null || packName.isBlank()) {
return;
}
RESULTS.remove(packName);
}
public static void clear() {
RESULTS.clear();
}
}
@@ -0,0 +1,67 @@
/*
* 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.pack;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class PackValidationResult {
private final String packName;
private final List<String> blockingErrors;
private final List<String> warnings;
private final List<String> removedUnusedFiles;
private final long validatedAtMillis;
public PackValidationResult(String packName,
List<String> blockingErrors,
List<String> warnings,
List<String> removedUnusedFiles,
long validatedAtMillis) {
this.packName = packName;
this.blockingErrors = blockingErrors == null ? new ArrayList<>() : new ArrayList<>(blockingErrors);
this.warnings = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings);
this.removedUnusedFiles = removedUnusedFiles == null ? new ArrayList<>() : new ArrayList<>(removedUnusedFiles);
this.validatedAtMillis = validatedAtMillis;
}
public String getPackName() {
return packName;
}
public boolean isLoadable() {
return blockingErrors.isEmpty();
}
public List<String> getBlockingErrors() {
return Collections.unmodifiableList(blockingErrors);
}
public List<String> getWarnings() {
return Collections.unmodifiableList(warnings);
}
public List<String> getRemovedUnusedFiles() {
return Collections.unmodifiableList(removedUnusedFiles);
}
public long getValidatedAtMillis() {
return validatedAtMillis;
}
}
@@ -0,0 +1,369 @@
/*
* 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.pack;
import art.arcane.iris.Iris;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
public final class PackValidator {
private static final String TRASH_ROOT = ".iris-trash";
private static final String DATAPACK_IMPORTS = "datapack-imports";
private static final String EXTERNAL_DATAPACKS = "externaldatapacks";
private static final String OBJECTS_FOLDER = "objects";
private static final String DIMENSIONS_FOLDER = "dimensions";
private static final List<String> MANAGED_RESOURCE_FOLDERS = List.of(
"biomes",
"regions",
"entities",
"spawners",
"loot",
"generators",
"expressions",
"markers",
"blocks",
"mods"
);
private static final DateTimeFormatter TRASH_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
private PackValidator() {
}
public static PackValidationResult validate(File packFolder) {
String packName = packFolder == null ? "<unknown>" : packFolder.getName();
List<String> blockingErrors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
List<String> removedUnusedFiles = new ArrayList<>();
long validatedAt = System.currentTimeMillis();
if (packFolder == null || !packFolder.isDirectory()) {
blockingErrors.add("Pack folder does not exist or is not a directory.");
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
File dimensionsFolder = new File(packFolder, DIMENSIONS_FOLDER);
if (!dimensionsFolder.isDirectory()) {
blockingErrors.add("Missing dimensions/ folder.");
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
File[] dimensionFiles = dimensionsFolder.listFiles(f -> f.isFile() && f.getName().endsWith(".json"));
if (dimensionFiles == null || dimensionFiles.length == 0) {
blockingErrors.add("No dimension JSON files under dimensions/.");
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
validateDimensions(packFolder, dimensionFiles, blockingErrors, warnings);
try {
String packTextCorpus = buildPackTextCorpus(packFolder);
runUnusedResourceGc(packFolder, packTextCorpus, removedUnusedFiles, warnings);
} catch (Throwable e) {
Iris.reportError("PackValidator GC pass failed for pack '" + packName + "'", e);
warnings.add("Unused-resource GC pass failed: " + e.getMessage());
}
return new PackValidationResult(packName, blockingErrors, warnings, removedUnusedFiles, validatedAt);
}
private static void validateDimensions(File packFolder, File[] dimensionFiles, List<String> blockingErrors, List<String> warnings) {
File regionsFolder = new File(packFolder, "regions");
File biomesFolder = new File(packFolder, "biomes");
for (File dimFile : dimensionFiles) {
String dimensionKey = stripExtension(dimFile.getName());
JSONObject dimJson;
try {
dimJson = new JSONObject(Files.readString(dimFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
blockingErrors.add("Dimension '" + dimensionKey + "' has invalid JSON: " + e.getMessage());
continue;
}
JSONArray regionsArray = dimJson.optJSONArray("regions");
if (regionsArray == null || regionsArray.length() == 0) {
blockingErrors.add("Dimension '" + dimensionKey + "' declares no regions.");
continue;
}
int resolvedRegions = 0;
for (int i = 0; i < regionsArray.length(); i++) {
String regionKey = regionsArray.optString(i, null);
if (regionKey == null || regionKey.isBlank()) {
warnings.add("Dimension '" + dimensionKey + "' has a blank region entry at index " + i + ".");
continue;
}
File regionFile = new File(regionsFolder, regionKey + ".json");
if (!regionFile.isFile()) {
blockingErrors.add("Dimension '" + dimensionKey + "' references missing region '" + regionKey + "'.");
continue;
}
JSONObject regionJson;
try {
regionJson = new JSONObject(Files.readString(regionFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
blockingErrors.add("Region '" + regionKey + "' has invalid JSON: " + e.getMessage());
continue;
}
int anyBiome = countBiomeRefs(regionJson, "landBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "seaBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "shoreBiomes", biomesFolder, regionKey, warnings)
+ countBiomeRefs(regionJson, "caveBiomes", biomesFolder, regionKey, warnings);
if (anyBiome == 0) {
blockingErrors.add("Region '" + regionKey + "' has no resolvable biomes.");
}
resolvedRegions++;
}
if (resolvedRegions == 0) {
blockingErrors.add("Dimension '" + dimensionKey + "' has no resolvable regions.");
}
}
}
private static int countBiomeRefs(JSONObject regionJson, String field, File biomesFolder, String regionKey, List<String> warnings) {
JSONArray arr = regionJson.optJSONArray(field);
if (arr == null) {
return 0;
}
int resolved = 0;
for (int i = 0; i < arr.length(); i++) {
String biomeKey = arr.optString(i, null);
if (biomeKey == null || biomeKey.isBlank()) {
continue;
}
File biomeFile = new File(biomesFolder, biomeKey + ".json");
if (!biomeFile.isFile()) {
warnings.add("Region '" + regionKey + "' references missing biome '" + biomeKey + "' in " + field + ".");
continue;
}
resolved++;
}
return resolved;
}
private static String buildPackTextCorpus(File packFolder) {
StringBuilder sb = new StringBuilder(1 << 16);
try (Stream<Path> stream = Files.walk(packFolder.toPath())) {
stream.filter(Files::isRegularFile)
.filter(PackValidator::isScannableJsonPath)
.forEach(p -> {
try {
sb.append(Files.readString(p, StandardCharsets.UTF_8));
sb.append('\n');
} catch (Throwable ignored) {
}
});
} catch (Throwable e) {
Iris.reportError("PackValidator failed to walk pack folder for corpus scan", e);
}
return sb.toString();
}
private static boolean isScannableJsonPath(Path path) {
String name = path.getFileName().toString();
if (!name.endsWith(".json")) {
return false;
}
String str = path.toString().replace(File.separatorChar, '/');
if (str.contains("/" + TRASH_ROOT + "/")) {
return false;
}
if (str.contains("/" + DATAPACK_IMPORTS + "/")) {
return false;
}
if (str.contains("/" + EXTERNAL_DATAPACKS + "/")) {
return false;
}
if (str.contains("/" + OBJECTS_FOLDER + "/")) {
return false;
}
if (str.contains("/.iris/")) {
return false;
}
return true;
}
private static void runUnusedResourceGc(File packFolder, String corpus, List<String> removedUnusedFiles, List<String> warnings) {
if (corpus == null || corpus.isEmpty()) {
return;
}
File trashRoot = new File(packFolder, TRASH_ROOT + File.separator + LocalDateTime.now().format(TRASH_STAMP));
Set<File> scheduledForTrash = new LinkedHashSet<>();
for (String folderName : MANAGED_RESOURCE_FOLDERS) {
File resourceFolder = new File(packFolder, folderName);
if (!resourceFolder.isDirectory()) {
continue;
}
List<File> files = listJsonRecursive(resourceFolder);
for (File resourceFile : files) {
String key = deriveKey(resourceFolder, resourceFile);
if (key == null || key.isBlank()) {
continue;
}
if (isReferenced(corpus, key)) {
continue;
}
scheduledForTrash.add(resourceFile);
}
}
if (scheduledForTrash.isEmpty()) {
return;
}
for (File file : scheduledForTrash) {
try {
Path src = file.toPath();
Path relative = packFolder.toPath().relativize(src);
Path dest = trashRoot.toPath().resolve(relative);
Files.createDirectories(dest.getParent());
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
removedUnusedFiles.add(relative.toString().replace(File.separatorChar, '/'));
} catch (Throwable e) {
Iris.reportError("PackValidator failed to move unused file " + file.getPath() + " to trash", e);
warnings.add("Failed to quarantine unused file " + file.getName() + ": " + e.getMessage());
}
}
}
private static boolean isReferenced(String corpus, String key) {
String needleQuoted = "\"" + key + "\"";
if (corpus.contains(needleQuoted)) {
return true;
}
int slash = key.indexOf('/');
if (slash > 0) {
String tail = key.substring(slash + 1);
if (!tail.isBlank() && corpus.contains("\"" + tail + "\"")) {
return true;
}
}
return false;
}
private static List<File> listJsonRecursive(File root) {
List<File> out = new ArrayList<>();
try (Stream<Path> stream = Files.walk(root.toPath())) {
stream.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".json"))
.forEach(p -> out.add(p.toFile()));
} catch (Throwable ignored) {
}
return out;
}
private static String deriveKey(File resourceFolder, File resourceFile) {
Path relative = resourceFolder.toPath().relativize(resourceFile.toPath());
String str = relative.toString().replace(File.separatorChar, '/');
if (!str.endsWith(".json")) {
return null;
}
return str.substring(0, str.length() - ".json".length());
}
private static String stripExtension(String name) {
int dot = name.lastIndexOf('.');
return dot <= 0 ? name : name.substring(0, dot);
}
public static int restoreTrash(File packFolder) {
if (packFolder == null || !packFolder.isDirectory()) {
return 0;
}
File trashRoot = new File(packFolder, TRASH_ROOT);
if (!trashRoot.isDirectory()) {
return 0;
}
File[] dumps = trashRoot.listFiles(File::isDirectory);
if (dumps == null || dumps.length == 0) {
return 0;
}
Arrays.sort(dumps, Comparator.comparing(File::getName));
File latestDump = dumps[dumps.length - 1];
int restored = 0;
try (Stream<Path> stream = Files.walk(latestDump.toPath())) {
List<Path> files = stream.filter(Files::isRegularFile).toList();
for (Path src : files) {
Path relative = latestDump.toPath().relativize(src);
Path dest = packFolder.toPath().resolve(relative);
Files.createDirectories(dest.getParent());
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
restored++;
}
} catch (Throwable e) {
Iris.reportError("PackValidator failed to restore trash for pack " + packFolder.getName(), e);
}
deleteFolderQuiet(latestDump);
return restored;
}
private static void deleteFolderQuiet(File folder) {
if (folder == null || !folder.exists()) {
return;
}
try (Stream<Path> stream = Files.walk(folder.toPath())) {
stream.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
} catch (Throwable ignored) {
}
}
public static Set<String> listReferencedKeysFromCorpus(String corpus) {
Set<String> keys = new HashSet<>();
if (corpus == null) {
return keys;
}
int i = 0;
while (i < corpus.length()) {
int start = corpus.indexOf('"', i);
if (start < 0) {
break;
}
int end = corpus.indexOf('"', start + 1);
if (end < 0) {
break;
}
keys.add(corpus.substring(start + 1, end));
i = end + 1;
}
return keys;
}
}
@@ -157,16 +157,15 @@ public class DeepSearchPregenerator extends Thread implements Listener {
for (int j = 0; j < 16; j++) {
int height = engine.getHeight(xx + i, zz + j);
if (height > 300) {
File found = new File("plugins" + "iris" + "found.txt");
FileWriter writer = new FileWriter(found);
if (!found.exists()) {
found.createNewFile();
}
File found = new File("plugins", "iris" + File.separator + "found.txt");
found.getParentFile().mkdirs();
IrisBiome biome = engine.getBiome(xx, engine.getHeight(), zz);
Iris.info("Found at! " + xx + ", " + zz + "Biome ID: " + biome.getName() + ", ");
writer.write("Biome at: X: " + xx + " Z: " + zz + "Biome ID: " + biome.getName() + ", ");
Iris.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;
}
}
}
}
}
@@ -156,11 +156,16 @@ public class IrisPregenerator {
}
private long computeETA() {
double d = (long) (generated.get() > 1024 ? // Generated chunks exceed 1/8th of total?
// If yes, use smooth function (which gets more accurate over time since its less sensitive to outliers)
((totalChunks.get() - generated.get()) * ((double) (M.ms() - startTime.get()) / (double) generated.get())) :
// If no, use quick function (which is less accurate over time but responds better to the initial delay)
((totalChunks.get() - generated.get()) / chunksPerSecond.getAverage()) * 1000);
long gen = generated.get();
long total = totalChunks.get();
long remaining = total - gen;
double d;
if (gen > 1024) {
d = remaining * ((double) (M.ms() - startTime.get()) / (double) gen);
} else {
double cps = chunksPerSecond.getAverage();
d = cps > 0 ? (remaining / cps) * 1000 : 0;
}
return Double.isFinite(d) && d != INVALID ? (long) d : 0;
}
@@ -175,11 +180,7 @@ public class IrisPregenerator {
checkRegions();
PrecisionStopwatch p = PrecisionStopwatch.start();
task.iterateRegions((x, z) -> visitRegion(x, z, true));
if (generator.isAsyncChunkMode()) {
visitChunksInterleaved();
} else {
task.iterateRegions((x, z) -> visitRegion(x, z, false));
}
task.iterateRegions((x, z) -> visitRegion(x, z, false));
Iris.info("Pregen took " + Form.duration((long) p.getMilliseconds()));
shutdown();
if (benchmarking == null) {
@@ -248,6 +249,10 @@ public class IrisPregenerator {
if (saveLatch.flip()) {
listener.onSaving();
generator.save();
Mantle mantle = getMantle();
if (mantle != null) {
mantle.trim(0, 0);
}
}
generatedRegions.add(pos);
@@ -263,46 +268,6 @@ public class IrisPregenerator {
generator.supportsRegions(x, z, listener);
}
private void visitChunksInterleaved() {
task.iterateAllChunksInterleaved((regionX, regionZ, chunkX, chunkZ, firstChunkInRegion, lastChunkInRegion) -> {
while (paused.get() && !shutdown.get()) {
J.sleep(50);
}
Position2 regionPos = new Position2(regionX, regionZ);
if (shutdown.get()) {
if (!generatedRegions.contains(regionPos)) {
listener.onRegionSkipped(regionX, regionZ);
generatedRegions.add(regionPos);
}
return false;
}
if (generatedRegions.contains(regionPos)) {
return true;
}
if (firstChunkInRegion) {
currentGeneratorMethod.set(generator.getMethod(regionX, regionZ));
listener.onRegionGenerating(regionX, regionZ);
}
generator.generateChunk(chunkX, chunkZ, listener);
if (lastChunkInRegion) {
listener.onRegionGenerated(regionX, regionZ);
if (saveLatch.flip()) {
listener.onSaving();
generator.save();
}
generatedRegions.add(regionPos);
checkRegions();
}
return true;
});
}
public void pause() {
paused.set(true);
}
@@ -121,56 +121,6 @@ public class PregenTask {
iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s)));
}
public void iterateAllChunksInterleaved(InterleavedChunkSpiraled spiraled) {
if (spiraled == null) {
return;
}
KList<RegionChunkCursor> cursors = new KList<>();
Bound bound = bounds.chunk();
iterateRegions((regionX, regionZ) -> {
RegionChunkCursor cursor = new RegionChunkCursor(regionX, regionZ, bound);
if (cursor.hasNext()) {
cursors.add(cursor);
}
});
boolean hasProgress = true;
while (hasProgress) {
hasProgress = false;
for (RegionChunkCursor cursor : cursors) {
if (!cursor.hasNext()) {
continue;
}
hasProgress = true;
long chunk = cursor.next();
if (chunk == Long.MIN_VALUE) {
continue;
}
int chunkX = (int) (chunk >> 32);
int chunkZ = (int) chunk;
boolean shouldContinue = spiraled.on(
cursor.getRegionX(),
cursor.getRegionZ(),
chunkX,
chunkZ,
cursor.getIndex() == 1,
!cursor.hasNext()
);
if (!shouldContinue) {
return;
}
}
}
}
@FunctionalInterface
public interface InterleavedChunkSpiraled {
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
}
private class Bounds {
private Bound chunk = null;
private Bound region = null;
@@ -206,9 +156,9 @@ public class PregenTask {
}
}
private record Bound(int minX, int maxX, int minZ, int maxZ, int sizeX, int sizeZ) {
private record Bound(int minX, int minZ, int maxX, int maxZ, int sizeX, int sizeZ) {
private Bound(int minX, int minZ, int maxX, int maxZ) {
this(minX, maxX, minZ, maxZ, maxZ - minZ + 1, maxZ - minZ + 1);
this(minX, minZ, maxX, maxZ, maxX - minX + 1, maxZ - minZ + 1);
}
boolean check(int x, int z) {
@@ -231,76 +181,4 @@ public class PregenTask {
throw new IllegalStateException("This Position2 may not be modified");
}
}
private static final class RegionChunkCursor {
private final int regionX;
private final int regionZ;
private final Bound bound;
private final int[] order;
private final int chunkOffsetX;
private final int chunkOffsetZ;
private int scanIndex;
private int emittedIndex;
private int currentChunkX;
private int currentChunkZ;
private boolean hasCurrent;
private RegionChunkCursor(int regionX, int regionZ, Bound bound) {
this.regionX = regionX;
this.regionZ = regionZ;
this.bound = bound;
this.chunkOffsetX = PowerOfTwoCoordinates.regionToChunk(regionX);
this.chunkOffsetZ = PowerOfTwoCoordinates.regionToChunk(regionZ);
this.order = orderForPull(-chunkOffsetX, -chunkOffsetZ);
this.scanIndex = 0;
this.emittedIndex = 0;
advance();
}
private boolean hasNext() {
return hasCurrent;
}
private long next() {
if (!hasNext()) {
return Long.MIN_VALUE;
}
long high = (long) currentChunkX << 32;
long low = currentChunkZ & 0xFFFFFFFFL;
emittedIndex++;
advance();
return high | low;
}
private void advance() {
hasCurrent = false;
while (scanIndex < order.length) {
int local = order[scanIndex];
scanIndex++;
int chunkX = chunkOffsetX + PowerOfTwoCoordinates.unpackLocal32X(local);
int chunkZ = chunkOffsetZ + PowerOfTwoCoordinates.unpackLocal32Z(local);
if (!bound.check(chunkX, chunkZ)) {
continue;
}
currentChunkX = chunkX;
currentChunkZ = chunkZ;
hasCurrent = true;
return;
}
}
private int getRegionX() {
return regionX;
}
private int getRegionZ() {
return regionZ;
}
private int getIndex() {
return emittedIndex;
}
}
}
@@ -293,49 +293,138 @@ public class IrisProject {
return future;
}
private static final int STUDIO_PROGRESS_BAR_WIDTH = 44;
private void startStudioOpenReporter(VolmitSender sender, AtomicReference<String> stage, AtomicReference<Double> progress, AtomicBoolean complete, AtomicBoolean failed) {
String[] spinner = {"|", "/", "-", "\\"};
AtomicInteger spinIndex = new AtomicInteger(0);
AtomicLong nextConsoleUpdate = new AtomicLong(0L);
AtomicLong startMs = new AtomicLong(System.currentTimeMillis());
AtomicInteger taskId = new AtomicInteger(-1);
org.bukkit.boss.BossBar bossBar;
if (sender.isPlayer() && sender.player() != null) {
bossBar = Bukkit.createBossBar(
C.GOLD + "Studio " + C.AQUA + "OPENING",
org.bukkit.boss.BarColor.BLUE,
org.bukkit.boss.BarStyle.SEGMENTED_20
);
bossBar.setProgress(0.0D);
bossBar.addPlayer(sender.player());
bossBar.setVisible(true);
} else {
bossBar = null;
}
int scheduledTaskId = J.ar(() -> {
double currentProgress = Math.max(0D, Math.min(0.99D, progress.get()));
String currentStage = describeStage(stage.get());
int percent = (int) Math.round(currentProgress * 100.0D);
long elapsed = System.currentTimeMillis() - startMs.get();
if (complete.get()) {
J.car(taskId.get());
if (failed.get()) {
if (bossBar != null) {
bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress)));
bossBar.setColor(org.bukkit.boss.BarColor.RED);
bossBar.setTitle(C.GOLD + "Studio " + C.RED + "FAILED" + C.GRAY + " " + C.YELLOW + percent + "%");
J.a(() -> { bossBar.removeAll(); bossBar.setVisible(false); }, 60);
}
if (sender.isPlayer()) {
sender.sendProgress(1D, "Studio open failed");
String action = buildStudioProgressBar(currentProgress)
+ C.GRAY + " " + C.RED + "FAILED"
+ C.GRAY + " | " + C.WHITE + currentStage;
sender.sendAction(action);
} else {
sender.sendMessage(C.RED + "Studio open failed.");
}
} else if (sender.isPlayer()) {
sender.sendProgress(1D, "Studio ready");
} else {
sender.sendMessage(C.GREEN + "Studio ready.");
if (bossBar != null) {
bossBar.setProgress(1.0D);
bossBar.setColor(org.bukkit.boss.BarColor.GREEN);
bossBar.setTitle(C.GOLD + "Studio " + C.GREEN + "READY" + C.GRAY + " " + C.YELLOW + "100%");
J.a(() -> { bossBar.removeAll(); bossBar.setVisible(false); }, 60);
}
if (sender.isPlayer()) {
String action = buildStudioProgressBar(1.0D)
+ C.GRAY + " " + C.GREEN + "100%"
+ C.GRAY + " | " + C.GREEN + "Studio ready"
+ C.DARK_GRAY + " " + Form.duration(elapsed, 1);
sender.sendAction(action);
} else {
sender.sendMessage(C.GREEN + "Studio ready " + C.GRAY + "(" + Form.duration(elapsed, 1) + ")");
}
}
return;
}
double currentProgress = Math.max(0D, Math.min(0.97D, progress.get()));
String currentStage = stage.get();
String currentSpinner = spinner[Math.floorMod(spinIndex.getAndIncrement(), spinner.length)];
if (sender.isPlayer() && sender.player() != null) {
if (bossBar != null) {
bossBar.setProgress(Math.max(0.0D, Math.min(1.0D, currentProgress)));
bossBar.setTitle(C.GOLD + "Studio " + C.AQUA + "OPENING" + C.GRAY + " " + C.YELLOW + percent + "%");
}
if (sender.isPlayer()) {
sender.sendProgress(currentProgress, "Studio " + currentSpinner + " " + currentStage);
return;
}
long now = System.currentTimeMillis();
long nextUpdate = nextConsoleUpdate.get();
if (now >= nextUpdate) {
sender.sendMessage(C.WHITE + "Studio " + Form.pc(currentProgress, 0) + C.GRAY + " - " + currentStage);
nextConsoleUpdate.set(now + 1500L);
String action = buildStudioProgressBar(currentProgress)
+ C.GRAY + " " + C.YELLOW + percent + "%"
+ C.GRAY + " | " + C.WHITE + currentStage
+ C.DARK_GRAY + " " + Form.duration(elapsed, 0);
sender.sendAction(action);
} else {
long now = System.currentTimeMillis();
long nextUpdate = nextConsoleUpdate.get();
if (now >= nextUpdate) {
String bar = buildStudioConsoleBar(currentProgress);
sender.sendMessage(C.GOLD + "Studio " + C.AQUA + bar + " " + C.YELLOW + percent + "%" + C.GRAY + " " + currentStage + C.DARK_GRAY + " (" + Form.duration(elapsed, 0) + ")");
nextConsoleUpdate.set(now + 1500L);
}
}
}, 3);
taskId.set(scheduledTaskId);
}
private static String buildStudioProgressBar(double progress) {
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * STUDIO_PROGRESS_BAR_WIDTH);
StringBuilder bar = new StringBuilder(STUDIO_PROGRESS_BAR_WIDTH * 3 + 4);
bar.append(C.DARK_GRAY).append("[");
for (int i = 0; i < STUDIO_PROGRESS_BAR_WIDTH; i++) {
bar.append(i < filled ? C.GREEN : C.DARK_GRAY).append("|");
}
bar.append(C.DARK_GRAY).append("]");
return bar.toString();
}
private static String buildStudioConsoleBar(double progress) {
int width = 20;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * width);
StringBuilder bar = new StringBuilder();
bar.append("[");
for (int i = 0; i < width; i++) {
bar.append(i < filled ? "#" : "-");
}
bar.append("]");
return bar.toString();
}
private static String describeStage(String stage) {
if (stage == null || stage.isBlank()) return "Initializing";
return switch (stage) {
case "Queued" -> "Queued";
case "resolve_dimension" -> "Resolving dimension";
case "prepare_world_pack" -> "Preparing world pack";
case "install_datapacks" -> "Installing datapacks";
case "create_world" -> "Creating world";
case "apply_world_rules" -> "Applying world rules";
case "prepare_generator" -> "Preparing generator";
case "request_entry_chunk" -> "Loading entry chunk";
case "resolve_safe_entry" -> "Finding safe spawn";
case "teleport_player" -> "Teleporting";
case "finalize_open" -> "Finalizing";
case "cleanup" -> "Cleaning up";
default -> Form.capitalizeWords(stage.replace('_', ' '));
};
}
public CompletableFuture<StudioOpenCoordinator.StudioCloseResult> close() {
if (activeProvider == null) {
return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null));
@@ -294,19 +294,12 @@ public final class WorldRuntimeControlService {
}
static int[] buildSafeLocationScanOrder(World world, Location source) {
int x = source.getBlockX();
int z = source.getBlockZ();
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
int highestY = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(x, z) + 1));
int[] scanOrder = new int[maxY - minY + 1];
int index = 0;
for (int y = highestY; y >= minY; y--) {
scanOrder[index++] = y;
}
for (int y = highestY + 1; y <= maxY; y++) {
for (int y = maxY; y >= minY; y--) {
scanOrder[index++] = y;
}
@@ -26,6 +26,8 @@ import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pack.IrisPack;
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.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.tools.IrisToolbelt;
@@ -69,12 +71,12 @@ public class StudioSVC implements IrisService {
File f = IrisPack.packsPack(pack);
if (!f.exists()) {
Iris.info("Downloading Default Pack " + pack);
if (pack.equals("overworld")) {
Iris.info("Downloading Default Pack " + pack);
String url = "https://github.com/IrisDimensions/overworld/releases/download/" + INMS.OVERWORLD_TAG + "/overworld.zip";
Iris.service(StudioSVC.class).downloadRelease(Iris.getSender(), url, false, false);
} else {
downloadSearch(Iris.getSender(), pack, false);
Iris.warn("Default pack '" + pack + "' is not installed. Please download it manually with /iris download");
}
}
});
@@ -130,11 +132,14 @@ public class StudioSVC implements IrisService {
IrisDimension dim = IrisData.loadAnyDimension(type, null);
if (dim == null) {
for (File i : getWorkspaceFolder().listFiles()) {
if (i.isFile() && i.getName().equals(type + ".iris")) {
sender.sendMessage("Found " + type + ".iris in " + WORKSPACE_NAME + " folder");
ZipUtil.unpack(i, folder);
break;
File[] workspaceFiles = getWorkspaceFolder().listFiles();
if (workspaceFiles != null) {
for (File i : workspaceFiles) {
if (i.isFile() && i.getName().equals(type + ".iris")) {
sender.sendMessage("Found " + type + ".iris in " + WORKSPACE_NAME + " folder");
ZipUtil.unpack(i, folder);
break;
}
}
}
} else {
@@ -153,26 +158,29 @@ public class StudioSVC implements IrisService {
if (!dimensionFile.exists() || !dimensionFile.isFile()) {
downloadSearch(sender, type, false);
File downloaded = getWorkspaceFolder(type);
File[] files = downloaded.listFiles();
for (File i : downloaded.listFiles()) {
if (i.isFile()) {
try {
FileUtils.copyFile(i, new File(folder, i.getName()));
} catch (IOException e) {
e.printStackTrace();
Iris.reportError(e);
}
} else {
try {
FileUtils.copyDirectory(i, new File(folder, i.getName()));
} catch (IOException e) {
e.printStackTrace();
Iris.reportError(e);
if (files != null) {
for (File i : files) {
if (i.isFile()) {
try {
FileUtils.copyFile(i, new File(folder, i.getName()));
} catch (IOException e) {
e.printStackTrace();
Iris.reportError(e);
}
} else {
try {
FileUtils.copyDirectory(i, new File(folder, i.getName()));
} catch (IOException e) {
e.printStackTrace();
Iris.reportError(e);
}
}
}
}
IO.delete(downloaded);
IO.delete(downloaded);
}
}
if (!dimensionFile.exists() || !dimensionFile.isFile()) {
@@ -198,26 +206,24 @@ public class StudioSVC implements IrisService {
}
public void downloadSearch(VolmitSender sender, String key, boolean trim, boolean forceOverwrite) {
String url = "?";
try {
url = getListing(false).get(key);
String url = getListing(false).get(key);
if (url == null) {
Iris.warn("ITS ULL for " + key);
sender.sendMessage("Pack '" + key + "' was not found in the pack listing.");
sender.sendMessage("Use /iris download <user/repo> <branch> to download manually.");
return;
}
url = url == null ? key : url;
Iris.info("Assuming URL " + url);
String branch = "master";
Iris.info("Resolved pack '" + key + "' to " + url);
String[] nodes = url.split("\\Q/\\E");
String repo = nodes.length == 1 ? "IrisDimensions/" + nodes[0] : nodes[0] + "/" + nodes[1];
branch = nodes.length > 2 ? nodes[2] : branch;
String branch = nodes.length > 2 ? nodes[2] : "stable";
download(sender, repo, branch, trim, forceOverwrite, false);
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
sender.sendMessage("Failed to download '" + key + "' from " + url + ".");
sender.sendMessage("Failed to download '" + key + "'.");
}
}
@@ -246,7 +252,7 @@ public class StudioSVC implements IrisService {
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 IrisDimensions/overworld branch=master");
sender.sendMessage("For example: /iris download IrisDimensions/overworld branch=stable");
return;
}
sender.sendMessage("Unpacking " + repo);
@@ -355,9 +361,6 @@ public class StudioSVC implements IrisService {
l.put(i, a.getString(i));
}
// TEMP FIX
l.put("IrisDimensions/overworld/master", "IrisDimensions/overworld/stable");
l.put("overworld", "IrisDimensions/overworld/stable");
return l;
}
@@ -379,7 +382,23 @@ public class StudioSVC implements IrisService {
}
}
private static boolean blockIfPackBroken(VolmitSender sender, String dimm) {
PackValidationResult validation = PackValidationRegistry.get(dimm);
if (validation == null || validation.isLoadable()) {
return false;
}
sender.sendMessage("Cannot open studio '" + dimm + "' - pack has blocking errors:");
for (String reason : validation.getBlockingErrors()) {
sender.sendMessage(" - " + reason);
}
sender.sendMessage("Fix the pack and run /iris pack validate " + dimm + " to revalidate.");
return true;
}
public void open(VolmitSender sender, long seed, String dimm, Consumer<World> onDone) throws IrisException {
if (blockIfPackBroken(sender, dimm)) {
return;
}
CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> pendingClose = close();
pendingClose.whenComplete((closeResult, closeThrowable) -> {
if (closeThrowable != null) {
@@ -572,16 +591,18 @@ public class StudioSVC implements IrisService {
public void create(VolmitSender sender, String s, String downloadable) {
boolean shouldDelete = false;
File importPack = getWorkspaceFolder(downloadable);
File[] packFiles = importPack.listFiles();
if (importPack.listFiles().length == 0) {
if (packFiles == null || packFiles.length == 0) {
downloadSearch(sender, downloadable, false);
packFiles = importPack.listFiles();
if (importPack.listFiles().length > 0) {
if (packFiles != null && packFiles.length > 0) {
shouldDelete = true;
}
}
if (importPack.listFiles().length == 0) {
if (packFiles == null || packFiles.length == 0) {
sender.sendMessage("Couldn't find the pack to create a new dimension from.");
return;
}
@@ -336,12 +336,22 @@ public class IrisCreator {
return;
}
int percent = (int) Math.round(progress * 100.0D);
int remaining = required - generated;
if (sender.isPlayer()) {
sender.sendProgress(progress, "Generating");
int barWidth = 44;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, progress)) * barWidth);
StringBuilder bar = new StringBuilder(barWidth * 3 + 4);
bar.append(C.DARK_GRAY).append("[");
for (int bi = 0; bi < barWidth; bi++) {
bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|");
}
bar.append(C.DARK_GRAY).append("]");
sender.sendAction(bar.toString() + C.GRAY + " " + C.YELLOW + percent + "%" + C.DARK_GRAY + " " + Form.f(generated) + "/" + Form.f(required) + " chunks");
return;
}
sender.sendMessage(C.WHITE + "Generating " + Form.pc(progress) + ((C.GRAY + " (" + (required - generated) + " Left)")));
sender.sendMessage(C.GOLD + "Generating " + C.YELLOW + percent + "%" + C.GRAY + " " + Form.f(generated) + "/" + Form.f(required) + " chunks" + C.DARK_GRAY + " (" + remaining + " left)");
}, interval));
});
return taskId;
@@ -356,12 +366,22 @@ public class IrisCreator {
return;
}
double p = progress.get();
int percent = (int) Math.round(p * 100.0D);
if (sender.isPlayer()) {
sender.sendProgress(progress.get(), "Pregenerating");
int barWidth = 44;
int filled = (int) Math.round(Math.max(0.0D, Math.min(1.0D, p)) * barWidth);
StringBuilder bar = new StringBuilder(barWidth * 3 + 4);
bar.append(C.DARK_GRAY).append("[");
for (int bi = 0; bi < barWidth; bi++) {
bar.append(bi < filled ? C.GREEN : C.DARK_GRAY).append("|");
}
bar.append(C.DARK_GRAY).append("]");
sender.sendAction(bar.toString() + C.GRAY + " " + C.YELLOW + percent + "%" + C.GRAY + " | " + C.WHITE + "Pregenerating");
return;
}
sender.sendMessage(C.WHITE + "Pregenerating " + Form.pc(progress.get()));
sender.sendMessage(C.GOLD + "Pregenerating " + C.YELLOW + percent + "%");
}, interval));
return taskId;
}
@@ -19,9 +19,11 @@
package art.arcane.iris.engine.decorator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.util.common.data.B;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.math.RNG;
@@ -40,9 +42,13 @@ public class IrisCeilingDecorator extends IrisEngineDecorator {
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1, Hunk<BlockData> data, IrisBiome biome, int height, int max) {
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = getDecorator(rng, biome, realX, realZ);
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
if (decorator != null) {
if (!decorator.isStacking()) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, fixFaces(decorator.getBlockData100(biome, rng, realX, height, realZ, getData()), data, x, z, realX, height, realZ));
} else {
int stack = decorator.getHeight(rng, realX, realZ, getData());
@@ -53,6 +59,9 @@ public class IrisCeilingDecorator extends IrisEngineDecorator {
}
if (stack == 1) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return;
}
@@ -92,6 +101,9 @@ public class IrisCeilingDecorator extends IrisEngineDecorator {
((PointedDripstone) bd).setVerticalDirection(BlockFace.DOWN);
}
if (caveSkipFluid && B.isFluid(data.get(x, h, z))) {
break;
}
data.set(x, h, z, bd);
}
}
@@ -51,6 +51,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
IrisDecorator decorator = getDecorator(rng, biome, realX, realZ);
bdx = data.get(x, height, z);
boolean underwater = height < getDimension().getFluidHeight() && biome.getInferredType() != InferredType.CAVE;
boolean caveSkipFluid = biome.getInferredType() == InferredType.CAVE;
if (decorator != null) {
if (!decorator.isForcePlace() && !decorator.getSlopeCondition().isDefault()
@@ -68,6 +69,9 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
}
if (decorator.getForceBlock() != null) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, fixFaces(decorator.getForceBlock().getBlockData(getData()), data, x, z, realX, height, realZ));
} else if (!decorator.isForcePlace()) {
if (decorator.getWhitelist() != null && decorator.getWhitelist().stream().noneMatch(d -> d.getBlockData(getData()).equals(bdx))) {
@@ -82,7 +86,9 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
bd = bd.clone();
((Bisected) bd).setHalf(Bisected.Half.TOP);
try {
data.set(x, height + 2, z, bd);
if (!caveSkipFluid || !B.isFluid(data.get(x, height + 2, z))) {
data.set(x, height + 2, z, bd);
}
} catch (Throwable e) {
Iris.reportError(e);
}
@@ -107,6 +113,9 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
}
if (stack == 1) {
if (caveSkipFluid && B.isFluid(data.get(x, height, z))) {
return;
}
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return;
}
@@ -130,6 +139,10 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
break;
}
if (caveSkipFluid && B.isFluid(data.get(x, height + 1 + i, z))) {
break;
}
if (bd instanceof PointedDripstone) {
PointedDripstone.Thickness th = PointedDripstone.Thickness.BASE;
@@ -960,50 +960,11 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
}
default void gotoBiome(IrisBiome biome, Player player, boolean teleport) {
Set<String> regionKeys = getDimension()
.getAllRegions(this).stream()
.filter((i) -> i.getAllBiomeIds().contains(biome.getLoadKey()))
.map(IrisRegistrant::getLoadKey)
.collect(Collectors.toSet());
Locator<IrisBiome> lb = Locator.surfaceBiome(biome.getLoadKey());
Locator<IrisBiome> locator = (engine, chunk)
-> regionKeys.contains(getRegion((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())
&& lb.matches(engine, chunk);
if (!regionKeys.isEmpty()) {
locator.find(player, teleport, "Biome " + biome.getName());
} else {
player.sendMessage(C.RED + biome.getName() + " is not in any defined regions!");
}
Locator.surfaceBiome(biome.getLoadKey()).find(player, teleport, "Biome " + biome.getName());
}
default void gotoObject(String s, Player player, boolean teleport) {
Set<String> biomeKeys = getDimension().getAllBiomes(this).stream()
.filter((i) -> i.getObjects().stream().anyMatch((f) -> f.getPlace().contains(s)))
.map(IrisRegistrant::getLoadKey)
.collect(Collectors.toSet());
Set<String> regionKeys = getDimension().getAllRegions(this).stream()
.filter((i) -> i.getAllBiomeIds().stream().anyMatch(biomeKeys::contains)
|| i.getObjects().stream().anyMatch((f) -> f.getPlace().contains(s)))
.map(IrisRegistrant::getLoadKey)
.collect(Collectors.toSet());
Locator<IrisObject> sl = Locator.object(s);
Locator<IrisBiome> locator = (engine, chunk) -> {
if (biomeKeys.contains(getSurfaceBiome((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())) {
return sl.matches(engine, chunk);
} else if (regionKeys.contains(getRegion((chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8).getLoadKey())) {
return sl.matches(engine, chunk);
}
return false;
};
if (!regionKeys.isEmpty()) {
locator.find(player, teleport, "Object " + s);
} else {
player.sendMessage(C.RED + s + " is not in any defined regions or biomes!");
}
Locator.object(s).find(player, teleport, "Object " + s);
}
default boolean hasObjectPlacement(String objectKey) {
@@ -114,9 +114,14 @@ public interface Locator<T> {
boolean matches(Engine engine, Position2 chunk);
default void find(Player player, boolean teleport, String message) {
find(player, location -> {
find(player, 120_000, location -> {
if (location == null) {
player.sendMessage(C.RED + "Could not find " + message + " within search range.");
return;
}
if (teleport) {
J.runEntity(player, () -> teleportAsyncSafely(player, location));
player.sendMessage(C.GREEN + "Teleporting to " + message + "...");
} else {
player.sendMessage(C.GREEN + message + " at: " + location.getBlockX() + " " + location.getBlockY() + " " + location.getBlockZ());
}
@@ -124,7 +129,7 @@ public interface Locator<T> {
}
default void find(Player player, Consumer<Location> consumer) {
find(player, 30_000, consumer);
find(player, 120_000, consumer);
}
default void find(Player player, long timeout, Consumer<Location> consumer) {
@@ -137,11 +142,13 @@ public interface Locator<T> {
Position2 at = find(engine, new Position2(player.getLocation().getBlockX() >> 4, player.getLocation().getBlockZ() >> 4), timeout, checks::set).get();
if (at != null) {
consumer.accept(new Location(world, (at.getX() << 4) + 8,
engine.getHeight(
(at.getX() << 4) + 8,
(at.getZ() << 4) + 8, false),
(at.getZ() << 4) + 8));
int bx = (at.getX() << 4) + 8;
int bz = (at.getZ() << 4) + 8;
consumer.accept(new Location(world, bx,
world.getHighestBlockYAt(bx, bz) + 2,
bz));
} else {
consumer.accept(null);
}
} catch (WrongEngineBroException | InterruptedException | ExecutionException e) {
e.printStackTrace();
@@ -172,18 +179,17 @@ public interface Locator<T> {
cancelSearch();
return MultiBurst.burst.completeValue(() -> {
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 17;
int tc = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) * 32;
MultiBurst burst = MultiBurst.burst;
AtomicBoolean found = new AtomicBoolean(false);
Position2 cursor = pos;
AtomicInteger searched = new AtomicInteger();
AtomicBoolean stop = new AtomicBoolean(false);
AtomicReference<Position2> foundPos = new AtomicReference<>();
PrecisionStopwatch px = PrecisionStopwatch.start();
LocatorCanceller.cancel = () -> stop.set(true);
AtomicReference<Position2> next = new AtomicReference<>(cursor);
AtomicReference<Position2> next = new AtomicReference<>(pos);
Spiraler s = new Spiraler(100000, 100000, (x, z) -> next.set(new Position2(x, z)));
s.setOffset(cursor.getX(), cursor.getZ());
s.setOffset(pos.getX(), pos.getZ());
s.next();
while (!found.get() && !stop.get() && px.getMilliseconds() < timeout) {
BurstExecutor e = burst.burst(tc);
@@ -192,11 +198,11 @@ public interface Locator<T> {
Position2 p = next.get();
s.next();
e.queue(() -> {
if (found.get()) {
return;
}
if (matches(engine, p)) {
if (foundPos.get() == null) {
foundPos.set(p);
}
foundPos.compareAndSet(null, p);
found.set(true);
}
searched.incrementAndGet();
@@ -42,10 +42,8 @@ public class IrisCaveCarver3D {
private static final byte LIQUID_LAVA = 2;
private static final byte LIQUID_FORCED_AIR = 3;
private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 32;
private static final int ADAPTIVE_DEEP_MIN_PLANE_COLUMNS = 64;
private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8;
private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12;
private static final int ADAPTIVE_DEEP_NEAR_SURFACE_DIVISOR = 4;
private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.25D;
private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D;
private static final ThreadLocal<Scratch> SCRATCH = ThreadLocal.withInitial(Scratch::new);
@@ -232,8 +230,6 @@ public class IrisCaveCarver3D {
}
}
int minCarveCells = Math.max(0, profile.getMinCarveCells());
double recoveryThresholdBoost = Math.max(0, profile.getRecoveryThresholdBoost());
int carved;
if (exactSampling) {
if (adaptiveSampling) {
@@ -258,29 +254,6 @@ public class IrisCaveCarver3D {
0D,
false
);
if (carved < minCarveCells && recoveryThresholdBoost > 0D) {
carved += carvePassAdaptive(
chunk,
x0,
z0,
minY,
maxY,
adaptiveSampleStep,
adaptiveThresholdMargin,
surfaceBreakThresholdBoost,
columnMaxY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
clampedWeights,
verticalEdgeFade,
matterByY,
resolvedMinWeight,
resolvedThresholdPenalty,
recoveryThresholdBoost,
true
);
}
} else {
carved = carvePassExact(
chunk,
@@ -301,27 +274,6 @@ public class IrisCaveCarver3D {
0D,
false
);
if (carved < minCarveCells && recoveryThresholdBoost > 0D) {
carved += carvePassExact(
chunk,
x0,
z0,
minY,
maxY,
surfaceBreakThresholdBoost,
columnMaxY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
clampedWeights,
verticalEdgeFade,
matterByY,
resolvedMinWeight,
resolvedThresholdPenalty,
recoveryThresholdBoost,
true
);
}
}
} else {
int latticeStep = sampleStep;
@@ -345,28 +297,6 @@ public class IrisCaveCarver3D {
0D,
false
);
if (carved < minCarveCells && recoveryThresholdBoost > 0D) {
carved += carvePassLattice(
chunk,
x0,
z0,
minY,
maxY,
latticeStep,
surfaceBreakThresholdBoost,
columnMaxY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
clampedWeights,
verticalEdgeFade,
matterByY,
resolvedMinWeight,
resolvedThresholdPenalty,
recoveryThresholdBoost,
true
);
}
if (carved == 0 && hasFallbackCandidates(columnMaxY, clampedWeights, minY, resolvedMinWeight)) {
carved += carvePassFallback(
chunk,
@@ -388,28 +318,6 @@ public class IrisCaveCarver3D {
0D,
false
);
if (carved < minCarveCells && recoveryThresholdBoost > 0D) {
carved += carvePassFallback(
chunk,
x0,
z0,
minY,
maxY,
sampleStep,
surfaceBreakThresholdBoost,
columnMaxY,
surfaceBreakFloorY,
surfaceBreakColumn,
columnThreshold,
clampedWeights,
verticalEdgeFade,
matterByY,
resolvedMinWeight,
resolvedThresholdPenalty,
recoveryThresholdBoost,
true
);
}
}
}
@@ -614,14 +522,7 @@ public class IrisCaveCarver3D {
continue;
}
int effectiveAdaptiveSampleStep = resolveAdaptivePlaneSampleStep(
y,
planeColumnIndices,
planeCount,
adaptiveSampleStep,
surfaceBreakColumn,
surfaceBreakFloorY
);
int effectiveAdaptiveSampleStep = resolveAdaptivePlaneSampleStep(y, adaptiveSampleStep);
double effectiveAdaptiveThresholdMargin = resolveAdaptivePlaneThresholdMargin(
adaptiveThresholdMargin,
adaptiveSampleStep,
@@ -678,31 +579,14 @@ public class IrisCaveCarver3D {
return carved;
}
private int resolveAdaptivePlaneSampleStep(
int y,
int[] planeColumnIndices,
int planeCount,
int adaptiveSampleStep,
boolean[] surfaceBreakColumn,
int[] surfaceBreakFloorY
) {
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP || planeCount < ADAPTIVE_DEEP_MIN_PLANE_COLUMNS) {
private int resolveAdaptivePlaneSampleStep(int y, int adaptiveSampleStep) {
if (adaptiveSampleStep >= ADAPTIVE_DEEP_SAMPLE_STEP) {
return adaptiveSampleStep;
}
int nearSurfaceColumns = 0;
int allowedNearSurfaceColumns = Math.max(8, planeCount / ADAPTIVE_DEEP_NEAR_SURFACE_DIVISOR);
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
int columnIndex = planeColumnIndices[planeIndex];
if (surfaceBreakColumn[columnIndex] || y > (surfaceBreakFloorY[columnIndex] - ADAPTIVE_DEEP_SURFACE_MARGIN)) {
nearSurfaceColumns++;
if (nearSurfaceColumns > allowedNearSurfaceColumns) {
return adaptiveSampleStep;
}
}
}
return ADAPTIVE_DEEP_SAMPLE_STEP;
int profileMaxY = (int) Math.ceil(profile.getVerticalRange().getMax());
int fineBandFloorY = profileMaxY - profile.getSurfaceBreakDepth() - ADAPTIVE_DEEP_SURFACE_MARGIN;
return y >= fineBandFloorY ? adaptiveSampleStep : ADAPTIVE_DEEP_SAMPLE_STEP;
}
private double resolveAdaptivePlaneThresholdMargin(
@@ -140,37 +140,28 @@ public class MantleCarvingComponent extends IrisMantleComponent {
continue;
}
IrisCaveProfile dominantProfile = null;
double dominantKernelWeight = Double.NEGATIVE_INFINITY;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
for (int profileIndex = 0; profileIndex < profileCount; profileIndex++) {
IrisCaveProfile profile = kernelProfiles[profileIndex];
double kernelWeight = kernelProfileWeights[profileIndex];
if (kernelWeight > dominantKernelWeight) {
dominantProfile = profile;
dominantKernelWeight = kernelWeight;
} else if (kernelWeight == dominantKernelWeight
&& profileSortKey(profile) < profileSortKey(dominantProfile)) {
dominantProfile = profile;
}
kernelProfiles[profileIndex] = null;
kernelProfileWeights[profileIndex] = 0D;
}
if (dominantProfile == null) {
continue;
}
double columnWeight = clampWeight(kernelWeight / totalKernelWeight);
if (columnWeight < MIN_WEIGHT) {
continue;
}
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
double dominantWeight = clampWeight(dominantKernelWeight / totalKernelWeight);
double[] weights = columnProfileWeights.get(dominantProfile);
if (weights == null) {
weights = new double[CHUNK_AREA];
columnProfileWeights.put(dominantProfile, weights);
} else if (!activeProfiles.containsKey(dominantProfile)) {
Arrays.fill(weights, 0D);
double[] weights = columnProfileWeights.get(profile);
if (weights == null) {
weights = new double[CHUNK_AREA];
columnProfileWeights.put(profile, weights);
} else if (!activeProfiles.containsKey(profile)) {
Arrays.fill(weights, 0D);
}
activeProfiles.put(profile, Boolean.TRUE);
weights[columnIndex] = columnWeight;
}
activeProfiles.put(dominantProfile, Boolean.TRUE);
weights[columnIndex] = dominantWeight;
}
}
@@ -440,7 +431,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
continue;
}
if (cachedChunkHeights != null) {
surfaceHeights[columnIndex] = (int) Math.round(cachedChunkHeights[columnIndex]);
surfaceHeights[columnIndex] = (int) Math.round(cachedChunkHeights[(localZ << 4) + localX]);
continue;
}
surfaceHeights[columnIndex] = getEngineMantle().getEngine().getHeight(worldX, worldZ);
@@ -37,10 +37,8 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.noise.NoiseType;
@@ -373,19 +371,46 @@ public class MantleObjectComponent extends IrisMantleComponent {
int zz = rng.i(z, z + 15);
int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v);
int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v);
if (surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius)) {
rejected++;
continue;
}
boolean overCave = surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius);
int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v);
try {
int result = v.place(xx, -1, zz, writer, effectivePlacement, rng, (b, data) -> {
writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id);
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
int result = -1;
String fallbackPath = "surface";
if (overCave) {
int caveFloorY = findNearestCaveFloor(writer, xx, zz);
if (caveFloorY > 0) {
IrisObjectPlacement floorPlacement = effectivePlacement.toPlacement(v.getLoadKey());
floorPlacement.setMode(ObjectPlaceMode.FAST_MIN_HEIGHT);
result = v.place(xx, caveFloorY, zz, writer, floorPlacement, rng, (b, data) -> {
writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id);
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
}, null, getData());
fallbackPath = "cave-floor";
}
}, null, getData());
if (result < 0) {
IrisObjectPlacement stiltPlacement = effectivePlacement.toPlacement(v.getLoadKey());
stiltPlacement.setMode(ObjectPlaceMode.FAST_MIN_STILT);
result = v.place(xx, -1, zz, writer, stiltPlacement, rng, (b, data) -> {
writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id);
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
}, null, getData());
fallbackPath = "stilt";
}
} else {
result = v.place(xx, -1, zz, writer, effectivePlacement, rng, (b, data) -> {
writer.setData(b.getX(), b.getY(), b.getZ(), v.getLoadKey() + "@" + id);
if (effectivePlacement.isDolphinTarget() && effectivePlacement.isUnderwater() && B.isStorageChest(data)) {
writer.setData(b.getX(), b.getY(), b.getZ(), MatterStructurePOI.BURIED_TREASURE);
}
}, null, getData());
}
if (result >= 0) {
placed++;
@@ -400,6 +425,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " resultY=" + result
+ " px=" + xx
+ " pz=" + zz
+ " overCave=" + overCave
+ " fallback=" + fallbackPath
+ " densityIndex=" + i
+ " density=" + density);
}
@@ -729,6 +756,14 @@ public class MantleObjectComponent extends IrisMantleComponent {
return null;
}
private int findNearestCaveFloor(MantleWriter writer, int x, int z) {
KList<Integer> anchors = scanCaveAnchorColumn(writer, IrisCaveAnchorMode.FLOOR, 1, 0, x, z);
if (anchors.isEmpty()) {
return -1;
}
return anchors.get(anchors.size() - 1);
}
private int findCaveAnchorY(MantleWriter writer, RNG rng, int x, int z, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, KMap<Long, KList<Integer>> anchorCache) {
long key = Cache.key(x, z);
KList<Integer> anchors = anchorCache.computeIfAbsent(key, (k) -> scanCaveAnchorColumn(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z));
@@ -744,55 +779,58 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
private KList<Integer> scanCaveAnchorColumn(MantleWriter writer, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, int x, int z) {
KList<Integer> anchors = new KList<>();
int height = getEngineMantle().getEngine().getHeight();
int step = Math.max(1, anchorScanStep);
int surfaceY = getEngineMantle().getEngine().getHeight(x, z);
int maxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface));
if (maxAnchorY <= 1) {
logCaveAnchorDiag(writer, x, z, surfaceY, maxAnchorY, height, objectMinDepthBelowSurface, 0, 0);
int baseMaxAnchorY = Math.min(height - 1, surfaceY - Math.max(0, objectMinDepthBelowSurface));
if (baseMaxAnchorY <= 1) {
return new KList<>();
}
KList<Integer> anchors = scanCaveAnchorRange(writer, anchorMode, step, x, z, height, baseMaxAnchorY);
if (!anchors.isEmpty()) {
return anchors;
}
int carvedCount = 0;
for (int y = 1; y < maxAnchorY; y += step) {
if (!writer.isCarved(x, y, z)) {
continue;
int widenedMaxAnchorY = Math.min(height - 1, surfaceY - 3);
widenedMaxAnchorY = Math.min(widenedMaxAnchorY, baseMaxAnchorY + Math.max(0, objectMinDepthBelowSurface) / 2);
if (widenedMaxAnchorY > baseMaxAnchorY) {
anchors = scanCaveAnchorRange(writer, anchorMode, step, x, z, height, widenedMaxAnchorY);
if (!anchors.isEmpty()) {
return anchors;
}
carvedCount++;
boolean solidBelow = y <= 0 || !writer.isCarved(x, y - 1, z);
boolean solidAbove = y >= (height - 1) || !writer.isCarved(x, y + 1, z);
if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) {
anchors.add(y);
}
}
if (anchors.isEmpty()) {
logCaveAnchorDiag(writer, x, z, surfaceY, maxAnchorY, height, objectMinDepthBelowSurface, carvedCount, 0);
}
return anchors;
}
private void logCaveAnchorDiag(MantleWriter writer, int x, int z, int surfaceY, int maxAnchorY, int height, int minDepth, int carvedCount, int anchorCount) {
long now = System.currentTimeMillis();
CaveRejectLogState state = CAVE_REJECT_LOG_STATE.computeIfAbsent("anchor-diag-" + (x >> 4) + "," + (z >> 4), k -> new CaveRejectLogState());
if (now - state.lastLogMs.get() < CAVE_REJECT_LOG_THROTTLE_MS) {
return;
private KList<Integer> scanCaveAnchorRange(MantleWriter writer, IrisCaveAnchorMode anchorMode, int step, int x, int z, int height, int maxAnchorY) {
KList<Integer> anchors = new KList<>();
for (int y = 1; y < maxAnchorY; y += step) {
if (!writer.isCarved(x, y, z)) {
continue;
}
boolean solidBelow = hasSolidNeighbor(writer, x, y, z, height, -1);
boolean solidAbove = hasSolidNeighbor(writer, x, y, z, height, 1);
if (matchesCaveAnchor(anchorMode, solidBelow, solidAbove)) {
anchors.add(y);
}
}
state.lastLogMs.set(now);
MantleChunk<Matter> chunk = writer.acquireChunk(x >> 4, z >> 4);
Iris.info("Cave anchor diag: block=" + x + "," + z
+ " chunk=" + (x >> 4) + "," + (z >> 4)
+ " surfaceY=" + surfaceY
+ " maxAnchorY=" + maxAnchorY
+ " worldHeight=" + height
+ " minDepth=" + minDepth
+ " carvedInColumn=" + carvedCount
+ " anchorsFound=" + anchorCount
+ " chunkRef=" + (chunk == null ? "null" : System.identityHashCode(chunk))
+ " writerRef=" + System.identityHashCode(writer));
return anchors;
}
private boolean hasSolidNeighbor(MantleWriter writer, int x, int y, int z, int height, int direction) {
for (int d = 1; d <= 3; d++) {
int ny = y + (direction * d);
if (ny < 0 || ny >= height) {
return true;
}
if (!writer.isCarved(x, ny, z)) {
return true;
}
}
return false;
}
private boolean matchesCaveAnchor(IrisCaveAnchorMode anchorMode, boolean solidBelow, boolean solidAbove) {
@@ -52,6 +52,9 @@ import java.util.Map;
public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
private static final ThreadLocal<CarveScratch> SCRATCH = ThreadLocal.withInitial(CarveScratch::new);
private static final int CAVE_BIOME_BLEND_RADIUS = 3;
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
private final RNG rng;
private final BlockData AIR = Material.CAVE_AIR.createBlockData();
private final BlockData LAVA = Material.LAVA.createBlockData();
@@ -75,6 +78,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
scratch.reset();
PackedWallBuffer walls = scratch.walls;
ColumnMask[] columnMasks = scratch.columnMasks;
ColumnMask[] boundaryMasks = scratch.boundaryMasks;
MatterCavern[] boundaryCaverns = scratch.boundaryCaverns;
int[] surfaceHeights = scratch.surfaceHeights;
Map<String, IrisBiome> customBiomeCache = scratch.customBiomeCache;
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
@@ -137,6 +142,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
output.setRaw(rx, yy, rz, AIR);
}
});
addCrossChunkBoundaryWalls(mantle, mc, walls, boundaryMasks, boundaryCaverns, x, z, surfaceHeights);
getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
@@ -154,7 +160,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
BlockData data = biome.getWall().get(rng, worldX, yy, worldZ, getData());
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy <= surfaceHeights[columnIndex]) {
if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy < surfaceHeights[columnIndex]) {
output.setRaw(rx, yy, rz, data);
}
}
@@ -163,6 +169,17 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
processColumnFromMask(output, mc, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache);
}
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
if (boundaryMasks[columnIndex].isEmpty() || !columnMasks[columnIndex].isEmpty()) {
continue;
}
MatterCavern cavern = boundaryCaverns[columnIndex];
if (cavern == null) {
continue;
}
processBoundaryColumnFromMask(output, boundaryMasks[columnIndex], cavern, columnIndex, x, z, resolverState, caveBiomeCache, customBiomeCache);
}
} finally {
getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds());
}
@@ -172,6 +189,86 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
}
}
private void addCrossChunkBoundaryWalls(
Mantle<Matter> mantle,
MantleChunk<Matter> mc,
PackedWallBuffer walls,
ColumnMask[] boundaryMasks,
MatterCavern[] boundaryCaverns,
int chunkX,
int chunkZ,
int[] surfaceHeights
) {
int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
int maxSurfaceY = 0;
for (int index = 0; index < surfaceHeights.length; index++) {
if (surfaceHeights[index] > maxSurfaceY) {
maxSurfaceY = surfaceHeights[index];
}
}
int maxY = Math.min(getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight() - 1, maxSurfaceY + 1);
if (maxY < 1) {
return;
}
boolean westLoaded = mantle.hasTectonicPlate(((chunkX - 1) >> 5), (chunkZ >> 5));
boolean eastLoaded = mantle.hasTectonicPlate(((chunkX + 1) >> 5), (chunkZ >> 5));
boolean northLoaded = mantle.hasTectonicPlate((chunkX >> 5), ((chunkZ - 1) >> 5));
boolean southLoaded = mantle.hasTectonicPlate((chunkX >> 5), ((chunkZ + 1) >> 5));
if (!westLoaded && !eastLoaded && !northLoaded && !southLoaded) {
return;
}
for (int yy = 1; yy <= maxY; yy++) {
for (int offset = 0; offset < 16; offset++) {
if (westLoaded) {
tryAddBoundaryWall(mantle, mc, walls, boundaryMasks, boundaryCaverns, 0, yy, offset, baseX, baseZ, -1, 0);
}
if (eastLoaded) {
tryAddBoundaryWall(mantle, mc, walls, boundaryMasks, boundaryCaverns, 15, yy, offset, baseX, baseZ, 1, 0);
}
if (northLoaded) {
tryAddBoundaryWall(mantle, mc, walls, boundaryMasks, boundaryCaverns, offset, yy, 0, baseX, baseZ, 0, -1);
}
if (southLoaded) {
tryAddBoundaryWall(mantle, mc, walls, boundaryMasks, boundaryCaverns, offset, yy, 15, baseX, baseZ, 0, 1);
}
}
}
}
private void tryAddBoundaryWall(
Mantle<Matter> mantle,
MantleChunk<Matter> mc,
PackedWallBuffer walls,
ColumnMask[] boundaryMasks,
MatterCavern[] boundaryCaverns,
int localX,
int yy,
int localZ,
int baseX,
int baseZ,
int dx,
int dz
) {
int worldX = baseX + localX;
int worldZ = baseZ + localZ;
if (mc.get(worldX, yy, worldZ, MatterCavern.class) != null) {
return;
}
MatterCavern neighbor = mantle.get(worldX + dx, yy, worldZ + dz, MatterCavern.class);
if (neighbor == null) {
return;
}
walls.put(localX, yy, localZ, neighbor);
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
boundaryMasks[columnIndex].add(yy);
if (boundaryCaverns[columnIndex] == null) {
boundaryCaverns[columnIndex] = neighbor;
}
}
private void processColumnFromMask(
Hunk<BlockData> output,
MantleChunk<Matter> mc,
@@ -226,6 +323,117 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
}
}
private void processBoundaryColumnFromMask(
Hunk<BlockData> output,
ColumnMask boundaryMask,
MatterCavern cavern,
int columnIndex,
int chunkX,
int chunkZ,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache
) {
int firstHeight = boundaryMask.nextSetBit(0);
if (firstHeight < 0) {
return;
}
int rx = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int rz = columnIndex & 15;
int worldX = rx + PowerOfTwoCoordinates.chunkToBlock(chunkX);
int worldZ = rz + PowerOfTwoCoordinates.chunkToBlock(chunkZ);
int zoneFloor = firstHeight;
int zoneCeiling = firstHeight;
int y = boundaryMask.nextSetBit(firstHeight + 1);
while (y >= 0) {
if (y == zoneCeiling + 1) {
zoneCeiling = y;
} else {
paintBoundaryZone(output, cavern, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
zoneFloor = y;
zoneCeiling = y;
}
y = boundaryMask.nextSetBit(y + 1);
}
paintBoundaryZone(output, cavern, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling, resolverState, caveBiomeCache, customBiomeCache);
}
private void paintBoundaryZone(
Hunk<BlockData> output,
MatterCavern cavern,
int rx,
int rz,
int worldX,
int worldZ,
int zoneFloor,
int zoneCeiling,
IrisDimensionCarvingResolver.State resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
Map<String, IrisBiome> customBiomeCache
) {
int center = (zoneFloor + zoneCeiling) / 2;
String customBiome = cavern.getCustomBiome();
IrisBiome biome = customBiome.isEmpty()
? resolveCaveBiome(caveBiomeCache, worldX, center, worldZ, resolverState)
: resolveCustomBiome(customBiomeCache, customBiome);
if (biome == null) {
return;
}
biome.setInferredType(InferredType.CAVE);
KList<BlockData> floorLayers = biome.generateLayers(getDimension(), worldX, worldZ, rng, 3, zoneFloor, getData(), getComplex());
for (int i = 0; i < zoneFloor - 1; i++) {
if (!floorLayers.hasIndex(i)) {
break;
}
int fy = zoneFloor - i - 1;
if (fy < 0) {
break;
}
BlockData down = output.getRaw(rx, fy, rz);
if (!B.isSolid(down)) {
break;
}
BlockData layer = floorLayers.get(i);
if (B.isOre(down)) {
output.setRaw(rx, fy, rz, B.toDeepSlateOre(down, layer));
continue;
}
output.setRaw(rx, fy, rz, layer);
}
int worldMaxY = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
KList<BlockData> ceilingLayers = biome.generateCeilingLayers(getDimension(), worldX, worldZ, rng, 3, zoneCeiling, getData(), getComplex());
for (int i = 0; i < ceilingLayers.size(); i++) {
int cy = zoneCeiling + i + 1;
if (cy >= worldMaxY) {
break;
}
BlockData up = output.getRaw(rx, cy, rz);
if (!B.isSolid(up)) {
continue;
}
BlockData layer = ceilingLayers.get(i);
if (B.isOre(up)) {
output.setRaw(rx, cy, rz, B.toDeepSlateOre(up, layer));
continue;
}
output.setRaw(rx, cy, rz, layer);
}
}
private void processZone(Hunk<BlockData> output, MantleChunk<Matter> mc, Mantle<Matter> mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
int center = (zone.floor + zone.ceiling) / 2;
String customBiome = "";
@@ -323,6 +531,38 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
}
private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
IrisBiome center = sampleCaveBiome(caveBiomeCache, x, y, z, resolverState);
if (center == null) {
return null;
}
IrisBiome xPos = sampleCaveBiome(caveBiomeCache, x + CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
IrisBiome xNeg = sampleCaveBiome(caveBiomeCache, x - CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
IrisBiome zPos = sampleCaveBiome(caveBiomeCache, x, y, z + CAVE_BIOME_BLEND_RADIUS, resolverState);
IrisBiome zNeg = sampleCaveBiome(caveBiomeCache, x, y, z - CAVE_BIOME_BLEND_RADIUS, resolverState);
if (xPos == center && xNeg == center && zPos == center && zNeg == center) {
return center;
}
int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
return center;
}
roll -= CAVE_BIOME_BLEND_CENTER_WEIGHT;
if (roll == 0) {
return xPos != null ? xPos : center;
}
if (roll == 1) {
return xNeg != null ? xNeg : center;
}
if (roll == 2) {
return zPos != null ? zPos : center;
}
return zNeg != null ? zNeg : center;
}
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
long key = BlockPosition.toLong(x, y, z);
IrisBiome cachedBiome = caveBiomeCache.get(key);
if (cachedBiome != null) {
@@ -478,6 +718,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
private static final class CarveScratch {
private final ColumnMask[] columnMasks = new ColumnMask[256];
private final ColumnMask[] boundaryMasks = new ColumnMask[256];
private final MatterCavern[] boundaryCaverns = new MatterCavern[256];
private final int[] surfaceHeights = new int[256];
private final PackedWallBuffer walls = new PackedWallBuffer(512);
private final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
@@ -485,12 +727,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
private CarveScratch() {
for (int index = 0; index < columnMasks.length; index++) {
columnMasks[index] = new ColumnMask();
boundaryMasks[index] = new ColumnMask();
}
}
private void reset() {
for (int index = 0; index < columnMasks.length; index++) {
columnMasks[index].clear();
boundaryMasks[index].clear();
boundaryCaverns[index] = null;
}
walls.clear();
customBiomeCache.clear();
@@ -29,37 +29,37 @@ import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.annotations.*;
import art.arcane.iris.engine.object.annotations.functions.ComponentFlagFunction;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.plugin.VolmitSender;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World.Environment;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import java.io.*;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.common.plugin.VolmitSender;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World.Environment;
import org.bukkit.block.Biome;
import org.bukkit.block.data.BlockData;
import java.io.*;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@Accessors(chain = true)
@AllArgsConstructor
@@ -74,21 +74,21 @@ public class IrisDimension extends IrisRegistrant {
private final transient AtomicCache<CNG> rockLayerGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> fluidLayerGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> coordFracture = new AtomicCache<>();
private final transient AtomicCache<Double> sinr = new AtomicCache<>();
private final transient AtomicCache<Double> cosr = new AtomicCache<>();
private final transient AtomicCache<Double> rad = new AtomicCache<>();
private final transient AtomicCache<Boolean> featuresUsed = new AtomicCache<>();
private final transient AtomicCache<Map<String, IrisDimensionCarvingEntry>> carvingEntryIndex = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<Double> sinr = new AtomicCache<>();
private final transient AtomicCache<Double> cosr = new AtomicCache<>();
private final transient AtomicCache<Double> rad = new AtomicCache<>();
private final transient AtomicCache<Boolean> featuresUsed = new AtomicCache<>();
private final transient AtomicCache<Map<String, IrisDimensionCarvingEntry>> carvingEntryIndex = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
@MinNumber(2)
@Required
@Desc("The human readable name of this dimension")
private String name = "A Dimension";
@MinNumber(1)
@MaxNumber(2032)
@Desc("Maximum height at which players can be teleported to through gameplay.")
private int logicalHeight = 256;
@MinNumber(1)
@MaxNumber(2032)
@Desc("Maximum height at which players can be teleported to through gameplay.")
private int logicalHeight = 256;
@Desc("If set to true, Iris will remove chunks to allow visualizing cross sections of chunks easily")
private boolean debugChunkCrossSections = false;
@Desc("Vertically split up the biome palettes with 3 air blocks in between to visualize them")
@@ -99,10 +99,10 @@ public class IrisDimension extends IrisRegistrant {
@MaxNumber(16)
@Desc("Customize the palette height explosion")
private int explodeBiomePaletteSize = 3;
@MinNumber(2)
@MaxNumber(16)
@Desc("Every X/Z % debugCrossSectionsMod == 0 cuts the chunk")
private int debugCrossSectionsMod = 3;
@MinNumber(2)
@MaxNumber(16)
@Desc("Every X/Z % debugCrossSectionsMod == 0 cuts the chunk")
private int debugCrossSectionsMod = 3;
@Desc("Tree growth override settings")
private IrisTreeSettings treeSettings = new IrisTreeSettings();
@Desc("Spawn Entities in this dimension over time. Iris will continually replenish these mobs just like vanilla does.")
@@ -143,29 +143,31 @@ public class IrisDimension extends IrisRegistrant {
private boolean postProcessing = true;
@Desc("Add slabs in post processing")
private boolean postProcessingSlabs = true;
@Desc("Add painted walls in post processing")
private boolean postProcessingWalls = true;
@Desc("Enable or disable all carving for this dimension")
private boolean carvingEnabled = true;
@ArrayType(type = IrisDimensionCarvingEntry.class, min = 1)
@Desc("Dimension-level cave biome carving overrides with absolute world Y ranges")
private KList<IrisDimensionCarvingEntry> carving = new KList<>();
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Configuration of fluid bodies such as rivers & lakes")
private IrisFluidBodies fluidBodies = new IrisFluidBodies();
@ArrayType(type = IrisExternalDatapack.class, min = 1)
@Desc("Pack-scoped external datapack sources for structure import and optional vanilla replacement")
private KList<IrisExternalDatapack> externalDatapacks = new KList<>();
@Desc("forceConvertTo320Height")
private Boolean forceConvertTo320Height = false;
@Desc("Add painted walls in post processing")
private boolean postProcessingWalls = true;
@Desc("Enable or disable all carving for this dimension")
private boolean carvingEnabled = true;
@ArrayType(type = IrisDimensionCarvingEntry.class, min = 1)
@Desc("Dimension-level cave biome carving overrides with absolute world Y ranges")
private KList<IrisDimensionCarvingEntry> carving = new KList<>();
@Desc("Profile-driven 3D cave configuration")
private IrisCaveProfile caveProfile = new IrisCaveProfile();
@Desc("Configuration of fluid bodies such as rivers & lakes")
private IrisFluidBodies fluidBodies = new IrisFluidBodies();
@Desc("Enable or disable vanilla structure generation from the extracted vanilla datapack. When disabled, no vanilla structures spawn. When enabled, structures come from the vanilla datapack and can be overridden by external datapacks.")
private boolean vanillaStructures = true;
@ArrayType(type = IrisExternalDatapack.class, min = 1)
@Desc("Pack-scoped external datapack sources for structure import and optional vanilla replacement")
private KList<IrisExternalDatapack> externalDatapacks = new KList<>();
@Desc("forceConvertTo320Height")
private Boolean forceConvertTo320Height = false;
@Desc("The world environment")
private Environment environment = Environment.NORMAL;
@RegistryListResource(IrisRegion.class)
@Required
@ArrayType(min = 1, type = String.class)
@Desc("Define all of the regions to include in this dimension. Dimensions -> Regions -> Biomes -> Objects etc")
private KList<String> regions = new KList<>();
@RegistryListResource(IrisRegion.class)
@Required
@ArrayType(min = 1, type = String.class)
@Desc("Define all of the regions to include in this dimension. Dimensions -> Regions -> Biomes -> Objects etc")
private KList<String> regions = new KList<>();
@Required
@MinNumber(0)
@MaxNumber(1024)
@@ -244,121 +246,121 @@ public class IrisDimension extends IrisRegistrant {
private boolean disableExplorerMaps = false;
@Desc("Collection of ores to be generated")
@ArrayType(type = IrisOreGenerator.class, min = 1)
private KList<IrisOreGenerator> ores = new KList<>();
private KList<IrisOreGenerator> ores = new KList<>();
@MinNumber(0)
@MaxNumber(318)
@Desc("The Subterrain Fluid Layer Height")
private int caveLavaHeight = 8;
@RegistryListFunction(ComponentFlagFunction.class)
@ArrayType(type = String.class)
@Desc("Collection of disabled components")
private KList<MantleFlag> disabledComponents = new KList<>();
@RegistryListFunction(ComponentFlagFunction.class)
@ArrayType(type = String.class)
@Desc("Collection of disabled components")
private KList<MantleFlag> disabledComponents = new KList<>();
public int getMaxHeight() {
return (int) getDimensionHeight().getMax();
}
public int getMinHeight() {
return (int) getDimensionHeight().getMin();
}
public Map<String, IrisDimensionCarvingEntry> getCarvingEntryIndex() {
return carvingEntryIndex.aquire(() -> {
Map<String, IrisDimensionCarvingEntry> index = new HashMap<>();
KList<IrisDimensionCarvingEntry> entries = getCarving();
if (entries == null || entries.isEmpty()) {
return index;
}
for (IrisDimensionCarvingEntry entry : entries) {
if (entry == null) {
continue;
}
String entryId = entry.getId();
if (entryId == null || entryId.isBlank()) {
continue;
}
index.put(entryId.trim(), entry);
}
return index;
});
}
public void setCarving(KList<IrisDimensionCarvingEntry> carving) {
this.carving = carving == null ? new KList<>() : carving;
carvingEntryIndex.reset();
}
public int getMinHeight() {
return (int) getDimensionHeight().getMin();
}
public BlockData generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) {
KList<IrisOreGenerator> localOres = surface ? getSurfaceOres() : getUndergroundOres();
return generateOres(localOres, x, y, z, rng, data);
}
public BlockData generateSurfaceOres(int x, int y, int z, RNG rng, IrisData data) {
return generateOres(getSurfaceOres(), x, y, z, rng, data);
}
public BlockData generateUndergroundOres(int x, int y, int z, RNG rng, IrisData data) {
return generateOres(getUndergroundOres(), x, y, z, rng, data);
}
public boolean hasSurfaceOres() {
return !getSurfaceOres().isEmpty();
}
public boolean hasUndergroundOres() {
return !getUndergroundOres().isEmpty();
}
private BlockData generateOres(KList<IrisOreGenerator> localOres, int x, int y, int z, RNG rng, IrisData data) {
if (localOres.isEmpty()) {
return null;
}
int oreCount = localOres.size();
for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) {
IrisOreGenerator oreGenerator = localOres.get(oreIndex);
BlockData ore = oreGenerator.generate(x, y, z, rng, data);
if (ore != null) {
return ore;
}
}
return null;
}
public void setOres(KList<IrisOreGenerator> ores) {
this.ores = ores == null ? new KList<>() : ores;
surfaceOreCache.reset();
undergroundOreCache.reset();
}
private KList<IrisOreGenerator> getSurfaceOres() {
return getOres(true);
}
private KList<IrisOreGenerator> getUndergroundOres() {
return getOres(false);
}
private KList<IrisOreGenerator> getOres(boolean surface) {
AtomicCache<KList<IrisOreGenerator>> oreCache = surface ? surfaceOreCache : undergroundOreCache;
return oreCache.aquire(() -> {
KList<IrisOreGenerator> filtered = new KList<>();
KList<IrisOreGenerator> localOres = ores;
int oreCount = localOres.size();
for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) {
IrisOreGenerator oreGenerator = localOres.get(oreIndex);
if (oreGenerator.isGenerateSurface() == surface) {
filtered.add(oreGenerator);
}
}
return filtered;
});
}
public Map<String, IrisDimensionCarvingEntry> getCarvingEntryIndex() {
return carvingEntryIndex.aquire(() -> {
Map<String, IrisDimensionCarvingEntry> index = new HashMap<>();
KList<IrisDimensionCarvingEntry> entries = getCarving();
if (entries == null || entries.isEmpty()) {
return index;
}
for (IrisDimensionCarvingEntry entry : entries) {
if (entry == null) {
continue;
}
String entryId = entry.getId();
if (entryId == null || entryId.isBlank()) {
continue;
}
index.put(entryId.trim(), entry);
}
return index;
});
}
public void setCarving(KList<IrisDimensionCarvingEntry> carving) {
this.carving = carving == null ? new KList<>() : carving;
carvingEntryIndex.reset();
}
public BlockData generateOres(int x, int y, int z, RNG rng, IrisData data, boolean surface) {
KList<IrisOreGenerator> localOres = surface ? getSurfaceOres() : getUndergroundOres();
return generateOres(localOres, x, y, z, rng, data);
}
public BlockData generateSurfaceOres(int x, int y, int z, RNG rng, IrisData data) {
return generateOres(getSurfaceOres(), x, y, z, rng, data);
}
public BlockData generateUndergroundOres(int x, int y, int z, RNG rng, IrisData data) {
return generateOres(getUndergroundOres(), x, y, z, rng, data);
}
public boolean hasSurfaceOres() {
return !getSurfaceOres().isEmpty();
}
public boolean hasUndergroundOres() {
return !getUndergroundOres().isEmpty();
}
private BlockData generateOres(KList<IrisOreGenerator> localOres, int x, int y, int z, RNG rng, IrisData data) {
if (localOres.isEmpty()) {
return null;
}
int oreCount = localOres.size();
for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) {
IrisOreGenerator oreGenerator = localOres.get(oreIndex);
BlockData ore = oreGenerator.generate(x, y, z, rng, data);
if (ore != null) {
return ore;
}
}
return null;
}
public void setOres(KList<IrisOreGenerator> ores) {
this.ores = ores == null ? new KList<>() : ores;
surfaceOreCache.reset();
undergroundOreCache.reset();
}
private KList<IrisOreGenerator> getSurfaceOres() {
return getOres(true);
}
private KList<IrisOreGenerator> getUndergroundOres() {
return getOres(false);
}
private KList<IrisOreGenerator> getOres(boolean surface) {
AtomicCache<KList<IrisOreGenerator>> oreCache = surface ? surfaceOreCache : undergroundOreCache;
return oreCache.aquire(() -> {
KList<IrisOreGenerator> filtered = new KList<>();
KList<IrisOreGenerator> localOres = ores;
int oreCount = localOres.size();
for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) {
IrisOreGenerator oreGenerator = localOres.get(oreIndex);
if (oreGenerator.isGenerateSurface() == surface) {
filtered.add(oreGenerator);
}
}
return filtered;
});
}
public int getFluidHeight() {
return fluidHeight - (int) dimensionHeight.getMin();
@@ -452,217 +454,217 @@ public class IrisDimension extends IrisRegistrant {
return landBiomeStyle;
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> folders, KSet<String> biomes) {
KMap<String, String> customBiomeToVanillaBiome = new KMap<>();
String namespace = getLoadKey().toLowerCase(Locale.ROOT);
for (IrisBiome irisBiome : getAllBiomes(data)) {
if (!irisBiome.isCustom()) {
continue;
}
Biome vanillaDerivative = irisBiome.getVanillaDerivative();
NamespacedKey vanillaDerivativeKey = vanillaDerivative == null ? null : vanillaDerivative.getKey();
String vanillaBiomeKey = vanillaDerivativeKey == null ? null : vanillaDerivativeKey.toString();
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customBiomeId = customBiome.getId();
String customBiomeKey = namespace + ":" + customBiomeId.toLowerCase(Locale.ROOT);
String json = customBiome.generateJson(fixer);
synchronized (biomes) {
if (!biomes.add(customBiomeId)) {
Iris.verbose("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
continue;
}
}
if (vanillaBiomeKey != null) {
customBiomeToVanillaBiome.put(customBiomeKey, vanillaBiomeKey);
}
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/" + namespace + "/worldgen/biome/" + customBiomeId + ".json");
Iris.verbose(" Installing Data Pack Biome: " + output.getPath());
output.getParentFile().mkdirs();
try {
IO.writeAll(output, json);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
}
}
}
installStructureBiomeTags(folders, customBiomeToVanillaBiome);
}
private void installStructureBiomeTags(KList<File> folders, KMap<String, String> customBiomeToVanillaBiome) {
if (customBiomeToVanillaBiome.isEmpty()) {
return;
}
KMap<String, KList<String>> vanillaTags = INMS.get().getVanillaStructureBiomeTags();
if (vanillaTags == null || vanillaTags.isEmpty()) {
return;
}
KMap<String, KSet<String>> customTagValues = new KMap<>();
for (Map.Entry<String, String> customBiomeEntry : customBiomeToVanillaBiome.entrySet()) {
String customBiomeKey = customBiomeEntry.getKey();
String vanillaBiomeKey = customBiomeEntry.getValue();
if (vanillaBiomeKey == null) {
continue;
}
for (Map.Entry<String, KList<String>> tagEntry : vanillaTags.entrySet()) {
KList<String> values = tagEntry.getValue();
if (values == null || !values.contains(vanillaBiomeKey)) {
continue;
}
customTagValues.computeIfAbsent(tagEntry.getKey(), key -> new KSet<>()).add(customBiomeKey);
}
}
if (customTagValues.isEmpty()) {
return;
}
for (File datapacks : folders) {
for (Map.Entry<String, KSet<String>> tagEntry : customTagValues.entrySet()) {
String tagPath = tagEntry.getKey();
KSet<String> customValues = tagEntry.getValue();
if (customValues == null || customValues.isEmpty()) {
continue;
}
File output = new File(datapacks, "iris/data/minecraft/tags/worldgen/biome/" + tagPath + ".json");
try {
writeMergedStructureBiomeTag(output, customValues);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
}
}
}
private void writeMergedStructureBiomeTag(File output, KSet<String> customValues) throws IOException {
synchronized (IrisDimension.class) {
KSet<String> mergedValues = readExistingStructureBiomeTagValues(output);
mergedValues.addAll(customValues);
JSONArray values = new JSONArray();
KList<String> sortedValues = new KList<>(mergedValues).sort();
for (String value : sortedValues) {
values.put(value);
}
JSONObject json = new JSONObject();
json.put("replace", false);
json.put("values", values);
writeAtomicFile(output, json.toString(4));
}
}
private KSet<String> readExistingStructureBiomeTagValues(File output) {
KSet<String> values = new KSet<>();
if (output == null || !output.exists()) {
return values;
}
try {
JSONObject json = new JSONObject(IO.readAll(output));
if (!json.has("values")) {
return values;
}
JSONArray existingValues = json.getJSONArray("values");
for (int index = 0; index < existingValues.length(); index++) {
Object rawValue = existingValues.get(index);
if (rawValue == null) {
continue;
}
String value = String.valueOf(rawValue).trim();
if (!value.isEmpty()) {
values.add(value);
}
}
} catch (Throwable e) {
Iris.warn("Skipping malformed existing structure biome tag file: " + output.getPath());
}
return values;
}
private void writeAtomicFile(File output, String contents) throws IOException {
File parent = output.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
File temp = new File(parent, output.getName() + ".tmp-" + System.nanoTime());
IO.writeAll(temp, contents);
Path tempPath = temp.toPath();
Path outputPath = output.toPath();
try {
Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING);
}
}
public void installBiomes(IDataFixer fixer, DataProvider data, KList<File> folders, KSet<String> biomes) {
KMap<String, String> customBiomeToVanillaBiome = new KMap<>();
String namespace = getLoadKey().toLowerCase(Locale.ROOT);
public Dimension getBaseDimension() {
return switch (getEnvironment()) {
case NETHER -> Dimension.NETHER;
case THE_END -> Dimension.END;
default -> Dimension.OVERWORLD;
};
}
public String getDimensionTypeKey() {
return sanitizeDimensionTypeKeyValue(getLoadKey());
}
public static String sanitizeDimensionTypeKeyValue(String value) {
if (value == null || value.isBlank()) {
return "dimension";
}
String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/");
sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_");
sanitized = sanitized.replaceAll("/+", "/");
sanitized = sanitized.replaceAll("^/+", "");
sanitized = sanitized.replaceAll("/+$", "");
if (sanitized.contains("..")) {
sanitized = sanitized.replace("..", "_");
}
sanitized = sanitized.replace("/", "_");
return sanitized.isBlank() ? "dimension" : sanitized;
}
public IrisDimensionType getDimensionType() {
return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
}
public void installDimensionType(IDataFixer fixer, KList<File> folders) {
IrisDimensionType type = getDimensionType();
String json = type.toJson(fixer);
String dimensionTypeKey = getDimensionTypeKey();
Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + dimensionTypeKey + '"');
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json");
output.getParentFile().mkdirs();
try {
IO.writeAll(output, json);
} catch (IOException e) {
for (IrisBiome irisBiome : getAllBiomes(data)) {
if (!irisBiome.isCustom()) {
continue;
}
Biome vanillaDerivative = irisBiome.getVanillaDerivative();
NamespacedKey vanillaDerivativeKey = vanillaDerivative == null ? null : vanillaDerivative.getKey();
String vanillaBiomeKey = vanillaDerivativeKey == null ? null : vanillaDerivativeKey.toString();
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customBiomeId = customBiome.getId();
String customBiomeKey = namespace + ":" + customBiomeId.toLowerCase(Locale.ROOT);
String json = customBiome.generateJson(fixer);
synchronized (biomes) {
if (!biomes.add(customBiomeId)) {
Iris.verbose("Duplicate Data Pack Biome: " + getLoadKey() + "/" + customBiomeId);
continue;
}
}
if (vanillaBiomeKey != null) {
customBiomeToVanillaBiome.put(customBiomeKey, vanillaBiomeKey);
}
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/" + namespace + "/worldgen/biome/" + customBiomeId + ".json");
Iris.verbose(" Installing Data Pack Biome: " + output.getPath());
output.getParentFile().mkdirs();
try {
IO.writeAll(output, json);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
}
}
}
installStructureBiomeTags(folders, customBiomeToVanillaBiome);
}
private void installStructureBiomeTags(KList<File> folders, KMap<String, String> customBiomeToVanillaBiome) {
if (customBiomeToVanillaBiome.isEmpty()) {
return;
}
KMap<String, KList<String>> vanillaTags = INMS.get().getVanillaStructureBiomeTags();
if (vanillaTags == null || vanillaTags.isEmpty()) {
return;
}
KMap<String, KSet<String>> customTagValues = new KMap<>();
for (Map.Entry<String, String> customBiomeEntry : customBiomeToVanillaBiome.entrySet()) {
String customBiomeKey = customBiomeEntry.getKey();
String vanillaBiomeKey = customBiomeEntry.getValue();
if (vanillaBiomeKey == null) {
continue;
}
for (Map.Entry<String, KList<String>> tagEntry : vanillaTags.entrySet()) {
KList<String> values = tagEntry.getValue();
if (values == null || !values.contains(vanillaBiomeKey)) {
continue;
}
customTagValues.computeIfAbsent(tagEntry.getKey(), key -> new KSet<>()).add(customBiomeKey);
}
}
if (customTagValues.isEmpty()) {
return;
}
for (File datapacks : folders) {
for (Map.Entry<String, KSet<String>> tagEntry : customTagValues.entrySet()) {
String tagPath = tagEntry.getKey();
KSet<String> customValues = tagEntry.getValue();
if (customValues == null || customValues.isEmpty()) {
continue;
}
File output = new File(datapacks, "iris/data/minecraft/tags/worldgen/biome/" + tagPath + ".json");
try {
writeMergedStructureBiomeTag(output, customValues);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
}
}
}
private void writeMergedStructureBiomeTag(File output, KSet<String> customValues) throws IOException {
synchronized (IrisDimension.class) {
KSet<String> mergedValues = readExistingStructureBiomeTagValues(output);
mergedValues.addAll(customValues);
JSONArray values = new JSONArray();
KList<String> sortedValues = new KList<>(mergedValues).sort();
for (String value : sortedValues) {
values.put(value);
}
JSONObject json = new JSONObject();
json.put("replace", false);
json.put("values", values);
writeAtomicFile(output, json.toString(4));
}
}
private KSet<String> readExistingStructureBiomeTagValues(File output) {
KSet<String> values = new KSet<>();
if (output == null || !output.exists()) {
return values;
}
try {
JSONObject json = new JSONObject(IO.readAll(output));
if (!json.has("values")) {
return values;
}
JSONArray existingValues = json.getJSONArray("values");
for (int index = 0; index < existingValues.length(); index++) {
Object rawValue = existingValues.get(index);
if (rawValue == null) {
continue;
}
String value = String.valueOf(rawValue).trim();
if (!value.isEmpty()) {
values.add(value);
}
}
} catch (Throwable e) {
Iris.warn("Skipping malformed existing structure biome tag file: " + output.getPath());
}
return values;
}
private void writeAtomicFile(File output, String contents) throws IOException {
File parent = output.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
File temp = new File(parent, output.getName() + ".tmp-" + System.nanoTime());
IO.writeAll(temp, contents);
Path tempPath = temp.toPath();
Path outputPath = output.toPath();
try {
Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tempPath, outputPath, StandardCopyOption.REPLACE_EXISTING);
}
}
public Dimension getBaseDimension() {
return switch (getEnvironment()) {
case NETHER -> Dimension.NETHER;
case THE_END -> Dimension.END;
default -> Dimension.OVERWORLD;
};
}
public String getDimensionTypeKey() {
return sanitizeDimensionTypeKeyValue(getLoadKey());
}
public static String sanitizeDimensionTypeKeyValue(String value) {
if (value == null || value.isBlank()) {
return "dimension";
}
String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/");
sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_");
sanitized = sanitized.replaceAll("/+", "/");
sanitized = sanitized.replaceAll("^/+", "");
sanitized = sanitized.replaceAll("/+$", "");
if (sanitized.contains("..")) {
sanitized = sanitized.replace("..", "_");
}
sanitized = sanitized.replace("/", "_");
return sanitized.isBlank() ? "dimension" : sanitized;
}
public IrisDimensionType getDimensionType() {
return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
}
public void installDimensionType(IDataFixer fixer, KList<File> folders) {
IrisDimensionType type = getDimensionType();
String json = type.toJson(fixer);
String dimensionTypeKey = getDimensionTypeKey();
Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + dimensionTypeKey + '"');
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json");
output.getParentFile().mkdirs();
try {
IO.writeAll(output, json);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
@@ -1,8 +1,6 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.volmlib.util.collection.KList;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -12,7 +10,7 @@ import lombok.experimental.Accessors;
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Desc("Defines a pack-scoped external datapack source for structure import and optional vanilla replacement")
@Desc("Defines an external datapack source. When replace is true, minecraft namespace entries override the vanilla datapack.")
public class IrisExternalDatapack {
@Desc("Stable id for this external datapack entry")
private String id = "";
@@ -26,28 +24,6 @@ public class IrisExternalDatapack {
@Desc("If true, Iris hard-fails startup when this external datapack cannot be synced/imported/installed")
private boolean required = false;
@Desc("If true, minecraft namespace worldgen assets may replace vanilla targets listed in replaceTargets")
private boolean replaceVanilla = false;
@Desc("If true, structures projected from this datapack id receive smartbore foundation extension during generation")
private boolean supportSmartBore = true;
@Desc("Explicit replacement targets for minecraft namespace assets")
private IrisExternalDatapackReplaceTargets replaceTargets = new IrisExternalDatapackReplaceTargets();
@ArrayType(type = IrisExternalDatapackStructureAlias.class, min = 1)
@Desc("Optional structure alias mappings used to synthesize vanilla structure replacements from non-minecraft source keys")
private KList<IrisExternalDatapackStructureAlias> structureAliases = new KList<>();
@ArrayType(type = IrisExternalDatapackStructureSetAlias.class, min = 1)
@Desc("Optional structure-set alias mappings used to synthesize vanilla structure_set replacements from non-minecraft source keys")
private KList<IrisExternalDatapackStructureSetAlias> structureSetAliases = new KList<>();
@ArrayType(type = IrisExternalDatapackTemplateAlias.class, min = 1)
@Desc("Optional template location alias mappings applied while projecting template pools")
private KList<IrisExternalDatapackTemplateAlias> templateAliases = new KList<>();
@ArrayType(type = IrisExternalDatapackStructurePatch.class, min = 1)
@Desc("Structure placement patches applied when this external datapack is projected")
private KList<IrisExternalDatapackStructurePatch> structurePatches = new KList<>();
@Desc("If true, this datapack replaces vanilla worldgen entries. The datapack itself determines what it overrides.")
private boolean replace = false;
}
@@ -18,8 +18,8 @@ public class IrisExternalDatapackBinding {
@Desc("Enable or disable this scoped binding")
private boolean enabled = true;
@Desc("Override replaceVanilla behavior for this scoped binding (null keeps dimension default)")
private Boolean replaceVanillaOverride = null;
@Desc("Override replace behavior for this scoped binding (null keeps dimension default)")
private Boolean replaceOverride = null;
@Desc("Include child biomes recursively when collecting scoped biome boundaries")
private boolean includeChildren = true;
@@ -1,54 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Desc;
import art.arcane.volmlib.util.collection.KList;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Desc("Explicit minecraft namespace targets that may be replaced by an external datapack")
public class IrisExternalDatapackReplaceTargets {
@ArrayType(type = String.class, min = 1)
@Desc("Structure ids that may be replaced when replaceVanilla is enabled")
private KList<String> structures = new KList<>();
@ArrayType(type = String.class, min = 1)
@Desc("Structure set ids that may be replaced when replaceVanilla is enabled")
private KList<String> structureSets = new KList<>();
@ArrayType(type = String.class, min = 1)
@Desc("Template pool ids that may be replaced when replaceVanilla is enabled")
private KList<String> templatePools = new KList<>();
@ArrayType(type = String.class, min = 1)
@Desc("Processor list ids that may be replaced when replaceVanilla is enabled")
private KList<String> processorLists = new KList<>();
@ArrayType(type = String.class, min = 1)
@Desc("Biome has_structure tag ids that may be replaced when replaceVanilla is enabled")
private KList<String> biomeHasStructureTags = new KList<>();
@ArrayType(type = String.class, min = 1)
@Desc("Configured feature ids that may be replaced when replaceVanilla is enabled")
private KList<String> configuredFeatures = new KList<>();
@ArrayType(type = String.class, min = 1)
@Desc("Placed feature ids that may be replaced when replaceVanilla is enabled")
private KList<String> placedFeatures = new KList<>();
public boolean hasAnyTargets() {
return !structures.isEmpty()
|| !structureSets.isEmpty()
|| !templatePools.isEmpty()
|| !processorLists.isEmpty()
|| !biomeHasStructureTags.isEmpty()
|| !configuredFeatures.isEmpty()
|| !placedFeatures.isEmpty();
}
}
@@ -1,20 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Desc("Maps a vanilla structure replacement target to a source structure key from an external datapack")
public class IrisExternalDatapackStructureAlias {
@Desc("Vanilla replacement target structure id")
private String target = "";
@Desc("Source structure id to clone when the target id is not provided directly")
private String source = "";
}
@@ -1,23 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Desc("Defines a structure-level patch override for external datapack projection")
public class IrisExternalDatapackStructurePatch {
@Desc("Structure id to patch")
private String structure = "";
@Desc("Enable or disable this patch entry")
private boolean enabled = true;
@Desc("Absolute start height override for this structure")
private int startHeightAbsolute = -27;
}
@@ -1,20 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Desc("Maps a vanilla structure_set replacement target to a source structure_set key from an external datapack")
public class IrisExternalDatapackStructureSetAlias {
@Desc("Vanilla replacement target structure_set id")
private String target = "";
@Desc("Source structure_set id to clone when the target id is not provided directly")
private String source = "";
}
@@ -1,23 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.engine.object.annotations.Desc;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Desc("Maps missing template-pool element locations from an external datapack to replacement template locations")
public class IrisExternalDatapackTemplateAlias {
@Desc("Source template location to rewrite")
private String from = "";
@Desc("Target template location. Use minecraft:empty to convert the element to an empty pool element")
private String to = "";
@Desc("Enable or disable this alias entry")
private boolean enabled = true;
}
@@ -57,6 +57,8 @@ import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.MultipleFacing;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.block.data.type.Leaves;
import org.bukkit.block.data.type.Slab;
import org.bukkit.block.data.type.Stairs;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
@@ -131,13 +133,10 @@ public class IrisObject extends IrisRegistrant {
new IrisPosition(new BlockVector(size.getX() - 1, size.getY() - 1, size.getZ() - 1).subtract(center).toBlockVector()));
}
@SuppressWarnings({"resource", "RedundantSuppression"})
public static BlockVector sampleSize(File file) throws IOException {
FileInputStream in = new FileInputStream(file);
DataInputStream din = new DataInputStream(in);
BlockVector bv = new BlockVector(din.readInt(), din.readInt(), din.readInt());
Iris.later(din::close);
return bv;
try (DataInputStream din = new DataInputStream(new FileInputStream(file))) {
return new BlockVector(din.readInt(), din.readInt(), din.readInt());
}
}
private static List<BlockVector> blocksBetweenTwoPoints(Vector loc1, Vector loc2) {
@@ -159,6 +158,16 @@ public class IrisObject extends IrisRegistrant {
return locations;
}
private static boolean shouldStilt(BlockData data) {
if (!data.getMaterial().isOccluding()) {
return false;
}
if (data instanceof Stairs || data instanceof Slab) {
return false;
}
return data.getMaterial() != Material.DIRT_PATH;
}
public AxisAlignedBB getAABB() {
return aabb.aquire(() -> getAABBFor(new BlockVector(w, h, d)));
}
@@ -494,9 +503,9 @@ public class IrisObject extends IrisRegistrant {
return;
}
FileOutputStream out = new FileOutputStream(file);
write(out);
out.close();
try (FileOutputStream out = new FileOutputStream(file)) {
write(out);
}
}
public void write(File file, VolmitSender sender) throws IOException {
@@ -504,9 +513,9 @@ public class IrisObject extends IrisRegistrant {
return;
}
FileOutputStream out = new FileOutputStream(file);
write(out, sender);
out.close();
try (FileOutputStream out = new FileOutputStream(file)) {
write(out, sender);
}
}
public void shrinkwrap() {
@@ -672,7 +681,8 @@ public class IrisObject extends IrisRegistrant {
boolean warped = !config.getWarp().isFlat();
boolean stilting = (config.getMode().equals(ObjectPlaceMode.STILT) || config.getMode().equals(ObjectPlaceMode.FAST_STILT) ||
config.getMode() == ObjectPlaceMode.MIN_STILT || config.getMode() == ObjectPlaceMode.FAST_MIN_STILT ||
config.getMode() == ObjectPlaceMode.CENTER_STILT);
config.getMode() == ObjectPlaceMode.CENTER_STILT || config.getMode() == ObjectPlaceMode.ERODE_STILT);
boolean eroding = config.getMode() == ObjectPlaceMode.ERODE_STILT;
KMap<Position2, Integer> heightmap = config.getSnow() > 0 ? new KMap<>() : null;
int spinx = rng.imax() / 1000;
int spiny = rng.imax() / 1000;
@@ -954,7 +964,7 @@ public class IrisObject extends IrisRegistrant {
i = config.getRotation().rotate(i.clone(), spinx, spiny, spinz).clone();
i = config.getTranslate().translate(i.clone(), config.getRotation(), spinx, spiny, spinz).clone();
if (stilting && i.getBlockY() < lowest && B.isSolid(data)) {
if (stilting && i.getBlockY() < lowest && shouldStilt(data)) {
lowest = i.getBlockY();
}
@@ -1054,6 +1064,45 @@ public class IrisObject extends IrisRegistrant {
if (stilting) {
readLock.lock();
IrisStiltSettings settings = config.getStiltSettings();
double erodeCentroidX = 0;
double erodeCentroidZ = 0;
double erodeMaxDist = 1;
if (eroding) {
int centroidCount = 0;
for (BlockVector g : blocks.keys()) {
BlockVector rot = config.getRotation().rotate(g.clone(), spinx, spiny, spinz).clone();
rot = config.getTranslate().translate(rot.clone(), config.getRotation(), spinx, spiny, spinz).clone();
if (rot.getBlockY() == lowest) {
BlockData bd = blocks.get(g);
if (bd != null && shouldStilt(bd)) {
erodeCentroidX += rot.getX();
erodeCentroidZ += rot.getZ();
centroidCount++;
}
}
}
if (centroidCount > 0) {
erodeCentroidX /= centroidCount;
erodeCentroidZ /= centroidCount;
}
for (BlockVector g : blocks.keys()) {
BlockVector rot = config.getRotation().rotate(g.clone(), spinx, spiny, spinz).clone();
rot = config.getTranslate().translate(rot.clone(), config.getRotation(), spinx, spiny, spinz).clone();
if (rot.getBlockY() == lowest) {
BlockData bd = blocks.get(g);
if (bd != null && shouldStilt(bd)) {
double dx = rot.getX() - erodeCentroidX;
double dz = rot.getZ() - erodeCentroidZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist > erodeMaxDist) {
erodeMaxDist = dist;
}
}
}
}
}
for (BlockVector g : blocks.keys()) {
BlockData sourceData;
try {
@@ -1069,13 +1118,18 @@ public class IrisObject extends IrisRegistrant {
sourceData = AIR;
}
if (!B.isSolid(sourceData)) {
if (!shouldStilt(sourceData)) {
continue;
}
BlockData d = sourceData;
if (settings != null && settings.getPalette() != null) {
d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata);
} else {
Material mat = d.getMaterial();
if (mat == Material.GRASS_BLOCK || mat == Material.MYCELIUM || mat == Material.PODZOL || mat == Material.DIRT_PATH) {
d = Material.DIRT.createBlockData();
}
}
BlockVector i = g.clone();
@@ -1102,7 +1156,7 @@ public class IrisObject extends IrisRegistrant {
}
}
if (d == null || !B.isSolid(d))
if (d == null || !d.getMaterial().isOccluding())
continue;
xx = x + (int) Math.round(i.getX());
@@ -1127,7 +1181,31 @@ public class IrisObject extends IrisRegistrant {
if (settings.getYMax() != 0)
lowerBound -= Math.min(config.getStiltSettings().getYMax() - (lowest + y - highest), 0);
}
if (eroding) {
double dx = i.getX() - erodeCentroidX;
double dz = i.getZ() - erodeCentroidZ;
double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist;
normalizedDist = Math.min(normalizedDist, 1.0);
int totalDepth = (lowest + y) - lowerBound;
int erodeDepth = (int) (totalDepth * Math.pow(1.0 - normalizedDist, 1.5));
lowerBound = (lowest + y) - erodeDepth;
}
for (int j = lowest + y; j > lowerBound; j--) {
if (eroding) {
int depth = (lowest + y) - j;
int totalDepth = (lowest + y) - lowerBound;
double depthRatio = totalDepth > 0 ? (double) depth / totalDepth : 0;
if (depthRatio > 0.4) {
long hash = ((long) (xx * 341873128712L) ^ ((long) j * 132897987541L) ^ ((long) zz * 735791245321L));
double skipChance = (depthRatio - 0.4) / 0.6;
if ((Math.abs(hash) % 1000) / 1000.0 < skipChance * 0.7) {
continue;
}
}
}
if (B.isVineBlock(d)) {
MultipleFacing f = (MultipleFacing) d;
for (BlockFace face : f.getAllowedFaces()) {
@@ -62,6 +62,10 @@ public enum ObjectPlaceMode {
CENTER_STILT,
@Desc("Erode stilting tapers columns downward like an ice cream cone. Blocks near the center extend deepest while edge blocks drop off first. Blocks are randomly skipped in the lower portion for a rough organic eroded texture.")
ERODE_STILT,
@Desc("Samples the height of the terrain at every x,z position of your object and pushes it down to the surface. It's pretty much like a melt function over the terrain.")
PAINT
@@ -380,6 +380,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
INMS.get().placeStructures(c);
setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z);
engine.getWorldManager().onChunkLoad(c, true);
world.refreshChunk(c.getX(), c.getZ());
} finally {
Iris.tickets.removeTicket(c);
}
@@ -464,6 +465,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
CompletableFuture.runAsync(() -> {
setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z);
engine.getWorldManager().onChunkLoad(c, true);
world.refreshChunk(c.getX(), c.getZ());
}, syncExecutor).get();
} finally {
Iris.tickets.removeTicket(c);