mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-04-04 14:56:31 +00:00
f
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user