This commit is contained in:
Brian Neumann-Fopiano
2026-02-19 01:23:08 -05:00
parent 1f41e195cf
commit d643461e2e
13 changed files with 1942 additions and 62 deletions

View File

@@ -515,6 +515,7 @@ public class Iris extends VolmitPlugin implements Listener {
services.values().forEach(this::registerListener);
addShutdownHook();
processPendingStartupWorldDeletes();
IrisToolbelt.applyPregenPerformanceProfile();
if (J.isFolia()) {
checkForBukkitWorlds(s -> true);

View File

@@ -152,6 +152,11 @@ public class IrisSettings {
public boolean useVirtualThreads = false;
public boolean useTicketQueue = true;
public int maxConcurrency = 256;
public boolean startupNoisemapPrebake = true;
public boolean enablePregenPerformanceProfile = true;
public int pregenProfileNoiseCacheSize = 4_096;
public boolean pregenProfileEnableFastCache = true;
public boolean pregenProfileLogJvmHints = true;
}
@Data
@@ -210,6 +215,9 @@ public class IrisSettings {
public boolean commandSounds = true;
public boolean debug = false;
public boolean dumpMantleOnError = false;
public boolean validatePacksOnStartup = true;
public boolean stopStartupOnPackValidationFailure = false;
public int maxPackValidationErrorsPerPack = 200;
public boolean disableNMS = false;
public boolean pluginMetrics = true;
public boolean splashLogoStartup = true;

View File

@@ -24,6 +24,7 @@ import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.pregenerator.LazyPregenerator;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@@ -68,6 +69,11 @@ public class CommandLazyPregen implements DirectorExecutor {
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
PlatformChunkGenerator platform = IrisToolbelt.access(world);
if (platform != null) {
IrisToolbelt.applyPregenPerformanceProfile(platform.getEngine());
}
LazyPregenerator.LazyPregenJob pregenJob = LazyPregenerator.LazyPregenJob.builder()
.world(worldName)
.healingPosition(0)

View File

@@ -27,7 +27,9 @@ import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.service.ConversionSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisNoisemapPrebakePipeline;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
@@ -63,6 +65,7 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.iris.util.common.scheduling.jobs.ParallelRadiusJob;
import io.papermc.lib.PaperLib;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.util.BlockVector;
@@ -76,8 +79,11 @@ import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
@@ -512,6 +518,18 @@ public class CommandStudio implements DirectorExecutor {
File report = Iris.instance.getDataFile("profile.txt");
IrisProject project = new IrisProject(pack);
IrisData data = IrisData.get(pack);
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
long profileSeed = IrisNoisemapPrebakePipeline.dynamicStartupSeed();
if (activeEngine != null) {
profileSeed = activeEngine.getSeedManager().getSeed();
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
} else {
IrisToolbelt.applyPregenPerformanceProfile();
}
IrisNoisemapPrebakePipeline.prebake(data, new SeedManager(profileSeed), "studio-profile", dimension.getLoadKey());
KList<String> fileText = new KList<>();
@@ -691,6 +709,116 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
}
@Director(description = "Profiles a dimension with a cache warm-up pass", origin = DirectorOrigin.PLAYER)
public void profilecache(
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
IrisDimension dimension
) {
File pack = dimension.getLoadFile().getParentFile().getParentFile();
IrisData data = IrisData.get(pack);
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
long profileSeed = IrisNoisemapPrebakePipeline.dynamicStartupSeed();
if (activeEngine != null) {
profileSeed = activeEngine.getSeedManager().getSeed();
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
} else {
IrisToolbelt.applyPregenPerformanceProfile();
}
sender().sendMessage(C.YELLOW + "Warming noisemap cache for profile...");
IrisNoisemapPrebakePipeline.prebakeForced(data, new SeedManager(profileSeed), "studio-profilecache", dimension.getLoadKey());
sender().sendMessage(C.YELLOW + "Running measured profile pass...");
profile(dimension);
}
@Director(description = "List pack noise generators as pack/generator", aliases = {"pack-noise", "packnoises"})
public void packnoise() {
LinkedHashSet<File> packFolders = new LinkedHashSet<>();
File packsFolder = Iris.instance.getDataFolder("packs");
File[] children = packsFolder.listFiles();
if (children != null) {
for (File child : children) {
if (child != null && child.isDirectory()) {
packFolders.add(child);
}
}
}
StudioSVC studioService = Iris.service(StudioSVC.class);
if (studioService != null && studioService.isProjectOpen()) {
IrisProject activeProject = studioService.getActiveProject();
if (activeProject != null && activeProject.getPath() != null && activeProject.getPath().isDirectory()) {
packFolders.add(activeProject.getPath());
}
}
ArrayList<String> entries = new ArrayList<>();
for (File packFolder : packFolders) {
IrisData packData = IrisData.get(packFolder);
String packName = packFolder.getName();
String[] keys = packData.getGeneratorLoader().getPossibleKeys();
for (String key : keys) {
entries.add(packName + "/" + key);
}
}
if (entries.isEmpty()) {
sender().sendMessage(C.YELLOW + "No pack noise generators were found.");
return;
}
Collections.sort(entries);
sender().sendMessage(C.GREEN + "Pack noise generators: " + C.GOLD + entries.size());
for (String entry : entries) {
sender().sendMessage(C.GRAY + entry);
}
}
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
StudioSVC studioService = Iris.service(StudioSVC.class);
if (studioService != null && studioService.isProjectOpen()) {
IrisProject activeProject = studioService.getActiveProject();
if (activeProject != null) {
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
if (isGeneratorDimension(activeProvider, dimension)) {
return activeProvider;
}
}
}
if (!sender().isPlayer()) {
return null;
}
Player player = sender().player();
if (player == null) {
return null;
}
PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld());
if (isGeneratorDimension(worldAccess, dimension)) {
return worldAccess;
}
return null;
}
private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) {
if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) {
return false;
}
IrisDimension engineDimension = generator.getEngine().getDimension();
if (engineDimension == null || engineDimension.getLoadKey() == null) {
return false;
}
return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey());
}
@Director(description = "Spawn an Iris entity", aliases = "summon", origin = DirectorOrigin.PLAYER)
public void spawn(
@Param(description = "The entity to spawn")

View File

@@ -21,7 +21,8 @@ package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.pregenerator.LazyPregenerator;
import art.arcane.iris.core.pregenerator.TurboPregenerator;
import art.arcane.iris.core.pregenerator.TurboPregenerator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
@@ -70,6 +71,11 @@ public class CommandTurboPregen implements DirectorExecutor {
sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example.");
}
PlatformChunkGenerator platform = IrisToolbelt.access(world);
if (platform != null) {
IrisToolbelt.applyPregenPerformanceProfile(platform.getEngine());
}
TurboPregenerator.TurboPregenJob pregenJob = TurboPregenerator.TurboPregenJob.builder()
.world(worldName)
.radiusBlocks(radius)

View File

@@ -0,0 +1,576 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.engine.object.annotations.Snippet;
import art.arcane.iris.util.common.data.registry.KeyedRegistry;
import art.arcane.iris.util.common.data.registry.RegistryUtil;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.bukkit.NamespacedKey;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class PackValidationSVC implements IrisService {
private final Map<Class<?>, Map<String, Field>> fieldCache = new ConcurrentHashMap<>();
private final Map<Class<?>, Boolean> keyedTypeCache = new ConcurrentHashMap<>();
@Override
public void onEnable() {
IrisSettings.IrisSettingsGeneral general = IrisSettings.get().getGeneral();
if (!general.isValidatePacksOnStartup()) {
return;
}
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packFolders = packsFolder.listFiles(File::isDirectory);
if (packFolders == null || packFolders.length == 0) {
Iris.info("Startup pack validation skipped: no pack folders found.");
return;
}
int maxLoggedIssues = Math.max(1, general.getMaxPackValidationErrorsPerPack());
int totalPacks = 0;
int totalFiles = 0;
int totalErrors = 0;
int packsWithErrors = 0;
long started = System.currentTimeMillis();
Iris.info("Startup pack validation started for %d pack(s).", packFolders.length);
for (File packFolder : packFolders) {
totalPacks++;
PackValidationReport report = validatePack(packFolder, maxLoggedIssues);
totalFiles += report.filesChecked;
totalErrors += report.errorCount;
if (report.errorCount > 0) {
packsWithErrors++;
Iris.error("Pack \"%s\" has %d validation issue(s) across %d file(s).", report.packName, report.errorCount, report.filesChecked);
for (String issue : report.sampleIssues) {
Iris.error(" - %s", issue);
}
int hiddenIssues = report.errorCount - report.sampleIssues.size();
if (hiddenIssues > 0) {
Iris.error(" - ... %d additional issue(s) not shown", hiddenIssues);
}
}
}
long elapsed = System.currentTimeMillis() - started;
if (totalErrors == 0) {
Iris.info("Startup pack validation finished: %d pack(s), %d file(s), no issues (%dms).", totalPacks, totalFiles, elapsed);
return;
}
Iris.error("Startup pack validation finished: %d issue(s) in %d pack(s), %d file(s) checked (%dms).", totalErrors, packsWithErrors, totalFiles, elapsed);
if (general.isStopStartupOnPackValidationFailure()) {
throw new IllegalStateException("Pack validation failed with " + totalErrors + " issue(s).");
}
}
@Override
public void onDisable() {
}
private PackValidationReport validatePack(File packFolder, int maxLoggedIssues) {
PackValidationReport report = new PackValidationReport(packFolder.getName(), maxLoggedIssues);
IrisData data = IrisData.get(packFolder);
Collection<ResourceLoader<? extends IrisRegistrant>> loaders = data.getLoaders().values();
for (ResourceLoader<? extends IrisRegistrant> loader : loaders) {
Class<?> rootType = loader.getObjectClass();
if (rootType == null) {
continue;
}
List<File> folders = loader.getFolders();
for (File folder : folders) {
validateFolder(data, packFolder, folder, rootType, report);
}
}
validateSnippetFolders(data, packFolder, report);
return report;
}
private void validateSnippetFolders(IrisData data, File packFolder, PackValidationReport report) {
File snippetRoot = new File(packFolder, "snippet");
if (!snippetRoot.isDirectory()) {
return;
}
Map<String, Class<?>> snippetTypes = new HashMap<>();
for (Class<?> snippetType : data.resolveSnippets()) {
Snippet snippet = snippetType.getDeclaredAnnotation(Snippet.class);
if (snippet == null) {
continue;
}
snippetTypes.put(snippet.value(), snippetType);
}
for (Map.Entry<String, Class<?>> entry : snippetTypes.entrySet()) {
File typeFolder = new File(snippetRoot, entry.getKey());
if (!typeFolder.isDirectory()) {
continue;
}
validateFolder(data, packFolder, typeFolder, entry.getValue(), report);
}
}
private void validateFolder(IrisData data, File packFolder, File folder, Class<?> rootType, PackValidationReport report) {
List<File> jsonFiles = new ArrayList<>();
collectJsonFiles(folder, jsonFiles);
for (File jsonFile : jsonFiles) {
report.filesChecked++;
validateJsonFile(data, packFolder, jsonFile, rootType, report);
}
}
private void collectJsonFiles(File folder, List<File> output) {
File[] files = folder.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
collectJsonFiles(file, output);
continue;
}
if (file.isFile() && file.getName().endsWith(".json")) {
output.add(file);
}
}
}
private void validateJsonFile(IrisData data, File packFolder, File file, Class<?> rootType, PackValidationReport report) {
String content;
try {
content = IO.readAll(file);
} catch (Throwable e) {
report.addIssue(packFolder, file, "$", "Unable to read file: " + simpleMessage(e));
return;
}
JSONObject object;
try {
object = new JSONObject(content);
} catch (Throwable e) {
report.addIssue(packFolder, file, "$", "Invalid JSON syntax: " + simpleMessage(e));
return;
}
try {
Object decoded = data.getGson().fromJson(content, rootType);
if (decoded == null) {
report.addIssue(packFolder, file, "$", "Decoded value is null for root type " + rootType.getSimpleName());
}
} catch (Throwable e) {
report.addIssue(packFolder, file, "$", "Deserializer rejected file for " + rootType.getSimpleName() + ": " + simpleMessage(e));
}
validateObject(packFolder, file, "$", object, rootType, report);
}
private void validateObject(File packFolder, File file, String path, JSONObject object, Class<?> type, PackValidationReport report) {
if (type == null) {
return;
}
Map<String, Field> fields = getSerializableFields(type);
for (String key : object.keySet()) {
Field field = fields.get(key);
String keyPath = path + "." + key;
if (field == null) {
report.addIssue(packFolder, file, keyPath, "Unknown or misplaced key for type " + type.getSimpleName());
continue;
}
Object value = object.get(key);
validateValue(packFolder, file, keyPath, value, field.getType(), field.getGenericType(), field, report);
}
}
private void validateValue(File packFolder, File file, String path, Object value, Class<?> expectedType, Type genericType, Field sourceField, PackValidationReport report) {
Class<?> normalizedType = normalizeType(expectedType);
if (value == JSONObject.NULL) {
if (normalizedType.isPrimitive()) {
report.addIssue(packFolder, file, path, "Null value is not allowed for primitive type " + normalizedType.getSimpleName());
}
return;
}
if (Collection.class.isAssignableFrom(normalizedType)) {
validateCollection(packFolder, file, path, value, genericType, sourceField, report);
return;
}
if (normalizedType.isArray()) {
validateArray(packFolder, file, path, value, normalizedType, report);
return;
}
if (Map.class.isAssignableFrom(normalizedType)) {
validateMap(packFolder, file, path, value, genericType, report);
return;
}
if (normalizedType.isEnum()) {
validateEnum(packFolder, file, path, value, normalizedType, report);
return;
}
if (isKeyedType(normalizedType)) {
validateKeyed(packFolder, file, path, value, normalizedType, report);
return;
}
if (normalizedType == String.class) {
if (!(value instanceof String)) {
report.addIssue(packFolder, file, path, "Expected string value");
}
return;
}
if (normalizedType == Boolean.class) {
if (!(value instanceof Boolean)) {
report.addIssue(packFolder, file, path, "Expected boolean value");
}
return;
}
if (Number.class.isAssignableFrom(normalizedType)) {
if (!(value instanceof Number)) {
report.addIssue(packFolder, file, path, "Expected numeric value");
}
return;
}
if (normalizedType == Character.class) {
if (!(value instanceof String text) || text.length() != 1) {
report.addIssue(packFolder, file, path, "Expected single-character string value");
}
return;
}
Snippet snippet = normalizedType.getDeclaredAnnotation(Snippet.class);
if (snippet != null) {
if (value instanceof String reference) {
if (!reference.startsWith("snippet/")) {
report.addIssue(packFolder, file, path, "Snippet reference must start with snippet/");
}
return;
}
if (value instanceof JSONObject jsonObject) {
validateObject(packFolder, file, path, jsonObject, normalizedType, report);
return;
}
report.addIssue(packFolder, file, path, "Snippet value must be an object or snippet reference string");
return;
}
if (value instanceof JSONObject jsonObject) {
if (shouldValidateNestedObject(normalizedType)) {
validateObject(packFolder, file, path, jsonObject, normalizedType, report);
}
return;
}
if (value instanceof JSONArray) {
report.addIssue(packFolder, file, path, "Unexpected array value for type " + normalizedType.getSimpleName());
return;
}
if (shouldValidateNestedObject(normalizedType)) {
report.addIssue(packFolder, file, path, "Expected object value for type " + normalizedType.getSimpleName());
}
}
private void validateCollection(File packFolder, File file, String path, Object value, Type genericType, Field sourceField, PackValidationReport report) {
if (!(value instanceof JSONArray array)) {
report.addIssue(packFolder, file, path, "Expected array value");
return;
}
Class<?> elementType = resolveCollectionElementType(genericType, sourceField);
if (elementType == null || elementType == Object.class) {
return;
}
for (int i = 0; i < array.length(); i++) {
Object element = array.get(i);
validateValue(packFolder, file, path + "[" + i + "]", element, elementType, null, null, report);
}
}
private void validateArray(File packFolder, File file, String path, Object value, Class<?> expectedType, PackValidationReport report) {
if (!(value instanceof JSONArray array)) {
report.addIssue(packFolder, file, path, "Expected array value");
return;
}
Class<?> componentType = normalizeType(expectedType.getComponentType());
for (int i = 0; i < array.length(); i++) {
Object element = array.get(i);
validateValue(packFolder, file, path + "[" + i + "]", element, componentType, null, null, report);
}
}
private void validateMap(File packFolder, File file, String path, Object value, Type genericType, PackValidationReport report) {
if (!(value instanceof JSONObject object)) {
report.addIssue(packFolder, file, path, "Expected object value");
return;
}
Class<?> valueType = resolveMapValueType(genericType);
if (valueType == null || valueType == Object.class) {
return;
}
for (String key : object.keySet()) {
Object child = object.get(key);
validateValue(packFolder, file, path + "." + key, child, valueType, null, null, report);
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void validateEnum(File packFolder, File file, String path, Object value, Class<?> expectedType, PackValidationReport report) {
if (!(value instanceof String text)) {
report.addIssue(packFolder, file, path, "Expected enum string for " + expectedType.getSimpleName());
return;
}
try {
Enum.valueOf((Class<? extends Enum>) expectedType, text);
} catch (Throwable e) {
report.addIssue(packFolder, file, path, "Unknown enum value \"" + text + "\" for " + expectedType.getSimpleName());
}
}
@SuppressWarnings("unchecked")
private void validateKeyed(File packFolder, File file, String path, Object value, Class<?> expectedType, PackValidationReport report) {
if (!(value instanceof String text)) {
report.addIssue(packFolder, file, path, "Expected namespaced key string for " + expectedType.getSimpleName());
return;
}
NamespacedKey key = NamespacedKey.fromString(text);
if (key == null) {
report.addIssue(packFolder, file, path, "Invalid namespaced key format \"" + text + "\"");
return;
}
KeyedRegistry<Object> registry;
try {
registry = RegistryUtil.lookup((Class<Object>) expectedType);
} catch (Throwable e) {
report.addIssue(packFolder, file, path, "Unable to resolve keyed registry for " + expectedType.getSimpleName() + ": " + simpleMessage(e));
return;
}
if (registry.isEmpty()) {
return;
}
if (registry.get(key) == null) {
report.addIssue(packFolder, file, path, "Unknown registry key \"" + text + "\" for " + expectedType.getSimpleName());
}
}
private Map<String, Field> getSerializableFields(Class<?> type) {
return fieldCache.computeIfAbsent(type, this::buildSerializableFields);
}
private Map<String, Field> buildSerializableFields(Class<?> type) {
Map<String, Field> fields = new LinkedHashMap<>();
Class<?> cursor = type;
while (cursor != null && cursor != Object.class) {
Field[] declared = cursor.getDeclaredFields();
for (Field field : declared) {
int modifiers = field.getModifiers();
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) || field.isSynthetic()) {
continue;
}
fields.putIfAbsent(field.getName(), field);
}
cursor = cursor.getSuperclass();
}
return fields;
}
private Class<?> resolveCollectionElementType(Type genericType, Field sourceField) {
if (sourceField != null) {
ArrayType arrayType = sourceField.getDeclaredAnnotation(ArrayType.class);
if (arrayType != null && arrayType.type() != Object.class) {
return arrayType.type();
}
}
if (genericType instanceof ParameterizedType parameterizedType) {
Type[] arguments = parameterizedType.getActualTypeArguments();
if (arguments.length > 0) {
Class<?> resolved = resolveType(arguments[0]);
if (resolved != null) {
return resolved;
}
}
}
return Object.class;
}
private Class<?> resolveMapValueType(Type genericType) {
if (genericType instanceof ParameterizedType parameterizedType) {
Type[] arguments = parameterizedType.getActualTypeArguments();
if (arguments.length > 1) {
Class<?> resolved = resolveType(arguments[1]);
if (resolved != null) {
return resolved;
}
}
}
return Object.class;
}
private Class<?> resolveType(Type type) {
if (type instanceof Class<?> clazz) {
return normalizeType(clazz);
}
if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class<?> clazz) {
return normalizeType(clazz);
}
if (type instanceof WildcardType wildcardType) {
Type[] upperBounds = wildcardType.getUpperBounds();
if (upperBounds.length > 0) {
return resolveType(upperBounds[0]);
}
}
if (type instanceof GenericArrayType genericArrayType) {
Class<?> component = resolveType(genericArrayType.getGenericComponentType());
if (component != null) {
return normalizeType(Array.newInstance(component, 0).getClass());
}
}
return null;
}
private Class<?> normalizeType(Class<?> type) {
if (type == null) {
return Object.class;
}
if (!type.isPrimitive()) {
return type;
}
if (type == int.class) return Integer.class;
if (type == long.class) return Long.class;
if (type == double.class) return Double.class;
if (type == float.class) return Float.class;
if (type == short.class) return Short.class;
if (type == byte.class) return Byte.class;
if (type == boolean.class) return Boolean.class;
if (type == char.class) return Character.class;
return type;
}
private boolean shouldValidateNestedObject(Class<?> type) {
String name = type.getName();
return name.startsWith("art.arcane.iris.");
}
private boolean isKeyedType(Class<?> type) {
return keyedTypeCache.computeIfAbsent(type, this::resolveKeyedType);
}
@SuppressWarnings("unchecked")
private boolean resolveKeyedType(Class<?> type) {
try {
KeyedRegistry<Object> registry = RegistryUtil.lookup((Class<Object>) type);
return !registry.isEmpty();
} catch (Throwable ignored) {
return false;
}
}
private String simpleMessage(Throwable throwable) {
String message = throwable.getMessage();
if (message == null || message.trim().isEmpty()) {
return throwable.getClass().getSimpleName();
}
return throwable.getClass().getSimpleName() + ": " + message;
}
private String relativePath(File packFolder, File file) {
try {
Path packPath = packFolder.toPath();
Path filePath = file.toPath();
return packPath.relativize(filePath).toString().replace(File.separatorChar, '/');
} catch (Throwable ignored) {
return file.getPath();
}
}
private final class PackValidationReport {
private final String packName;
private final int maxIssues;
private final List<String> sampleIssues;
private int filesChecked;
private int errorCount;
private PackValidationReport(String packName, int maxIssues) {
this.packName = packName;
this.maxIssues = maxIssues;
this.sampleIssues = new ArrayList<>();
}
private void addIssue(File packFolder, File file, String path, String message) {
errorCount++;
if (sampleIssues.size() >= maxIssues) {
return;
}
String relative = relativePath(packFolder, file);
sampleIssues.add(relative + " " + path + " -> " + message);
}
}
}

View File

@@ -23,10 +23,13 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.link.FoliaWorldsLink;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.IrisNoisemapPrebakePipeline;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.exceptions.IrisException;
@@ -147,6 +150,7 @@ public class IrisCreator {
if (!studio() || benchmark) {
Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name()));
}
prebakeNoisemapsBeforeWorldCreate(d);
AtomicDouble pp = new AtomicDouble(0);
O<Boolean> done = new O<>();
@@ -279,6 +283,30 @@ public class IrisCreator {
return world;
}
private void prebakeNoisemapsBeforeWorldCreate(IrisDimension dimension) {
IrisSettings.IrisSettingsPregen pregenSettings = IrisSettings.get().getPregen();
if (!pregenSettings.isStartupNoisemapPrebake()) {
return;
}
try {
File targetDataFolder = new File(Bukkit.getWorldContainer(), name());
if (studio() && !benchmark) {
IrisData studioData = dimension.getLoader();
if (studioData != null) {
targetDataFolder = studioData.getDataFolder();
}
}
IrisData targetData = IrisData.get(targetDataFolder);
SeedManager seedManager = new SeedManager(seed());
IrisNoisemapPrebakePipeline.prebake(targetData, seedManager, name(), dimension.getLoadKey());
} catch (Throwable throwable) {
Iris.warn("Failed pre-create noisemap pre-bake for " + name() + "/" + dimension.getLoadKey() + ": " + throwable.getMessage());
Iris.reportError(throwable);
}
}
private Location resolveStudioEntryLocation(World world) {
CompletableFuture<Location> locationFuture = J.sfut(() -> {
Location spawnLocation = world.getSpawnLocation();

View File

@@ -48,6 +48,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -60,6 +61,7 @@ public class IrisToolbelt {
private static final Map<String, AtomicInteger> worldMaintenanceDepth = new ConcurrentHashMap<>();
private static final Map<String, AtomicInteger> worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>();
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
private static final AtomicBoolean PREGEN_PROFILE_JVM_HINT_LOGGED = new AtomicBoolean(false);
/**
* Will find / download / search for the dimension or return null
@@ -230,10 +232,51 @@ public class IrisToolbelt {
* @return the pregenerator job (already started)
*/
public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) {
applyPregenPerformanceProfile(engine);
boolean useCachedWrapper = cached && engine != null && !J.isFolia();
return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine);
}
public static boolean applyPregenPerformanceProfile() {
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
if (!pregen.isEnablePregenPerformanceProfile()) {
return false;
}
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, Math.max(1, pregen.getPregenProfileNoiseCacheSize()));
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
performance.setNoiseCacheSize(targetNoiseCacheSize);
changed = true;
}
if (pregen.isPregenProfileEnableFastCache() && !fastCacheEnabledBefore) {
System.setProperty("iris.cache.fast", "true");
changed = true;
}
if (pregen.isPregenProfileLogJvmHints()
&& pregen.isPregenProfileEnableFastCache()
&& PREGEN_PROFILE_JVM_HINT_LOGGED.compareAndSet(false, true)
&& !fastCacheEnabledBefore) {
Iris.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true");
}
return changed;
}
public static void applyPregenPerformanceProfile(Engine engine) {
boolean changed = applyPregenPerformanceProfile();
if (changed && engine != null) {
engine.hotloadComplex();
Iris.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
}
}
/**
* Start a pregenerator task. If the supplied generator is headless, headless mode is used,
* otherwise Hybrid mode is used.

View File

@@ -21,6 +21,7 @@ package art.arcane.iris.engine;
import com.google.common.util.concurrent.AtomicDouble;
import com.google.gson.Gson;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
import art.arcane.iris.core.gui.PregeneratorJob;
@@ -91,6 +92,7 @@ public class IrisEngine implements Engine {
private final int art;
private final AtomicCache<IrisEngineData> engineData = new AtomicCache<>();
private final AtomicBoolean cleaning;
private final AtomicBoolean noisemapPrebakeRunning;
private final ChronoLatch cleanLatch;
private final SeedManager seedManager;
private CompletableFuture<Long> hash32;
@@ -127,6 +129,7 @@ public class IrisEngine implements Engine {
mantle = new IrisEngineMantle(this);
context = new IrisContext(this);
cleaning = new AtomicBoolean(false);
noisemapPrebakeRunning = new AtomicBoolean(false);
execution = getData().getEnvironment().with(this);
if (studio) {
getData().dump();
@@ -195,6 +198,7 @@ public class IrisEngine implements Engine {
.toArray(File[]::new);
hash32.complete(IO.hashRecursive(roots));
});
scheduleStartupNoisemapPrebake();
} catch (Throwable e) {
Iris.error("FAILED TO SETUP ENGINE!");
e.printStackTrace();
@@ -211,6 +215,26 @@ public class IrisEngine implements Engine {
mode = getDimension().getMode().create(this);
}
private void scheduleStartupNoisemapPrebake() {
if (!IrisSettings.get().getPregen().isStartupNoisemapPrebake()) {
return;
}
if (!noisemapPrebakeRunning.compareAndSet(false, true)) {
return;
}
J.a(() -> {
try {
IrisNoisemapPrebakePipeline.prebake(this);
} catch (Throwable throwable) {
Iris.reportError(throwable);
} finally {
noisemapPrebakeRunning.set(false);
}
});
}
@Override
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateMatter(x, z, multicore, context);

View File

@@ -0,0 +1,973 @@
package art.arcane.iris.engine;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import org.bukkit.Bukkit;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BooleanSupplier;
public final class IrisNoisemapPrebakePipeline {
private static final long[] NO_SEEDS = new long[0];
private static final long STARTUP_PROGRESS_INTERVAL_MS = Long.getLong("iris.prebake.progress.interval", 30000L);
private static final int STATE_VERSION = 1;
private static final String STATE_FILE = "noisemap-prebake.state";
private static final AtomicInteger STARTUP_WORKER_SEQUENCE = new AtomicInteger();
private static final ConcurrentHashMap<Class<?>, Field[]> FIELD_CACHE = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, Boolean> SKIP_ONCE = new ConcurrentHashMap<>();
private static final Set<String> PREBAKE_LOADERS = Set.of(
"dimensions",
"regions",
"biomes",
"generators",
"caves",
"ravines",
"jigsaw-structures",
"mods",
"expressions"
);
private static final Set<String> PREBAKE_STATE_FOLDERS = Set.of(
"dimensions",
"regions",
"biomes",
"generators",
"caves",
"ravines",
"jigsaw-structures",
"jigsaw-pools",
"jigsaw-pieces",
"mods",
"expressions",
"scripts",
"images",
"snippet"
);
private IrisNoisemapPrebakePipeline() {
}
public static void prebakeInstalledPacksAtStartup() {
IrisSettings.IrisSettingsPregen settings = IrisSettings.get().getPregen();
List<PrebakeTarget> targets = collectStartupTargets();
if (targets.isEmpty()) {
Iris.info("Startup noisemap pre-bake skipped (no installed or self-contained packs found).");
return;
}
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
long startupSeed = dynamicStartupSeed();
SeedManager seedManager = new SeedManager(startupSeed);
int targetCount = targets.size();
int workerCount = Math.min(targetCount, Math.max(1, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
ExecutorService workers = Executors.newFixedThreadPool(workerCount, new StartupPrebakeThreadFactory());
CompletionService<StartupTargetResult> completion = new ExecutorCompletionService<>(workers);
StartupProgress progress = new StartupProgress(targetCount);
Iris.info("Startup pack noisemap pre-bake running in background targets=" + targetCount + " workers=" + workerCount + " seed=" + startupSeed);
for (PrebakeTarget target : targets) {
completion.submit(() -> new StartupTargetResult(target.label,
prebake(IrisData.get(target.folder), seedManager, "startup", target.label, () -> false, false, false, progress)));
}
int completedTargets = 0;
long lastProgress = System.currentTimeMillis();
while (completedTargets < targetCount) {
try {
Future<StartupTargetResult> future = completion.poll(2, TimeUnit.SECONDS);
if (future != null) {
StartupTargetResult result = future.get();
completedTargets++;
progress.onTargetCompleted(result.result);
}
} catch (Throwable throwable) {
completedTargets++;
progress.onTargetFailed();
Iris.reportError(throwable);
}
long now = System.currentTimeMillis();
if (completedTargets < targetCount && now - lastProgress >= STARTUP_PROGRESS_INTERVAL_MS) {
logStartupProgress(progress, stopwatch);
lastProgress = now;
}
}
workers.shutdownNow();
Iris.info("Startup pack noisemap pre-bake scan completed targets="
+ targetCount
+ " executed="
+ progress.executedTargets.get()
+ " unchanged="
+ progress.unchangedTargets.get()
+ " failed="
+ progress.failedTargets.get()
+ " styles="
+ progress.stylesFinished.get()
+ "/"
+ progress.stylesDiscovered.get()
+ " seed="
+ startupSeed
+ " in "
+ Form.duration(stopwatch.getMilliseconds(), 2));
}
public static int clearInstalledPackPrebakeStates() {
int cleared = 0;
List<PrebakeTarget> targets = collectStartupTargets();
for (PrebakeTarget target : targets) {
File state = stateFile(target.folder);
if (state.exists() && state.delete()) {
cleared++;
}
}
return cleared;
}
private static void logStartupProgress(StartupProgress progress, PrecisionStopwatch stopwatch) {
int targetCount = progress.targetTotal;
if (targetCount <= 0) {
return;
}
int completedTargets = progress.targetsCompleted.get();
int discoveredStyles = progress.stylesDiscovered.get();
int finishedStyles = progress.stylesFinished.get();
int executedTargets = progress.executedTargets.get();
int unchangedTargets = progress.unchangedTargets.get();
int failedTargets = progress.failedTargets.get();
double elapsed = stopwatch.getMilliseconds();
if (discoveredStyles > 0) {
int remainingStyles = Math.max(0, discoveredStyles - finishedStyles);
int percent = (int) Math.round((finishedStyles * 100D) / discoveredStyles);
String eta = "estimating";
if (finishedStyles > 0) {
long etaMillis = Math.max(0L, Math.round((elapsed / finishedStyles) * remainingStyles));
eta = Form.duration(etaMillis, 2);
}
Iris.info("Startup noisemap pre-bake progress "
+ percent
+ "% styles="
+ finishedStyles
+ "/"
+ discoveredStyles
+ " targets="
+ completedTargets
+ "/"
+ targetCount
+ " remaining="
+ remainingStyles
+ " executed="
+ executedTargets
+ " unchanged="
+ unchangedTargets
+ " failed="
+ failedTargets
+ " elapsed="
+ Form.duration(elapsed, 2)
+ " eta="
+ eta);
return;
}
int remainingTargets = Math.max(0, targetCount - completedTargets);
int percent = (int) Math.round((completedTargets * 100D) / targetCount);
String eta = "estimating";
if (completedTargets > 0) {
long etaMillis = Math.max(0L, Math.round((elapsed / completedTargets) * remainingTargets));
eta = Form.duration(etaMillis, 2);
}
Iris.info("Startup noisemap pre-bake progress "
+ percent
+ "% targets="
+ completedTargets
+ "/"
+ targetCount
+ " remaining="
+ remainingTargets
+ " executed="
+ executedTargets
+ " unchanged="
+ unchangedTargets
+ " failed="
+ failedTargets
+ " elapsed="
+ Form.duration(elapsed, 2)
+ " eta="
+ eta);
}
public static long dynamicStartupSeed() {
if (!Bukkit.getWorlds().isEmpty()) {
return Bukkit.getWorlds().get(0).getSeed();
}
String configuredSeed = ServerProperties.DATA.getProperty("level-seed", "").trim();
if (!configuredSeed.isEmpty()) {
try {
return Long.parseLong(configuredSeed);
} catch (NumberFormatException ignored) {
return mixSeed(0x9E3779B97F4A7C15L, configuredSeed.hashCode());
}
}
return mixSeed(0x94D049BB133111EBL, ServerProperties.LEVEL_NAME.hashCode());
}
public static void prebake(Engine engine) {
if (engine == null || engine.isClosed()) {
return;
}
if (!IrisSettings.get().getPregen().isStartupNoisemapPrebake()) {
return;
}
prebake(engine.getData(),
engine.getSeedManager(),
engine.getWorld().name(),
engine.getDimension().getLoadKey(),
engine::isClosed,
false,
true,
null);
}
public static void prebake(IrisData data, SeedManager seedManager, String worldName, String dimensionKey) {
if (!IrisSettings.get().getPregen().isStartupNoisemapPrebake()) {
return;
}
prebake(data, seedManager, worldName, dimensionKey, () -> false, true, true, null);
}
public static void prebakeForced(IrisData data, SeedManager seedManager, String worldName, String dimensionKey) {
prebake(data, seedManager, worldName, dimensionKey, () -> false, false, true, null);
}
private static PrebakeRunResult prebake(IrisData data,
SeedManager seedManager,
String worldName,
String dimensionKey,
BooleanSupplier shouldAbort,
boolean primeSkipOnce,
boolean logResult,
StartupProgress progress) {
if (data == null || seedManager == null) {
return PrebakeRunResult.SKIPPED;
}
if (shouldAbort != null && shouldAbort.getAsBoolean()) {
return PrebakeRunResult.SKIPPED;
}
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
String safeWorldName = worldName == null ? "unknown" : worldName;
String safeDimensionKey = dimensionKey == null ? "unknown" : dimensionKey;
boolean exhaustive = dynamicExhaustivePrebakeMode();
int fallbackCacheSize = dynamicFallbackCacheSize();
String key = prebakeKey(data, seedManager, safeDimensionKey, exhaustive, fallbackCacheSize);
if (SKIP_ONCE.remove(key) != null) {
if (logResult) {
Iris.info("Startup noisemap pre-bake skipped for " + safeWorldName + "/" + safeDimensionKey + " (already pre-baked before engine init).");
}
return PrebakeRunResult.SKIPPED;
}
String stateToken = prebakeStateToken(data, seedManager, exhaustive, fallbackCacheSize);
if (isCurrentState(data, stateToken)) {
if (primeSkipOnce) {
SKIP_ONCE.put(key, Boolean.TRUE);
}
if (logResult) {
Iris.info("Startup noisemap pre-bake skipped for " + safeWorldName + "/" + safeDimensionKey + " (unchanged pack state).");
}
return PrebakeRunResult.UNCHANGED;
}
List<StyleReference> styles = collectStyles(data, fallbackCacheSize, progress);
int styleCount = styles.size();
if (styleCount == 0) {
writeCurrentState(data, stateToken);
if (primeSkipOnce) {
SKIP_ONCE.put(key, Boolean.TRUE);
}
if (logResult) {
Iris.info("Startup noisemap pre-bake skipped for " + safeWorldName + "/" + safeDimensionKey + " (no cacheable styles found).");
}
return PrebakeRunResult.SKIPPED;
}
long[] domainSeeds = collectDomainSeeds(seedManager);
if (domainSeeds.length == 0) {
if (logResult) {
Iris.warn("Startup noisemap pre-bake skipped for " + safeWorldName + "/" + safeDimensionKey + " (no seed domains).");
}
return PrebakeRunResult.SKIPPED;
}
AtomicInteger prebakedStyles = new AtomicInteger();
AtomicInteger prebakedVariants = new AtomicInteger();
AtomicInteger failures = new AtomicInteger();
BurstExecutor burst = MultiBurst.burst.burst(styleCount);
for (StyleReference reference : styles) {
burst.queue(() -> {
if (shouldAbort != null && shouldAbort.getAsBoolean()) {
if (progress != null) {
progress.onStyleFinished();
}
return;
}
try {
int baked = prebakeStyle(reference, data, domainSeeds, exhaustive, fallbackCacheSize);
if (baked <= 0) {
return;
}
prebakedStyles.incrementAndGet();
prebakedVariants.addAndGet(baked);
} catch (Throwable throwable) {
failures.incrementAndGet();
Iris.reportError(throwable);
} finally {
if (progress != null) {
progress.onStyleFinished();
}
}
});
}
burst.complete();
if (failures.get() == 0) {
writeCurrentState(data, stateToken);
if (primeSkipOnce) {
SKIP_ONCE.put(key, Boolean.TRUE);
}
}
if (logResult) {
Iris.info("Startup noisemap pre-bake completed for "
+ safeWorldName
+ "/"
+ safeDimensionKey
+ " styles="
+ styleCount
+ " prebaked="
+ prebakedStyles.get()
+ " variants="
+ prebakedVariants.get()
+ " failures="
+ failures.get()
+ " mode="
+ (exhaustive ? "exhaustive" : "targeted")
+ " fallback="
+ fallbackCacheSize
+ " in "
+ Form.duration(stopwatch.getMilliseconds(), 2));
}
return new PrebakeRunResult(true, false, failures.get());
}
private static boolean dynamicExhaustivePrebakeMode() {
int cores = Runtime.getRuntime().availableProcessors();
long memoryGiB = Runtime.getRuntime().maxMemory() / (1024L * 1024L * 1024L);
return cores >= 24 && memoryGiB >= 64;
}
private static int dynamicFallbackCacheSize() {
int cores = Runtime.getRuntime().availableProcessors();
long memoryGiB = Runtime.getRuntime().maxMemory() / (1024L * 1024L * 1024L);
if (memoryGiB >= 24 && cores >= 12) {
return 64;
}
if (memoryGiB >= 16 && cores >= 8) {
return 48;
}
if (memoryGiB >= 8 && cores >= 6) {
return 40;
}
if (memoryGiB >= 4 && cores >= 4) {
return 32;
}
return 24;
}
private static String prebakeStateToken(IrisData data, SeedManager seedManager, boolean exhaustive, int fallbackCacheSize) {
File[] roots = prebakeStateRoots(data);
long stateHash = roots.length == 0 ? 0L : IO.hashRecursive(roots);
long seedHash = hashSeeds(collectDomainSeeds(seedManager));
return STATE_VERSION
+ "|"
+ Long.toUnsignedString(stateHash)
+ "|"
+ Long.toUnsignedString(seedHash)
+ "|"
+ (exhaustive ? "X" : "T")
+ "|"
+ fallbackCacheSize;
}
private static File[] prebakeStateRoots(IrisData data) {
File dataFolder = data.getDataFolder();
List<File> roots = new ArrayList<>();
for (String folder : PREBAKE_STATE_FOLDERS) {
File candidate = new File(dataFolder, folder);
if (candidate.exists()) {
roots.add(candidate);
}
}
roots.sort(Comparator.comparing(File::getName));
return roots.toArray(new File[0]);
}
private static long hashSeeds(long[] seeds) {
long hash = 1125899906842597L;
for (long seed : seeds) {
hash = (hash * 31L) ^ seed;
}
return hash;
}
private static File stateFile(IrisData data) {
return stateFile(data.getDataFolder());
}
private static File stateFile(File dataFolder) {
return new File(new File(dataFolder, ".cache"), STATE_FILE);
}
private static boolean isCurrentState(IrisData data, String token) {
File stateFile = stateFile(data);
if (!stateFile.exists()) {
return false;
}
Properties properties = new Properties();
try (FileInputStream input = new FileInputStream(stateFile)) {
properties.load(input);
} catch (IOException e) {
return false;
}
String previous = properties.getProperty("token");
return token.equals(previous);
}
private static void writeCurrentState(IrisData data, String token) {
File stateFile = stateFile(data);
File parent = stateFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
return;
}
Properties properties = new Properties();
properties.setProperty("token", token);
properties.setProperty("updated", Long.toString(System.currentTimeMillis()));
try (FileOutputStream output = new FileOutputStream(stateFile)) {
properties.store(output, "Iris noisemap prebake state");
} catch (IOException ignored) {
}
}
private static String prebakeKey(IrisData data, SeedManager seedManager, String dimensionKey, boolean exhaustive, int fallbackCacheSize) {
return data.getDataFolder().getAbsolutePath()
+ "|"
+ seedManager.getSeed()
+ "|"
+ dimensionKey
+ "|"
+ fallbackCacheSize
+ "|"
+ (exhaustive ? "X" : "T");
}
private static int prebakeStyle(StyleReference reference, IrisData data, long[] domainSeeds, boolean exhaustive, int fallbackCacheSize) {
IrisGeneratorStyle style = reference.style;
if (!isCacheable(style, fallbackCacheSize)) {
return 0;
}
int styleHash = reference.hash;
if (exhaustive) {
int variants = 0;
for (int i = 0; i < domainSeeds.length; i++) {
long mixedSeed = mixSeed(domainSeeds[i], styleHash + (i * 131));
CNG baked = style.createForPrebake(new RNG(mixedSeed), data, fallbackCacheSize);
if (baked != null) {
variants++;
}
}
return variants;
}
int index = Math.floorMod(styleHash, domainSeeds.length);
long primarySeed = mixSeed(domainSeeds[index], styleHash);
int variants = 0;
CNG primary = style.createForPrebake(new RNG(primarySeed), data, fallbackCacheSize);
if (primary != null) {
variants++;
}
return variants;
}
private static boolean isCacheable(IrisGeneratorStyle style, int fallbackCacheSize) {
if (style == null) {
return false;
}
return style.getCacheSize() > 0 || fallbackCacheSize > 0;
}
private static List<StyleReference> collectStyles(IrisData data, int fallbackCacheSize, StartupProgress progress) {
LinkedHashMap<Integer, StyleReference> styles = new LinkedHashMap<>();
Collection<ResourceLoader<? extends IrisRegistrant>> loaders = data.getLoaders().values();
for (ResourceLoader<? extends IrisRegistrant> loader : loaders) {
if (!PREBAKE_LOADERS.contains(loader.getFolderName())) {
continue;
}
String[] keys = loader.getPossibleKeys();
for (String key : keys) {
IrisRegistrant registrant = loader.load(key, false);
if (registrant == null) {
continue;
}
String rootPath = loader.getFolderName() + ":" + key;
collectFromObject(registrant, rootPath, styles, fallbackCacheSize, progress);
}
}
return new ArrayList<>(styles.values());
}
private static void collectFromObject(Object root, String rootPath, LinkedHashMap<Integer, StyleReference> styles, int fallbackCacheSize, StartupProgress progress) {
if (root == null) {
return;
}
IdentityHashMap<Object, Boolean> visited = new IdentityHashMap<>();
ArrayDeque<Node> queue = new ArrayDeque<>();
queue.add(new Node(root, rootPath));
while (!queue.isEmpty()) {
Node node = queue.removeFirst();
Object value = node.value;
if (value == null) {
continue;
}
if (value instanceof IrisGeneratorStyle style) {
if (isCacheable(style, fallbackCacheSize)) {
int styleSignature = style.prebakeSignature();
if (styles.putIfAbsent(styleSignature, new StyleReference(style, styleSignature)) == null && progress != null) {
progress.onStylesDiscovered(1);
}
}
}
Class<?> type = value.getClass();
if (isLeafType(type)) {
continue;
}
if (visited.put(value, Boolean.TRUE) != null) {
continue;
}
if (type.isArray()) {
int length = Array.getLength(value);
for (int i = 0; i < length; i++) {
Object element = Array.get(value, i);
queue.addLast(new Node(element, node.path + "[" + i + "]"));
}
continue;
}
if (value instanceof Iterable<?> iterable) {
int index = 0;
for (Object element : iterable) {
queue.addLast(new Node(element, node.path + "[" + index + "]"));
index++;
}
continue;
}
if (value instanceof Map<?, ?> map) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object key = entry.getKey();
Object entryValue = entry.getValue();
if (key != null) {
queue.addLast(new Node(key, node.path + "{k}"));
}
if (entryValue != null) {
queue.addLast(new Node(entryValue, node.path + "{v}"));
}
}
continue;
}
Field[] fields = fieldsOf(type);
for (Field field : fields) {
if (skipField(field)) {
continue;
}
Object fieldValue;
try {
if (!field.canAccess(value)) {
field.setAccessible(true);
}
fieldValue = field.get(value);
} catch (Throwable ignored) {
continue;
}
if (fieldValue == null) {
continue;
}
queue.addLast(new Node(fieldValue, node.path + "." + field.getName()));
}
}
}
private static List<PrebakeTarget> collectStartupTargets() {
LinkedHashMap<String, PrebakeTarget> targets = new LinkedHashMap<>();
File packsFolder = Iris.instance.getDataFolder("packs");
File[] packs = packsFolder.listFiles();
if (packs != null) {
Arrays.sort(packs, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
for (File pack : packs) {
addStartupTarget(targets, pack);
}
}
File worldContainer = Bukkit.getWorldContainer();
File[] worlds = worldContainer.listFiles();
if (worlds != null) {
Arrays.sort(worlds, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
for (File world : worlds) {
if (world == null || !world.isDirectory()) {
continue;
}
File selfContainedPack = new File(world, "iris/pack");
addStartupTarget(targets, selfContainedPack);
}
}
return new ArrayList<>(targets.values());
}
private static void addStartupTarget(LinkedHashMap<String, PrebakeTarget> targets, File folder) {
if (!isPrebakeDataFolder(folder)) {
return;
}
String canonicalPath = toCanonicalPath(folder);
if (targets.containsKey(canonicalPath)) {
return;
}
targets.put(canonicalPath, new PrebakeTarget(folder, startupLabel(folder)));
}
private static boolean isPrebakeDataFolder(File folder) {
if (folder == null || !folder.exists() || !folder.isDirectory()) {
return false;
}
for (String loaderFolder : PREBAKE_LOADERS) {
File candidate = new File(folder, loaderFolder);
if (candidate.exists() && candidate.isDirectory()) {
return true;
}
}
return false;
}
private static String startupLabel(File folder) {
File parent = folder.getParentFile();
if (parent != null && "iris".equalsIgnoreCase(parent.getName())) {
File worldFolder = parent.getParentFile();
if (worldFolder != null) {
return worldFolder.getName() + "/self-contained";
}
}
return folder.getName();
}
private static String toCanonicalPath(File folder) {
try {
return folder.getCanonicalPath();
} catch (IOException e) {
return folder.getAbsolutePath();
}
}
private static long[] collectDomainSeeds(SeedManager seedManager) {
if (seedManager == null) {
return NO_SEEDS;
}
return new long[]{
seedManager.getSeed(),
seedManager.getComplex(),
seedManager.getComplexStreams(),
seedManager.getBasic(),
seedManager.getHeight(),
seedManager.getComponent(),
seedManager.getScript(),
seedManager.getMantle(),
seedManager.getEntity(),
seedManager.getBiome(),
seedManager.getDecorator(),
seedManager.getTerrain(),
seedManager.getSpawn(),
seedManager.getJigsaw(),
seedManager.getCarve(),
seedManager.getDeposit(),
seedManager.getPost(),
seedManager.getBodies(),
seedManager.getMode()
};
}
private static Field[] fieldsOf(Class<?> type) {
return FIELD_CACHE.computeIfAbsent(type, IrisNoisemapPrebakePipeline::resolveFields);
}
private static Field[] resolveFields(Class<?> type) {
List<Field> fields = new ArrayList<>();
Class<?> cursor = type;
while (cursor != null && cursor != Object.class) {
Field[] declared = cursor.getDeclaredFields();
for (Field field : declared) {
fields.add(field);
}
cursor = cursor.getSuperclass();
}
return fields.toArray(new Field[0]);
}
private static boolean skipField(Field field) {
int modifiers = field.getModifiers();
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) || field.isSynthetic()) {
return true;
}
Class<?> type = field.getType();
if (type.isPrimitive()) {
return true;
}
return isLeafType(type);
}
private static boolean isLeafType(Class<?> type) {
if (type.isPrimitive() || type.isEnum()) {
return true;
}
if (type == String.class
|| type == Boolean.class
|| type == Character.class
|| Number.class.isAssignableFrom(type)
|| type == Class.class
|| type == Locale.class
|| type == File.class) {
return true;
}
String name = type.getName();
return name.startsWith("java.time.")
|| name.startsWith("java.util.concurrent.atomic.")
|| name.startsWith("org.bukkit.")
|| name.startsWith("net.minecraft.")
|| name.startsWith("art.arcane.iris.engine.data.cache.")
|| name.equals("art.arcane.volmlib.util.math.RNG");
}
private static long mixSeed(long seed, int hash) {
long mixed = seed ^ ((long) hash * 0x9E3779B97F4A7C15L);
mixed ^= mixed >>> 33;
mixed *= 0xff51afd7ed558ccdL;
mixed ^= mixed >>> 33;
mixed *= 0xc4ceb9fe1a85ec53L;
mixed ^= mixed >>> 33;
return mixed;
}
private static final class Node {
private final Object value;
private final String path;
private Node(Object value, String path) {
this.value = value;
this.path = path;
}
}
private static final class StyleReference {
private final IrisGeneratorStyle style;
private final int hash;
private StyleReference(IrisGeneratorStyle style, int hash) {
this.style = style;
this.hash = hash;
}
}
private static final class PrebakeRunResult {
private static final PrebakeRunResult SKIPPED = new PrebakeRunResult(false, false, 0);
private static final PrebakeRunResult UNCHANGED = new PrebakeRunResult(false, true, 0);
private final boolean executed;
private final boolean unchanged;
private final int failures;
private PrebakeRunResult(boolean executed, boolean unchanged, int failures) {
this.executed = executed;
this.unchanged = unchanged;
this.failures = failures;
}
}
private static final class PrebakeTarget {
private final File folder;
private final String label;
private PrebakeTarget(File folder, String label) {
this.folder = folder;
this.label = label;
}
}
private static final class StartupTargetResult {
private final String label;
private final PrebakeRunResult result;
private StartupTargetResult(String label, PrebakeRunResult result) {
this.label = label;
this.result = result;
}
}
private static final class StartupProgress {
private final int targetTotal;
private final AtomicInteger targetsCompleted = new AtomicInteger();
private final AtomicInteger executedTargets = new AtomicInteger();
private final AtomicInteger unchangedTargets = new AtomicInteger();
private final AtomicInteger failedTargets = new AtomicInteger();
private final AtomicInteger stylesDiscovered = new AtomicInteger();
private final AtomicInteger stylesFinished = new AtomicInteger();
private StartupProgress(int targetTotal) {
this.targetTotal = targetTotal;
}
private void onStylesDiscovered(int styleCount) {
if (styleCount > 0) {
stylesDiscovered.addAndGet(styleCount);
}
}
private void onStyleFinished() {
stylesFinished.incrementAndGet();
}
private void onTargetCompleted(PrebakeRunResult result) {
targetsCompleted.incrementAndGet();
if (result.executed) {
executedTargets.incrementAndGet();
}
if (result.unchanged) {
unchangedTargets.incrementAndGet();
}
if (result.failures > 0) {
failedTargets.incrementAndGet();
}
}
private void onTargetFailed() {
targetsCompleted.incrementAndGet();
failedTargets.incrementAndGet();
}
}
private static final class StartupPrebakeThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "Iris-NoisemapPrebake-" + STARTUP_WORKER_SEQUENCE.incrementAndGet());
thread.setDaemon(true);
return thread;
}
}
}

View File

@@ -88,7 +88,11 @@ public class IrisGeneratorStyle {
}
public CNG createNoCache(RNG rng, IrisData data) {
return createNoCache(rng, data, false);
return createNoCache(rng, data, false, 0, false);
}
public CNG createForPrebake(RNG rng, IrisData data, int fallbackCacheSize) {
return createNoCache(rng, data, false, Math.max(0, fallbackCacheSize), true);
}
@@ -110,14 +114,19 @@ public class IrisGeneratorStyle {
return Objects.hash(expression, imageMapHash(), script, multiplier, axialFracturing, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style);
}
private String cachePrefix(RNG rng) {
public int prebakeSignature() {
return hash();
}
private String cachePrefix(RNG rng, int effectiveCacheSize) {
return "style-" + Integer.toUnsignedString(hash())
+ "-seed-" + Long.toUnsignedString(rng.getSeed())
+ "-sz-" + effectiveCacheSize
+ "-src-";
}
private String cacheKey(RNG rng, long sourceStamp) {
return cachePrefix(rng) + Long.toUnsignedString(sourceStamp);
private String cacheKey(RNG rng, long sourceStamp, int effectiveCacheSize) {
return cachePrefix(rng, effectiveCacheSize) + Long.toUnsignedString(sourceStamp);
}
private long scriptStamp(IrisData data) {
@@ -153,6 +162,10 @@ public class IrisGeneratorStyle {
}
public CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached) {
return createNoCache(rng, data, actuallyCached, 0, false);
}
private CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached, int fallbackCacheSize, boolean quietCacheLog) {
CNG cng = null;
long sourceStamp = 0L;
if (getExpression() != null) {
@@ -182,17 +195,18 @@ public class IrisGeneratorStyle {
cng.setTrueFracturing(axialFracturing);
if (fracture != null) {
cng.fractureWith(fracture.create(rng.nextParallelRNG(2934), data), fracture.getMultiplier());
cng.fractureWith(fracture.createNoCache(rng.nextParallelRNG(2934), data, false, fallbackCacheSize, quietCacheLog), fracture.getMultiplier());
}
if (cellularFrequency > 0) {
cng = cng.cellularize(rng.nextParallelRNG(884466), cellularFrequency).scale(1D / cellularZoom).bake();
}
if (cacheSize > 0) {
String key = cacheKey(rng, sourceStamp);
clearStaleCacheEntries(data, cachePrefix(rng), key);
cng = cng.cached(cacheSize, key, data.getDataFolder());
int effectiveCacheSize = cacheSize > 0 ? cacheSize : Math.max(0, fallbackCacheSize);
if (effectiveCacheSize > 0) {
String key = cacheKey(rng, sourceStamp, effectiveCacheSize);
clearStaleCacheEntries(data, cachePrefix(rng, effectiveCacheSize), key);
cng = cng.cached(effectiveCacheSize, key, data.getDataFolder(), quietCacheLog);
}
return cng;

View File

@@ -31,10 +31,12 @@ import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
public class IrisInterpolation {
public static CNG cng = NoiseStyle.SIMPLEX.create(new RNG());
private static final ThreadLocal<NoiseSampleCache2D> NOISE_SAMPLE_CACHE_2D = ThreadLocal.withInitial(() -> new NoiseSampleCache2D(64));
public static double bezier(double t) {
return t * t * (3.0d - 2.0d * t);
@@ -286,35 +288,9 @@ public class IrisInterpolation {
}
public static int getRadiusFactor(int coord, double radius) {
if (radius == 2) {
return coord >> 1;
}
if (radius == 4) {
return coord >> 2;
}
if (radius == 8) {
return coord >> 3;
}
if (radius == 16) {
return coord >> 4;
}
if (radius == 32) {
return coord >> 5;
}
if (radius == 64) {
return coord >> 6;
}
if (radius == 128) {
return coord >> 7;
}
if (radius == 256) {
return coord >> 8;
}
if (radius == 512) {
return coord >> 9;
}
if (radius == 1024) {
return coord >> 10;
int radiusInt = (int) radius;
if (radius == radiusInt && radiusInt > 0 && (radiusInt & (radiusInt - 1)) == 0) {
return coord >> Integer.numberOfTrailingZeros(radiusInt);
}
return (int) Math.floor(coord / radius);
}
@@ -999,8 +975,14 @@ public class IrisInterpolation {
}
public static double getNoise(InterpolationMethod method, int x, int z, double h, NoiseProvider noise) {
NoiseSampleCache2D cache = new NoiseSampleCache2D(64);
NoiseProvider n = (x1, z1) -> cache.getOrSample(x1 - x, z1 - z, x1, z1, noise);
final NoiseProvider n;
if (usesSampleCache(method)) {
NoiseSampleCache2D cache = NOISE_SAMPLE_CACHE_2D.get();
cache.clear();
n = (x1, z1) -> cache.getOrSample(x1 - x, z1 - z, x1, z1, noise);
} else {
n = noise;
}
if (method.equals(InterpolationMethod.BILINEAR)) {
return getBilinearNoise(x, z, h, n);
@@ -1059,53 +1041,138 @@ public class IrisInterpolation {
return n.noise(x, z);
}
private static boolean usesSampleCache(InterpolationMethod method) {
return switch (method) {
case BILINEAR_STARCAST_3,
BILINEAR_STARCAST_6,
BILINEAR_STARCAST_9,
BILINEAR_STARCAST_12,
HERMITE_STARCAST_3,
HERMITE_STARCAST_6,
HERMITE_STARCAST_9,
HERMITE_STARCAST_12 -> true;
default -> false;
};
}
private static class NoiseSampleCache2D {
private long[] xBits;
private long[] zBits;
private double[] values;
private byte[] states;
private int mask;
private int resizeThreshold;
private int size;
public NoiseSampleCache2D(int initialCapacity) {
xBits = new long[initialCapacity];
zBits = new long[initialCapacity];
values = new double[initialCapacity];
int minimumCapacity = Math.max(8, initialCapacity);
int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity);
xBits = new long[tableSize];
zBits = new long[tableSize];
values = new double[tableSize];
states = new byte[tableSize];
mask = tableSize - 1;
resizeThreshold = Math.max(1, (tableSize * 3) >> 2);
size = 0;
}
public void clear() {
if (size == 0) {
return;
}
Arrays.fill(states, (byte) 0);
size = 0;
}
public double getOrSample(double relativeX, double relativeZ, double sampleX, double sampleZ, NoiseProvider provider) {
long rx = Double.doubleToLongBits(relativeX);
long rz = Double.doubleToLongBits(relativeZ);
for (int i = 0; i < size; i++) {
if (xBits[i] == rx && zBits[i] == rz) {
return values[i];
}
int slot = findSlot(rx, rz);
if (states[slot] != 0) {
return values[slot];
}
double value = provider.noise(sampleX, sampleZ);
if (size >= xBits.length) {
grow();
}
xBits[size] = rx;
zBits[size] = rz;
values[size] = value;
size++;
insert(slot, rx, rz, value);
return value;
}
private int findSlot(long rx, long rz) {
int slot = mix(rx, rz) & mask;
while (states[slot] != 0) {
if (xBits[slot] == rx && zBits[slot] == rz) {
break;
}
slot = (slot + 1) & mask;
}
return slot;
}
private void insert(int slot, long rx, long rz, double value) {
xBits[slot] = rx;
zBits[slot] = rz;
values[slot] = value;
states[slot] = 1;
size++;
if (size >= resizeThreshold) {
grow();
}
}
private int mix(long rx, long rz) {
long hash = rx * 0x9E3779B97F4A7C15L;
hash ^= Long.rotateLeft(rz * 0xC2B2AE3D27D4EB4FL, 32);
hash ^= (hash >>> 33);
hash *= 0xff51afd7ed558ccdL;
hash ^= (hash >>> 33);
return (int) hash;
}
private void grow() {
long[] previousXBits = xBits;
long[] previousZBits = zBits;
double[] previousValues = values;
byte[] previousStates = states;
int nextLength = xBits.length << 1;
long[] nextXBits = new long[nextLength];
long[] nextZBits = new long[nextLength];
double[] nextValues = new double[nextLength];
System.arraycopy(xBits, 0, nextXBits, 0, size);
System.arraycopy(zBits, 0, nextZBits, 0, size);
System.arraycopy(values, 0, nextValues, 0, size);
byte[] nextStates = new byte[nextLength];
xBits = nextXBits;
zBits = nextZBits;
values = nextValues;
states = nextStates;
mask = nextLength - 1;
resizeThreshold = Math.max(1, (nextLength * 3) >> 2);
size = 0;
for (int i = 0; i < previousStates.length; i++) {
if (previousStates[i] == 0) {
continue;
}
int slot = findSlot(previousXBits[i], previousZBits[i]);
xBits[slot] = previousXBits[i];
zBits[slot] = previousZBits[i];
values[slot] = previousValues[i];
states[slot] = 1;
size++;
}
}
private int tableSizeFor(int value) {
int n = value - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
int size = n + 1;
if (size < 8) {
return 8;
}
return size;
}
}

View File

@@ -238,6 +238,10 @@ public class CNG {
}
public CNG cached(int size, String key, File cacheFolder) {
return cached(size, key, cacheFolder, false);
}
public CNG cached(int size, String key, File cacheFolder, boolean quiet) {
if (size <= 0) {
return this;
}
@@ -271,7 +275,9 @@ public class CNG {
DataOutputStream dos = new DataOutputStream(fos);
fbc.writeCache(dos);
dos.close();
Iris.info("Saved Noise Cache " + f.getName());
if (!quiet) {
Iris.info("Saved Noise Cache " + f.getName());
}
} catch (IOException e) {
throw new RuntimeException(e);
}