WIP Building for latest.

Still Quantizing, WOrking on that next
also removed prebake
This commit is contained in:
Brian Neumann-Fopiano
2026-04-15 09:05:41 -04:00
parent 568fb07f66
commit aa706d027b
97 changed files with 8003 additions and 3087 deletions

View File

@@ -79,6 +79,8 @@ dependencies {
transitive = false
}
compileOnly(libs.multiverseCore)
compileOnly(libs.craftengine.core)
compileOnly(libs.craftengine.bukkit)
// Shaded
implementation('de.crazydev22.slimjar.helper:spigot:2.1.9')

View File

@@ -1 +1 @@
2117487583
466077434

View File

@@ -24,6 +24,10 @@ import com.google.gson.JsonParser;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.link.IrisPapiExpansion;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.loader.IrisData;
@@ -78,7 +82,6 @@ import java.io.*;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -98,9 +101,6 @@ public class Iris extends VolmitPlugin implements Listener {
private static File settingsFile;
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance();
private static final Map<String, ChunkGenerator> stagedRuntimeGenerators = new ConcurrentHashMap<>();
private static final Map<String, BiomeProvider> stagedRuntimeBiomeProviders = new ConcurrentHashMap<>();
static {
try {
InstanceState.updateInstanceId();
@@ -122,40 +122,30 @@ public class Iris extends VolmitPlugin implements Listener {
return sender;
}
public static void stageRuntimeWorldGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator, @Nullable BiomeProvider biomeProvider) {
stagedRuntimeGenerators.put(worldName, generator);
if (biomeProvider != null) {
stagedRuntimeBiomeProviders.put(worldName, biomeProvider);
} else {
stagedRuntimeBiomeProviders.remove(worldName);
}
}
@Nullable
private static ChunkGenerator consumeRuntimeWorldGenerator(@NotNull String worldName) {
return stagedRuntimeGenerators.remove(worldName);
}
@Nullable
private static BiomeProvider consumeRuntimeBiomeProvider(@NotNull String worldName) {
return stagedRuntimeBiomeProviders.remove(worldName);
}
public static void clearStagedRuntimeWorldGenerator(@NotNull String worldName) {
stagedRuntimeGenerators.remove(worldName);
stagedRuntimeBiomeProviders.remove(worldName);
}
@SuppressWarnings("unchecked")
public static <T> T service(Class<T> c) {
return (T) instance.services.get(c);
}
public static void callEvent(Event e) {
Runnable dispatcher = () -> {
try {
Bukkit.getPluginManager().callEvent(e);
} catch (Throwable ex) {
reportError("Event dispatch failed for \"" + e.getEventName() + "\".", ex);
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (ex instanceof Error error) {
throw error;
}
throw new IllegalStateException(ex);
}
};
if (!e.isAsynchronous()) {
J.s(() -> Bukkit.getPluginManager().callEvent(e));
J.s(dispatcher);
} else {
Bukkit.getPluginManager().callEvent(e);
dispatcher.run();
}
}
@@ -443,8 +433,22 @@ public class Iris extends VolmitPlugin implements Listener {
}
public static void reportError(Throwable e) {
if (e == null) {
return;
}
Bindings.capture(e);
if (IrisSettings.get().getGeneral().isDebug()) {
boolean debug = false;
if (instance != null) {
try {
IrisSettings currentSettings = IrisSettings.settings != null ? IrisSettings.settings : IrisSettings.get();
debug = currentSettings != null && currentSettings.getGeneral().isDebug();
} catch (Throwable ignored) {
debug = false;
}
}
if (debug) {
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
if (e.getCause() != null) {
@@ -467,6 +471,25 @@ public class Iris extends VolmitPlugin implements Listener {
}
}
public static void reportError(String context, Throwable e) {
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
try {
if (instance != null) {
Iris.error(message);
} else {
System.err.println("[Iris] " + message);
}
} catch (Throwable inner) {
System.err.println("[Iris] " + message);
inner.printStackTrace(System.err);
}
reportError(error);
error.printStackTrace(System.err);
}
public static void dump() {
try {
File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt");
@@ -533,6 +556,8 @@ public class Iris extends VolmitPlugin implements Listener {
addShutdownHook();
processPendingStartupWorldDeletes();
IrisToolbelt.applyPregenPerformanceProfile();
WorldLifecycleService.get();
WorldRuntimeControlService.get();
if (J.isFolia()) {
checkForBukkitWorlds(s -> true);
@@ -614,11 +639,10 @@ public class Iris extends VolmitPlugin implements Listener {
Iris.error("Failed to load world " + s + "!");
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
reportError(e);
reportError("Failed to load staged startup world \"" + s + "\".", e);
return;
}
Iris.error("Failed to load world " + s + "!");
e.printStackTrace();
reportError("Failed to load startup world \"" + s + "\".", e);
}
});
if (!deferredStartupWorlds.isEmpty()) {
@@ -626,8 +650,7 @@ public class Iris extends VolmitPlugin implements Listener {
Iris.warn("Bukkit.createWorld is intentionally unavailable in this startup phase. Worlds remain staged in bukkit.yml.");
}
} catch (Throwable e) {
e.printStackTrace();
reportError(e);
reportError("Failed while loading startup Iris worlds.", e);
}
}
@@ -673,6 +696,9 @@ public class Iris extends VolmitPlugin implements Listener {
private void processPendingStartupWorldDeletes() {
try {
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(Bukkit.getWorldContainer())) {
queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld);
}
if (queue.isEmpty()) {
return;
}
@@ -690,20 +716,33 @@ public class Iris extends VolmitPlugin implements Listener {
continue;
}
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
if (!worldFolder.exists()) {
boolean foundAny = false;
boolean deletedAll = true;
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
File worldFolder = new File(Bukkit.getWorldContainer(), familyWorldName);
if (!worldFolder.exists()) {
continue;
}
foundAny = true;
IO.delete(worldFolder);
if (worldFolder.exists()) {
deletedAll = false;
Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup.");
} else {
Iris.info("Deleted queued world folder \"" + familyWorldName + "\".");
}
}
if (!foundAny) {
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
continue;
}
IO.delete(worldFolder);
if (worldFolder.exists()) {
Iris.warn("Failed to delete queued world folder \"" + worldName + "\". Retrying on next startup.");
if (!deletedAll) {
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
continue;
}
Iris.info("Deleted queued world folder \"" + worldName + "\".");
}
writePendingWorldDeleteMap(remaining);
@@ -952,7 +991,7 @@ public class Iris extends VolmitPlugin implements Listener {
@Nullable
@Override
public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
BiomeProvider stagedBiomeProvider = consumeRuntimeBiomeProvider(worldName);
org.bukkit.generator.BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
if (stagedBiomeProvider != null) {
Iris.debug("Using staged runtime biome provider for " + worldName);
return stagedBiomeProvider;
@@ -963,7 +1002,7 @@ public class Iris extends VolmitPlugin implements Listener {
@Override
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
ChunkGenerator stagedGenerator = consumeRuntimeWorldGenerator(worldName);
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
if (stagedGenerator != null) {
Iris.debug("Using staged runtime generator for " + worldName);
return stagedGenerator;
@@ -1029,27 +1068,81 @@ public class Iris extends VolmitPlugin implements Listener {
private void printPacks() {
File packFolder = Iris.service(StudioSVC.class).getWorkspaceFolder();
File[] packs = packFolder.listFiles(File::isDirectory);
if (packs == null || packs.length == 0)
List<SplashPackMetadata> packs = collectSplashPacks(packFolder);
if (packs.isEmpty())
return;
Iris.info("Custom Dimensions: " + packs.length);
for (File f : packs)
printPack(f);
Iris.info("Custom Dimensions: " + packs.size());
for (SplashPackMetadata pack : packs) {
printPack(pack);
}
}
private void printPack(File pack) {
String dimName = pack.getName();
String version = "???";
try (FileReader r = new FileReader(new File(pack, "dimensions/" + dimName + ".json"))) {
JsonObject json = JsonParser.parseReader(r).getAsJsonObject();
if (json.has("version"))
version = json.get("version").getAsString();
} catch (IOException | JsonParseException ex) {
Iris.verbose("Failed to read dimension version metadata for " + dimName + ": "
+ ex.getClass().getSimpleName()
+ (ex.getMessage() == null ? "" : " - " + ex.getMessage()));
static List<SplashPackMetadata> collectSplashPacks(File packFolder) {
if (packFolder == null || !packFolder.isDirectory()) {
return Collections.emptyList();
}
File[] folders = packFolder.listFiles(File::isDirectory);
if (folders == null || folders.length == 0) {
return Collections.emptyList();
}
List<SplashPackMetadata> packs = new ArrayList<>();
for (File folder : folders) {
SplashPackMetadata metadata = readSplashPack(folder);
if (metadata != null) {
packs.add(metadata);
}
}
packs.sort(Comparator.comparing(SplashPackMetadata::name));
return packs;
}
static SplashPackMetadata readSplashPack(File pack) {
if (pack == null || !pack.isDirectory()) {
return null;
}
String dimName = pack.getName();
File dimensionFile = new File(pack, "dimensions/" + dimName + ".json");
if (!dimensionFile.isFile()) {
return null;
}
try (FileReader r = new FileReader(dimensionFile)) {
JsonObject json = JsonParser.parseReader(r).getAsJsonObject();
if (!json.has("version")) {
return null;
}
return new SplashPackMetadata(dimName, json.get("version").getAsString());
} catch (IOException | JsonParseException ex) {
reportError("Failed to read splash metadata for dimension pack \"" + dimName + "\".", ex);
return null;
}
}
private void printPack(SplashPackMetadata pack) {
Iris.info(" " + pack.name() + " v" + pack.version());
}
static final class SplashPackMetadata {
private final String name;
private final String version;
SplashPackMetadata(String name, String version) {
this.name = name;
this.version = version;
}
String name() {
return name;
}
String version() {
return version;
}
Iris.info(" " + dimName + " v" + version);
}
public int getIrisVersion() {

View File

@@ -38,6 +38,9 @@ public enum IrisRuntimeSchedulerMode {
if (containsIgnoreCase(bukkitName, "purpur")
|| containsIgnoreCase(bukkitVersion, "purpur")
|| containsIgnoreCase(serverClassName, "purpur")
|| containsIgnoreCase(bukkitName, "canvas")
|| containsIgnoreCase(bukkitVersion, "canvas")
|| containsIgnoreCase(serverClassName, "canvas")
|| containsIgnoreCase(bukkitName, "paper")
|| containsIgnoreCase(bukkitVersion, "paper")
|| containsIgnoreCase(serverClassName, "paper")

View File

@@ -159,7 +159,6 @@ public class IrisSettings {
public int chunkLoadTimeoutSeconds = 15;
public int timeoutWarnIntervalMs = 500;
public int saveIntervalMs = 120_000;
public boolean startupNoisemapPrebake = true;
public boolean enablePregenPerformanceProfile = true;
public int pregenProfileNoiseCacheSize = 4_096;
public boolean pregenProfileEnableFastCache = true;

View File

@@ -24,7 +24,6 @@ import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.engine.IrisNoisemapPrebakePipeline;
import art.arcane.iris.engine.object.*;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -77,10 +76,7 @@ public class ServerConfigurator {
}
deferredInstallPending = false;
boolean datapacksMissing = installDataPacks(true);
if (!datapacksMissing) {
IrisNoisemapPrebakePipeline.scheduleInstalledPacksPrebakeAsync();
}
installDataPacks(true);
}
public static void configureIfDeferred() {
@@ -129,8 +125,15 @@ public class ServerConfigurator {
return new KList<File>().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks"));
}
KList<File> worlds = new KList<>();
Bukkit.getServer().getWorlds().forEach(w -> worlds.add(new File(w.getWorldFolder(), "datapacks")));
if (worlds.isEmpty()) worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks"));
Bukkit.getServer().getWorlds().forEach(w -> {
File folder = resolveDatapacksFolder(w.getWorldFolder());
if (!worlds.contains(folder)) {
worlds.add(folder);
}
});
if (worlds.isEmpty()) {
worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks"));
}
return worlds;
}
@@ -180,9 +183,10 @@ public class ServerConfigurator {
Iris.verbose("Checking Data Packs...");
}
DimensionHeight height = new DimensionHeight(fixer);
KList<File> folders = getDatapacksFolder();
KList<File> baseFolders = getDatapacksFolder();
KList<File> folders = collectInstallDatapackFolders(baseFolders, extraWorldDatapackFoldersByPack);
if (includeExternal) {
installExternalDataPacks(folders, extraWorldDatapackFoldersByPack);
installExternalDataPacks(baseFolders, extraWorldDatapackFoldersByPack);
}
KMap<String, KSet<String>> biomes = new KMap<>();
@@ -205,6 +209,34 @@ public class ServerConfigurator {
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
}
static KList<File> collectInstallDatapackFolders(
KList<File> baseFolders,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
) {
KList<File> folders = new KList<>();
if (baseFolders != null) {
for (File folder : baseFolders) {
if (folder != null && !folders.contains(folder)) {
folders.add(folder);
}
}
}
if (extraWorldDatapackFoldersByPack == null || extraWorldDatapackFoldersByPack.isEmpty()) {
return folders;
}
for (KList<File> extraFolders : extraWorldDatapackFoldersByPack.values()) {
if (extraFolders == null || extraFolders.isEmpty()) {
continue;
}
for (File folder : extraFolders) {
if (folder != null && !folders.contains(folder)) {
folders.add(folder);
}
}
}
return folders;
}
private static void installExternalDataPacks(
KList<File> folders,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
@@ -915,7 +947,10 @@ public class ServerConfigurator {
if (packName.isBlank()) {
continue;
}
File datapacksFolder = new File(Bukkit.getWorldContainer(), worldName + File.separator + "datapacks");
org.bukkit.World world = Bukkit.getWorld(worldName);
File datapacksFolder = world == null
? new File(Bukkit.getWorldContainer(), worldName + File.separator + "datapacks")
: resolveDatapacksFolder(world.getWorldFolder());
addWorldDatapackFolder(foldersByPack, packName, datapacksFolder);
}
@@ -929,7 +964,7 @@ public class ServerConfigurator {
if (packName.isBlank()) {
continue;
}
File datapacksFolder = new File(world.getWorldFolder(), "datapacks");
File datapacksFolder = resolveDatapacksFolder(world.getWorldFolder());
addWorldDatapackFolder(foldersByPack, packName, datapacksFolder);
}
@@ -969,6 +1004,31 @@ public class ServerConfigurator {
}
}
public static File resolveDatapacksFolder(File worldFolder) {
File rootFolder = resolveWorldRootFolder(worldFolder);
return new File(rootFolder, "datapacks");
}
static File resolveWorldRootFolder(File worldFolder) {
if (worldFolder == null) {
return new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME);
}
File current = worldFolder.getAbsoluteFile();
while (current != null) {
if ("dimensions".equals(current.getName())) {
File parent = current.getParentFile();
if (parent != null) {
return parent;
}
break;
}
current = current.getParentFile();
}
return worldFolder.getAbsoluteFile();
}
private static String sanitizePackName(String value) {
if (value == null) {
return "";

View File

@@ -87,6 +87,7 @@ public class CommandDeveloper implements DirectorExecutor {
private static final Set<String> ACTIVE_DELETE_CHUNK_WORLDS = ConcurrentHashMap.newKeySet();
private CommandTurboPregen turboPregen;
private CommandLazyPregen lazyPregen;
private CommandSmoke smoke;
@Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true)
public void EngineStatus() {

View File

@@ -22,7 +22,7 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.ExternalDataPackPipeline;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.link.FoliaWorldsLink;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.service.StudioSVC;
@@ -196,8 +196,7 @@ public class CommandIris implements DirectorExecutor {
}
} catch (Throwable e) {
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
Iris.error("Exception raised during world creation: " + e.getMessage());
Iris.reportError(e);
Iris.reportError("Exception raised during world creation for \"" + name + "\".", e);
worldCreation = false;
return;
}
@@ -742,7 +741,7 @@ public class CommandIris implements DirectorExecutor {
return;
}
if (!FoliaWorldsLink.get().unloadWorld(world, false)) {
if (!WorldLifecycleService.get().unload(world, false)) {
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
return;
}
@@ -755,7 +754,7 @@ public class CommandIris implements DirectorExecutor {
}
} catch (IOException e) {
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
e.printStackTrace();
Iris.reportError("Failed to remove world \"" + world.getName() + "\" from bukkit.yml.", e);
}
IrisToolbelt.evacuate(world, "Deleting world");
deletingWorld = true;
@@ -2144,7 +2143,7 @@ public class CommandIris implements DirectorExecutor {
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
try {
IrisToolbelt.evacuate(world);
boolean unloaded = FoliaWorldsLink.get().unloadWorld(world, false);
boolean unloaded = WorldLifecycleService.get().unload(world, false);
if (unloaded) {
sender().sendMessage(C.GREEN + "World unloaded successfully.");
} else {
@@ -2152,7 +2151,7 @@ public class CommandIris implements DirectorExecutor {
}
} catch (Exception e) {
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
e.printStackTrace();
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
}
}

View File

@@ -0,0 +1,186 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.Iris;
import art.arcane.iris.core.runtime.DatapackReadinessResult;
import art.arcane.iris.core.runtime.SmokeDiagnosticsService;
import art.arcane.iris.core.runtime.SmokeTestService;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.director.annotations.Param;
import art.arcane.volmlib.util.format.Form;
import java.util.List;
@Director(name = "smoke", description = "Run Iris developer smoke diagnostics")
public class CommandSmoke implements DirectorExecutor {
@Director(description = "Run the full smoke suite", sync = true)
public void full(
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
String dimension,
@Param(description = "The seed to use", defaultValue = "1337")
long seed,
@Param(description = "Optional player validation target or none", defaultValue = "none")
String player,
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
boolean retainOnFailure
) {
String runId = SmokeTestService.get().startFullSmoke(sender(), dimension, seed, player, retainOnFailure);
announceRun(runId, "full");
}
@Director(description = "Run the studio smoke flow", sync = true)
public void studio(
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
String dimension,
@Param(description = "The seed to use", defaultValue = "1337")
long seed,
@Param(description = "Optional player validation target or none", defaultValue = "none")
String player,
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
boolean retainOnFailure
) {
String runId = SmokeTestService.get().startStudioSmoke(sender(), dimension, seed, player, retainOnFailure);
announceRun(runId, "studio");
}
@Director(description = "Run the create/unload smoke flow", sync = true)
public void create(
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
String dimension,
@Param(description = "The seed to use", defaultValue = "1337")
long seed,
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
boolean retainOnFailure
) {
String runId = SmokeTestService.get().startCreateSmoke(sender(), dimension, seed, retainOnFailure);
announceRun(runId, "create");
}
@Director(description = "Run the benchmark create/unload smoke flow", sync = true)
public void benchmark(
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
String dimension,
@Param(description = "The seed to use", defaultValue = "1337")
long seed,
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
boolean retainOnFailure
) {
String runId = SmokeTestService.get().startBenchmarkSmoke(sender(), dimension, seed, retainOnFailure);
announceRun(runId, "benchmark");
}
@Director(description = "Show live or persisted smoke status", sync = true)
public void status(
@Param(description = "Use latest or a specific run id", defaultValue = "latest")
String run
) {
SmokeDiagnosticsService.SmokeRunReport report = resolveReport(run);
if (report == null) {
sender().sendMessage(C.RED + "No smoke report found for \"" + run + "\".");
return;
}
sendReport(report);
}
@Director(description = "Inspect a currently loaded smoke/studio world", sync = true)
public void inspect(
@Param(description = "The loaded world name to inspect")
String world
) {
SmokeTestService.WorldInspection inspection = SmokeTestService.get().inspectWorld(world);
if (inspection == null) {
sender().sendMessage(C.RED + "World \"" + world + "\" is not currently loaded.");
return;
}
sender().sendMessage(C.GREEN + "Smoke inspection for " + C.GOLD + inspection.worldName());
sender().sendMessage(C.GRAY + "Lifecycle backend: " + C.WHITE + inspection.lifecycleBackend());
sender().sendMessage(C.GRAY + "Runtime backend: " + C.WHITE + inspection.runtimeBackend());
sender().sendMessage(C.GRAY + "Studio: " + C.WHITE + inspection.studio() + C.GRAY + " | Maintenance active: " + C.WHITE + inspection.maintenanceActive());
sender().sendMessage(C.GRAY + "Engine closed: " + C.WHITE + inspection.engineClosed() + C.GRAY + " | Engine failing: " + C.WHITE + inspection.engineFailing());
sender().sendMessage(C.GRAY + "Generation session: " + C.WHITE + inspection.generationSessionId() + C.GRAY + " | Active leases: " + C.WHITE + inspection.activeLeaseCount());
sender().sendMessage(C.GRAY + "Datapack folders: " + C.WHITE + joinList(inspection.datapackFolders()));
}
private void announceRun(String runId, String mode) {
sender().sendMessage(C.GREEN + "Started " + C.GOLD + mode + C.GREEN + " smoke run " + C.GOLD + runId + C.GREEN + ".");
sender().sendMessage(C.GREEN + "Use " + C.GOLD + "/iris developer smoke status run=" + runId + C.GREEN + " to monitor progress.");
sender().sendMessage(C.GREEN + "Latest report: " + C.GOLD + latestReportPath());
}
private SmokeDiagnosticsService.SmokeRunReport resolveReport(String run) {
if (run == null || run.isBlank() || run.equalsIgnoreCase("latest")) {
return SmokeTestService.get().latest();
}
return SmokeTestService.get().get(run);
}
private void sendReport(SmokeDiagnosticsService.SmokeRunReport report) {
String elapsed = Form.duration(Math.max(0L, report.getElapsedMs()), 0);
sender().sendMessage(C.GREEN + "Smoke run " + C.GOLD + report.getRunId() + C.GREEN + " (" + C.GOLD + report.getMode() + C.GREEN + ")");
sender().sendMessage(C.GRAY + "World: " + C.WHITE + fallback(report.getWorldName()) + C.GRAY + " | Outcome: " + C.WHITE + fallback(report.getOutcome()));
sender().sendMessage(C.GRAY + "Stage: " + C.WHITE + fallback(report.getStage()) + C.GRAY + " | Elapsed: " + C.WHITE + elapsed);
if (report.getStageDetail() != null && !report.getStageDetail().isBlank()) {
sender().sendMessage(C.GRAY + "Stage detail: " + C.WHITE + report.getStageDetail());
}
sender().sendMessage(C.GRAY + "Lifecycle backend: " + C.WHITE + fallback(report.getLifecycleBackend()));
sender().sendMessage(C.GRAY + "Runtime backend: " + C.WHITE + fallback(report.getRuntimeBackend()));
sender().sendMessage(C.GRAY + "Generation session: " + C.WHITE + report.getGenerationSessionId() + C.GRAY + " | Active leases: " + C.WHITE + report.getGenerationActiveLeases());
if (report.getEntryChunkX() != null && report.getEntryChunkZ() != null) {
sender().sendMessage(C.GRAY + "Entry chunk: " + C.WHITE + report.getEntryChunkX() + "," + report.getEntryChunkZ());
}
sender().sendMessage(C.GRAY + "Headless: " + C.WHITE + report.isHeadless() + C.GRAY + " | Player: " + C.WHITE + fallback(report.getPlayerName()));
sender().sendMessage(C.GRAY + "Retain on failure: " + C.WHITE + report.isRetainOnFailure() + C.GRAY + " | Cleanup applied: " + C.WHITE + report.isCleanupApplied());
sendDatapackReadiness(report.getDatapackReadiness());
if (!report.getNotes().isEmpty()) {
sender().sendMessage(C.GRAY + "Notes: " + C.WHITE + joinList(report.getNotes()));
}
if (report.getFailureType() != null && !report.getFailureType().isBlank()) {
sender().sendMessage(C.RED + "Failure: " + report.getFailureType() + C.GRAY + " - " + C.WHITE + fallback(report.getFailureMessage()));
if (!report.getFailureChain().isEmpty()) {
sender().sendMessage(C.RED + "Failure chain: " + C.WHITE + joinList(report.getFailureChain()));
}
}
}
private void sendDatapackReadiness(DatapackReadinessResult readiness) {
if (readiness == null) {
return;
}
sender().sendMessage(C.GRAY + "Datapack pack key: " + C.WHITE + fallback(readiness.getRequestedPackKey()));
sender().sendMessage(C.GRAY + "Datapack folders: " + C.WHITE + joinList(readiness.getResolvedDatapackFolders()));
sender().sendMessage(C.GRAY + "External datapack result: " + C.WHITE + fallback(readiness.getExternalDatapackInstallResult()));
sender().sendMessage(C.GRAY + "Verification passed: " + C.WHITE + readiness.isVerificationPassed() + C.GRAY + " | Restart required: " + C.WHITE + readiness.isRestartRequired());
if (!readiness.getMissingPaths().isEmpty()) {
sender().sendMessage(C.RED + "Missing datapack paths: " + C.WHITE + joinList(readiness.getMissingPaths()));
}
}
private String latestReportPath() {
if (Iris.instance == null) {
return "plugins/Iris/diagnostics/smoke/latest.json";
}
return Iris.instance.getDataFile("diagnostics", "smoke", "latest.json").getAbsolutePath();
}
private String joinList(List<String> values) {
if (values == null || values.isEmpty()) {
return "none";
}
return String.join(", ", values);
}
private String fallback(String value) {
if (value == null || value.isBlank()) {
return "none";
}
return value;
}
}

View File

@@ -26,9 +26,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.project.IrisProject;
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.ChunkReplacementListener;
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
@@ -141,8 +139,25 @@ public class CommandStudio implements DirectorExecutor {
return;
}
Iris.service(StudioSVC.class).close();
sender().sendMessage(C.GREEN + "Project Closed.");
sender().sendMessage(C.YELLOW + "Closing studio...");
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
if (throwable != null) {
sender().sendMessage(C.RED + "Studio close failed: " + throwable.getMessage());
return;
}
if (result != null && result.failureCause() != null) {
sender().sendMessage(C.RED + "Studio close failed: " + result.failureCause().getMessage());
return;
}
if (result != null && result.startupCleanupQueued()) {
sender().sendMessage(C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback.");
return;
}
sender().sendMessage(C.GREEN + "Studio closed.");
}));
}
@Director(description = "Create a new studio project", aliases = "+", sync = true)
@@ -455,17 +470,13 @@ public class CommandStudio implements DirectorExecutor {
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<>();
KMap<NoiseStyle, Double> styleTimings = new KMap<>();
@@ -644,30 +655,6 @@ 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<>();

View File

@@ -105,9 +105,37 @@ public class IrisLootEvent extends Event {
if (!Bukkit.isPrimaryThread()) {
Iris.warn("LootGenerateEvent was not called on the main thread, please report this issue.");
Thread.dumpStack();
J.sfut(() -> Bukkit.getPluginManager().callEvent(event)).join();
} else Bukkit.getPluginManager().callEvent(event);
J.sfut(() -> {
try {
Bukkit.getPluginManager().callEvent(event);
} catch (Throwable e) {
Iris.reportError("LootGenerateEvent dispatch failed at "
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e instanceof Error error) {
throw error;
}
throw new IllegalStateException(e);
}
}).join();
} else {
try {
Bukkit.getPluginManager().callEvent(event);
} catch (Throwable e) {
Iris.reportError("LootGenerateEvent dispatch failed at "
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e instanceof Error error) {
throw error;
}
throw new IllegalStateException(e);
}
}
return event.isCancelled();
}
}
}

View File

@@ -0,0 +1,58 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import java.util.concurrent.CompletableFuture;
final class BukkitPublicBackend implements WorldLifecycleBackend {
private final CapabilitySnapshot capabilities;
BukkitPublicBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
}
@Override
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
return true;
}
@Override
public CompletableFuture<World> create(WorldLifecycleRequest request) {
World existing = Bukkit.getWorld(request.worldName());
if (existing != null) {
return CompletableFuture.completedFuture(existing);
}
WorldCreator creator = request.toWorldCreator();
if (request.generator() != null) {
WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider());
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
}
try {
World world = creator.createWorld();
return CompletableFuture.completedFuture(world);
} catch (Throwable e) {
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
} finally {
WorldLifecycleStaging.clearAll(request.worldName());
}
}
@Override
public boolean unload(World world, boolean save) {
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
}
@Override
public String backendName() {
return "bukkit_public";
}
@Override
public String describeSelectionReason() {
return "public Bukkit world lifecycle path";
}
}

View File

@@ -0,0 +1,235 @@
package art.arcane.iris.core.lifecycle;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.function.Predicate;
final class CapabilityResolution {
private CapabilityResolution() {
}
static Method resolveCreateLevelMethod(Class<?> owner) throws NoSuchMethodException {
Method current = resolveMethod(owner, "createLevel", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 3
&& "LevelStem".equals(params[0].getSimpleName())
&& "WorldLoadingInfoAndData".equals(params[1].getSimpleName())
&& "WorldDataAndGenSettings".equals(params[2].getSimpleName());
});
if (current != null) {
return current;
}
Method legacy = resolveMethod(owner, "createLevel", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 4
&& "LevelStem".equals(params[0].getSimpleName())
&& "WorldLoadingInfo".equals(params[1].getSimpleName())
&& "LevelStorageAccess".equals(params[2].getSimpleName())
&& "PrimaryLevelData".equals(params[3].getSimpleName());
});
if (legacy != null) {
return legacy;
}
throw new NoSuchMethodException(owner.getName() + "#createLevel");
}
static Method resolveLevelStorageAccessMethod(Class<?> owner) throws NoSuchMethodException {
Method exactValidate = resolveMethod(owner, "validateAndCreateAccess", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 2
&& String.class.equals(params[0])
&& "ResourceKey".equals(params[1].getSimpleName())
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
});
if (exactValidate != null) {
return exactValidate;
}
Method oneArgValidate = resolveMethod(owner, "validateAndCreateAccess", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1
&& String.class.equals(params[0])
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
});
if (oneArgValidate != null) {
return oneArgValidate;
}
Method exactCreate = resolveMethod(owner, "createAccess", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 2
&& String.class.equals(params[0])
&& "ResourceKey".equals(params[1].getSimpleName())
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
});
if (exactCreate != null) {
return exactCreate;
}
Method oneArgCreate = resolveMethod(owner, "createAccess", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1
&& String.class.equals(params[0])
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
});
if (oneArgCreate != null) {
return oneArgCreate;
}
throw new NoSuchMethodException(owner.getName() + "#validateAndCreateAccess/createAccess");
}
static Method resolvePaperWorldDataMethod(Class<?> owner) throws NoSuchMethodException {
Method current = resolveMethod(owner, "loadWorldData", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 3
&& "MinecraftServer".equals(params[0].getSimpleName())
&& "ResourceKey".equals(params[1].getSimpleName())
&& String.class.equals(params[2])
&& "LoadedWorldData".equals(method.getReturnType().getSimpleName());
});
if (current != null) {
return current;
}
Method legacy = resolveMethod(owner, "getLevelData", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && "LevelStorageAccess".equals(params[0].getSimpleName());
});
if (legacy != null) {
return legacy;
}
throw new NoSuchMethodException(owner.getName() + "#loadWorldData/getLevelData");
}
static Constructor<?> resolveWorldLoadingInfoConstructor(Class<?> owner) throws NoSuchMethodException {
Constructor<?> current = resolveConstructor(owner, constructor -> {
Class<?>[] params = constructor.getParameterTypes();
return params.length == 4
&& "Environment".equals(params[0].getSimpleName())
&& "ResourceKey".equals(params[1].getSimpleName())
&& "ResourceKey".equals(params[2].getSimpleName())
&& boolean.class.equals(params[3]);
});
if (current != null) {
return current;
}
Constructor<?> legacy = resolveConstructor(owner, constructor -> {
Class<?>[] params = constructor.getParameterTypes();
return params.length == 5
&& int.class.equals(params[0])
&& String.class.equals(params[1])
&& String.class.equals(params[2])
&& "ResourceKey".equals(params[3].getSimpleName())
&& boolean.class.equals(params[4]);
});
if (legacy != null) {
return legacy;
}
throw new NoSuchMethodException(owner.getName() + "#<init>");
}
static Constructor<?> resolveWorldLoadingInfoAndDataConstructor(Class<?> owner) throws NoSuchMethodException {
Constructor<?> constructor = resolveConstructor(owner, candidate -> {
Class<?>[] params = candidate.getParameterTypes();
return params.length == 2
&& "WorldLoadingInfo".equals(params[0].getSimpleName())
&& "LoadedWorldData".equals(params[1].getSimpleName());
});
if (constructor == null) {
throw new NoSuchMethodException(owner.getName() + "#<init>");
}
return constructor;
}
static Method resolveCreateNewWorldDataMethod(Class<?> owner) throws NoSuchMethodException {
Method method = resolveMethod(owner, "createNewWorldData", candidate -> {
Class<?>[] params = candidate.getParameterTypes();
return params.length == 5
&& "DedicatedServerSettings".equals(params[0].getSimpleName())
&& "DataLoadContext".equals(params[1].getSimpleName())
&& "Registry".equals(params[2].getSimpleName())
&& boolean.class.equals(params[3])
&& boolean.class.equals(params[4]);
});
if (method == null) {
throw new NoSuchMethodException(owner.getName() + "#createNewWorldData");
}
return method;
}
static Method resolveServerRegistryAccessMethod(Class<?> owner) throws NoSuchMethodException {
Method method = resolveMethod(owner, "registryAccess", candidate -> candidate.getParameterCount() == 0
&& !void.class.equals(candidate.getReturnType()));
if (method == null) {
throw new NoSuchMethodException(owner.getName() + "#registryAccess");
}
return method;
}
static Method resolveMethod(Class<?> owner, String name, Predicate<Method> predicate) {
Method selected = scanMethods(owner.getMethods(), name, predicate);
if (selected != null) {
return selected;
}
Class<?> current = owner;
while (current != null) {
selected = scanMethods(current.getDeclaredMethods(), name, predicate);
if (selected != null) {
selected.setAccessible(true);
return selected;
}
current = current.getSuperclass();
}
return null;
}
static Field resolveField(Class<?> owner, String name) throws NoSuchFieldException {
Class<?> current = owner;
while (current != null) {
try {
Field field = current.getDeclaredField(name);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
}
}
throw new NoSuchFieldException(owner.getName() + "#" + name);
}
private static Method scanMethods(Method[] methods, String name, Predicate<Method> predicate) {
for (Method method : methods) {
if (!method.getName().equals(name)) {
continue;
}
if (predicate.test(method)) {
return method;
}
}
return null;
}
private static Constructor<?> resolveConstructor(Class<?> owner, Predicate<Constructor<?>> predicate) {
for (Constructor<?> constructor : owner.getConstructors()) {
if (predicate.test(constructor)) {
return constructor;
}
}
for (Constructor<?> constructor : owner.getDeclaredConstructors()) {
if (predicate.test(constructor)) {
constructor.setAccessible(true);
return constructor;
}
}
return null;
}
}

View File

@@ -0,0 +1,612 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.plugin.RegisteredServiceProvider;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Locale;
public final class CapabilitySnapshot {
public enum PaperLikeFlavor {
CURRENT_INFO_AND_DATA,
LEGACY_STORAGE_ACCESS,
UNSUPPORTED
}
private final ServerFamily serverFamily;
private final boolean regionizedRuntime;
private final Object worldsProvider;
private final Class<?> worldsLevelStemClass;
private final Class<?> worldsGeneratorTypeClass;
private final String worldsProviderResolution;
private final Object bukkitServer;
private final Object minecraftServer;
private final Method createLevelMethod;
private final PaperLikeFlavor paperLikeFlavor;
private final Class<?> paperWorldLoaderClass;
private final Method paperWorldDataMethod;
private final Constructor<?> worldLoadingInfoConstructor;
private final Constructor<?> worldLoadingInfoAndDataConstructor;
private final Method createNewWorldDataMethod;
private final Method levelStorageAccessMethod;
private final Field worldLoaderContextField;
private final Method serverRegistryAccessMethod;
private final Field settingsField;
private final Field optionsField;
private final Method isDemoMethod;
private final Method unloadWorldAsyncMethod;
private final Method chunkAtAsyncMethod;
private final Method removeLevelMethod;
private final String paperLikeResolution;
private CapabilitySnapshot(
ServerFamily serverFamily,
boolean regionizedRuntime,
Object worldsProvider,
Class<?> worldsLevelStemClass,
Class<?> worldsGeneratorTypeClass,
String worldsProviderResolution,
Object bukkitServer,
Object minecraftServer,
Method createLevelMethod,
PaperLikeFlavor paperLikeFlavor,
Class<?> paperWorldLoaderClass,
Method paperWorldDataMethod,
Constructor<?> worldLoadingInfoConstructor,
Constructor<?> worldLoadingInfoAndDataConstructor,
Method createNewWorldDataMethod,
Method levelStorageAccessMethod,
Field worldLoaderContextField,
Method serverRegistryAccessMethod,
Field settingsField,
Field optionsField,
Method isDemoMethod,
Method unloadWorldAsyncMethod,
Method chunkAtAsyncMethod,
Method removeLevelMethod,
String paperLikeResolution
) {
this.serverFamily = serverFamily;
this.regionizedRuntime = regionizedRuntime;
this.worldsProvider = worldsProvider;
this.worldsLevelStemClass = worldsLevelStemClass;
this.worldsGeneratorTypeClass = worldsGeneratorTypeClass;
this.worldsProviderResolution = worldsProviderResolution;
this.bukkitServer = bukkitServer;
this.minecraftServer = minecraftServer;
this.createLevelMethod = createLevelMethod;
this.paperLikeFlavor = paperLikeFlavor;
this.paperWorldLoaderClass = paperWorldLoaderClass;
this.paperWorldDataMethod = paperWorldDataMethod;
this.worldLoadingInfoConstructor = worldLoadingInfoConstructor;
this.worldLoadingInfoAndDataConstructor = worldLoadingInfoAndDataConstructor;
this.createNewWorldDataMethod = createNewWorldDataMethod;
this.levelStorageAccessMethod = levelStorageAccessMethod;
this.worldLoaderContextField = worldLoaderContextField;
this.serverRegistryAccessMethod = serverRegistryAccessMethod;
this.settingsField = settingsField;
this.optionsField = optionsField;
this.isDemoMethod = isDemoMethod;
this.unloadWorldAsyncMethod = unloadWorldAsyncMethod;
this.chunkAtAsyncMethod = chunkAtAsyncMethod;
this.removeLevelMethod = removeLevelMethod;
this.paperLikeResolution = paperLikeResolution;
}
public static CapabilitySnapshot probe() {
Server server = Bukkit.getServer();
Object bukkitServer = server;
boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server);
ServerFamily serverFamily = detectServerFamily(server, regionizedRuntime);
Object worldsProvider = null;
Class<?> worldsLevelStemClass = null;
Class<?> worldsGeneratorTypeClass = null;
String worldsProviderResolution = "inactive";
try {
Object[] worldsProviderData = resolveWorldsProvider();
worldsProvider = worldsProviderData[0];
worldsLevelStemClass = (Class<?>) worldsProviderData[1];
worldsGeneratorTypeClass = (Class<?>) worldsProviderData[2];
worldsProviderResolution = (String) worldsProviderData[3];
} catch (Throwable e) {
worldsProviderResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
}
Object minecraftServer = null;
Method createLevelMethod = null;
PaperLikeFlavor paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED;
Class<?> paperWorldLoaderClass = null;
Method paperWorldDataMethod = null;
Constructor<?> worldLoadingInfoConstructor = null;
Constructor<?> worldLoadingInfoAndDataConstructor = null;
Method createNewWorldDataMethod = null;
Method levelStorageAccessMethod = null;
Field worldLoaderContextField = null;
Method serverRegistryAccessMethod = null;
Field settingsField = null;
Field optionsField = null;
Method isDemoMethod = null;
Method removeLevelMethod = null;
String paperLikeResolution = "inactive";
try {
if (bukkitServer != null) {
Method getServerMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "getServer", method -> method.getParameterCount() == 0);
if (getServerMethod != null) {
minecraftServer = getServerMethod.invoke(bukkitServer);
}
}
if (minecraftServer != null) {
Class<?> minecraftServerClass = Class.forName("net.minecraft.server.MinecraftServer");
if (!minecraftServerClass.isInstance(minecraftServer)) {
throw new IllegalStateException("resolved server is not a MinecraftServer: " + minecraftServer.getClass().getName());
}
createLevelMethod = CapabilityResolution.resolveCreateLevelMethod(minecraftServer.getClass());
removeLevelMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "removeLevel", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && "ServerLevel".equals(params[0].getSimpleName());
});
worldLoaderContextField = CapabilityResolution.resolveField(minecraftServer.getClass(), "worldLoaderContext");
serverRegistryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(minecraftServer.getClass());
settingsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "settings");
optionsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "options");
isDemoMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "isDemo", method -> method.getParameterCount() == 0 && boolean.class.equals(method.getReturnType()));
Class<?> mainClass = Class.forName("net.minecraft.server.Main");
createNewWorldDataMethod = CapabilityResolution.resolveCreateNewWorldDataMethod(mainClass);
Class<?> paperLoaderCandidate = Class.forName("io.papermc.paper.world.PaperWorldLoader");
paperWorldLoaderClass = paperLoaderCandidate;
paperWorldDataMethod = CapabilityResolution.resolvePaperWorldDataMethod(paperLoaderCandidate);
Class<?> worldLoadingInfoClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo");
worldLoadingInfoConstructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(worldLoadingInfoClass);
if (createLevelMethod.getParameterCount() == 3) {
Class<?> worldLoadingInfoAndDataClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfoAndData");
worldLoadingInfoAndDataConstructor = CapabilityResolution.resolveWorldLoadingInfoAndDataConstructor(worldLoadingInfoAndDataClass);
paperLikeFlavor = PaperLikeFlavor.CURRENT_INFO_AND_DATA;
} else {
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
levelStorageAccessMethod = CapabilityResolution.resolveLevelStorageAccessMethod(levelStorageSourceClass);
paperLikeFlavor = PaperLikeFlavor.LEGACY_STORAGE_ACCESS;
}
paperLikeResolution = "available(flavor=" + paperLikeFlavor.name().toLowerCase(Locale.ROOT)
+ ", createLevel=" + createLevelMethod.toGenericString() + ")";
}
} catch (Throwable e) {
paperLikeResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
createLevelMethod = null;
paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED;
paperWorldLoaderClass = null;
paperWorldDataMethod = null;
worldLoadingInfoConstructor = null;
worldLoadingInfoAndDataConstructor = null;
createNewWorldDataMethod = null;
levelStorageAccessMethod = null;
worldLoaderContextField = null;
serverRegistryAccessMethod = null;
settingsField = null;
optionsField = null;
isDemoMethod = null;
removeLevelMethod = null;
}
Method unloadWorldAsyncMethod = null;
try {
if (bukkitServer != null) {
unloadWorldAsyncMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "unloadWorldAsync", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 3
&& World.class.equals(params[0])
&& boolean.class.equals(params[1])
&& "Consumer".equals(params[2].getSimpleName());
});
}
} catch (Throwable ignored) {
unloadWorldAsyncMethod = null;
}
Method chunkAtAsyncMethod = null;
try {
chunkAtAsyncMethod = CapabilityResolution.resolveMethod(World.class, "getChunkAtAsync", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 3
&& int.class.equals(params[0])
&& int.class.equals(params[1])
&& boolean.class.equals(params[2]);
});
} catch (Throwable ignored) {
chunkAtAsyncMethod = null;
}
return new CapabilitySnapshot(
serverFamily,
regionizedRuntime,
worldsProvider,
worldsLevelStemClass,
worldsGeneratorTypeClass,
worldsProviderResolution,
bukkitServer,
minecraftServer,
createLevelMethod,
paperLikeFlavor,
paperWorldLoaderClass,
paperWorldDataMethod,
worldLoadingInfoConstructor,
worldLoadingInfoAndDataConstructor,
createNewWorldDataMethod,
levelStorageAccessMethod,
worldLoaderContextField,
serverRegistryAccessMethod,
settingsField,
optionsField,
isDemoMethod,
unloadWorldAsyncMethod,
chunkAtAsyncMethod,
removeLevelMethod,
paperLikeResolution
);
}
public static CapabilitySnapshot forTesting(ServerFamily serverFamily, boolean regionizedRuntime, boolean worldsProviderHealthy, boolean paperLikeRuntimeHealthy) {
Object minecraftServer = paperLikeRuntimeHealthy ? new TestingPaperLikeServer("datapack-registry", "server-registry") : null;
Method createLevelMethod = null;
Field worldLoaderContextField = null;
Method serverRegistryAccessMethod = null;
try {
createLevelMethod = paperLikeRuntimeHealthy
? TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class)
: null;
worldLoaderContextField = paperLikeRuntimeHealthy
? CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext")
: null;
serverRegistryAccessMethod = paperLikeRuntimeHealthy
? CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class)
: null;
} catch (NoSuchMethodException | NoSuchFieldException e) {
throw new IllegalStateException(e);
}
return new CapabilitySnapshot(
serverFamily,
regionizedRuntime,
worldsProviderHealthy ? new Object() : null,
worldsProviderHealthy ? Object.class : null,
worldsProviderHealthy ? Object.class : null,
worldsProviderHealthy ? "test-provider" : "inactive",
null,
minecraftServer,
createLevelMethod,
paperLikeRuntimeHealthy ? PaperLikeFlavor.CURRENT_INFO_AND_DATA : PaperLikeFlavor.UNSUPPORTED,
null,
null,
null,
null,
null,
null,
worldLoaderContextField,
serverRegistryAccessMethod,
null,
null,
null,
null,
null,
null,
paperLikeRuntimeHealthy ? "available(test)" : "unsupported(test)"
);
}
public static CapabilitySnapshot forTestingRuntimeRegistries(ServerFamily serverFamily, boolean regionizedRuntime, Object datapackDimensions, Object serverRegistryAccess) {
TestingPaperLikeServer minecraftServer = new TestingPaperLikeServer(datapackDimensions, serverRegistryAccess);
Method createLevelMethod;
Field worldLoaderContextField;
Method registryAccessMethod;
try {
createLevelMethod = TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class);
worldLoaderContextField = CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext");
registryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class);
} catch (NoSuchMethodException | NoSuchFieldException e) {
throw new IllegalStateException(e);
}
return new CapabilitySnapshot(
serverFamily,
regionizedRuntime,
null,
null,
null,
"inactive",
null,
minecraftServer,
createLevelMethod,
PaperLikeFlavor.CURRENT_INFO_AND_DATA,
null,
null,
null,
null,
null,
null,
worldLoaderContextField,
registryAccessMethod,
null,
null,
null,
null,
null,
null,
"available(test-runtime-registries)"
);
}
public ServerFamily serverFamily() {
return serverFamily;
}
public boolean regionizedRuntime() {
return regionizedRuntime;
}
public Object worldsProvider() {
return worldsProvider;
}
public Class<?> worldsLevelStemClass() {
return worldsLevelStemClass;
}
public Class<?> worldsGeneratorTypeClass() {
return worldsGeneratorTypeClass;
}
public Object bukkitServer() {
return bukkitServer;
}
public Object minecraftServer() {
return minecraftServer;
}
public Method createLevelMethod() {
return createLevelMethod;
}
public PaperLikeFlavor paperLikeFlavor() {
return paperLikeFlavor;
}
public Class<?> paperWorldLoaderClass() {
return paperWorldLoaderClass;
}
public Method paperWorldDataMethod() {
return paperWorldDataMethod;
}
public Constructor<?> worldLoadingInfoConstructor() {
return worldLoadingInfoConstructor;
}
public Constructor<?> worldLoadingInfoAndDataConstructor() {
return worldLoadingInfoAndDataConstructor;
}
public Method createNewWorldDataMethod() {
return createNewWorldDataMethod;
}
public Method levelStorageAccessMethod() {
return levelStorageAccessMethod;
}
public Field worldLoaderContextField() {
return worldLoaderContextField;
}
public Method serverRegistryAccessMethod() {
return serverRegistryAccessMethod;
}
public Field settingsField() {
return settingsField;
}
public Field optionsField() {
return optionsField;
}
public Method isDemoMethod() {
return isDemoMethod;
}
public Method unloadWorldAsyncMethod() {
return unloadWorldAsyncMethod;
}
public Method chunkAtAsyncMethod() {
return chunkAtAsyncMethod;
}
public Method removeLevelMethod() {
return removeLevelMethod;
}
public boolean hasWorldsProvider() {
return worldsProvider != null && worldsLevelStemClass != null && worldsGeneratorTypeClass != null;
}
public boolean hasPaperLikeRuntime() {
return minecraftServer != null
&& createLevelMethod != null
&& serverRegistryAccessMethod != null
&& paperLikeFlavor != PaperLikeFlavor.UNSUPPORTED;
}
public String worldsProviderResolution() {
return worldsProviderResolution;
}
public String paperLikeResolution() {
return paperLikeResolution;
}
public String describe() {
return "family=" + serverFamily.id()
+ ", regionizedRuntime=" + regionizedRuntime
+ ", worldsProvider=" + worldsProviderResolution
+ ", paperLike=" + paperLikeResolution
+ ", serverRegistryAccess=" + (serverRegistryAccessMethod != null)
+ ", unloadAsync=" + (unloadWorldAsyncMethod != null)
+ ", chunkAsync=" + (chunkAtAsyncMethod != null);
}
private static ServerFamily detectServerFamily(Server server, boolean regionizedRuntime) {
String bukkitName = server == null ? "" : server.getName();
String bukkitVersion = server == null ? "" : server.getVersion();
String serverClassName = server == null ? "" : server.getClass().getName();
boolean canvasRuntime = hasCanvasRuntime();
if (containsIgnoreCase(bukkitName, "folia")
|| containsIgnoreCase(bukkitVersion, "folia")
|| containsIgnoreCase(serverClassName, "folia")) {
return ServerFamily.FOLIA;
}
if (canvasRuntime
|| containsIgnoreCase(bukkitName, "canvas")
|| containsIgnoreCase(bukkitVersion, "canvas")
|| containsIgnoreCase(serverClassName, "canvas")) {
return regionizedRuntime ? ServerFamily.CANVAS : ServerFamily.CANVAS;
}
if (containsIgnoreCase(bukkitName, "purpur")
|| containsIgnoreCase(bukkitVersion, "purpur")
|| containsIgnoreCase(serverClassName, "purpur")) {
return ServerFamily.PURPUR;
}
if (containsIgnoreCase(bukkitName, "paper")
|| containsIgnoreCase(bukkitVersion, "paper")
|| containsIgnoreCase(serverClassName, "paper")
|| containsIgnoreCase(bukkitName, "pufferfish")
|| containsIgnoreCase(bukkitVersion, "pufferfish")
|| containsIgnoreCase(serverClassName, "pufferfish")) {
return ServerFamily.PAPER;
}
if (containsIgnoreCase(bukkitName, "spigot")
|| containsIgnoreCase(bukkitVersion, "spigot")
|| containsIgnoreCase(serverClassName, "spigot")) {
return ServerFamily.SPIGOT;
}
if (containsIgnoreCase(bukkitName, "craftbukkit")
|| containsIgnoreCase(bukkitVersion, "craftbukkit")
|| containsIgnoreCase(serverClassName, "craftbukkit")
|| containsIgnoreCase(bukkitName, "bukkit")
|| containsIgnoreCase(bukkitVersion, "bukkit")) {
return ServerFamily.BUKKIT;
}
if (regionizedRuntime || J.isFolia()) {
return ServerFamily.FOLIA;
}
return ServerFamily.UNKNOWN;
}
private static boolean hasCanvasRuntime() {
try {
Class.forName("io.canvasmc.canvas.region.WorldRegionizer");
return true;
} catch (Throwable ignored) {
return false;
}
}
private static boolean containsIgnoreCase(String value, String needle) {
if (value == null || needle == null || needle.isEmpty()) {
return false;
}
return value.toLowerCase(Locale.ROOT).contains(needle.toLowerCase(Locale.ROOT));
}
private static Object[] resolveWorldsProvider() throws Throwable {
try {
Class<?> worldsProviderClass = Class.forName("net.thenextlvl.worlds.api.WorldsProvider");
Class<?> levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem");
Class<?> generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType");
Object provider = Bukkit.getServicesManager().load(worldsProviderClass);
String resolution = provider == null ? "inactive(service not registered)" : "active(service=" + provider.getClass().getName() + ")";
return new Object[]{provider, levelStemClass, generatorTypeClass, resolution};
} catch (Throwable ignored) {
}
Collection<Class<?>> knownServices = Bukkit.getServicesManager().getKnownServices();
for (Class<?> serviceClass : knownServices) {
if (!"net.thenextlvl.worlds.api.WorldsProvider".equals(serviceClass.getName())) {
continue;
}
RegisteredServiceProvider<?> registration = Bukkit.getServicesManager().getRegistration(serviceClass);
if (registration == null) {
continue;
}
Object provider = registration.getProvider();
ClassLoader loader = serviceClass.getClassLoader();
if (loader == null && provider != null) {
loader = provider.getClass().getClassLoader();
}
if (loader == null) {
continue;
}
Class<?> levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem", false, loader);
Class<?> generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType", false, loader);
return new Object[]{provider, levelStemClass, generatorTypeClass, "active(service-scan=" + provider.getClass().getName() + ")"};
}
return new Object[]{null, null, null, "inactive(service scan found nothing)"};
}
private static final class TestingPaperLikeServer {
private final TestingWorldLoaderContext worldLoaderContext;
private final Object registryAccess;
private TestingPaperLikeServer(Object datapackDimensions, Object registryAccess) {
this.worldLoaderContext = new TestingWorldLoaderContext(datapackDimensions);
this.registryAccess = registryAccess;
}
@SuppressWarnings("unused")
private void createLevel(Object levelStem, Object worldLoadingInfoAndData, Object worldDataAndGenSettings) {
}
@SuppressWarnings("unused")
private Object registryAccess() {
return registryAccess;
}
}
private static final class TestingWorldLoaderContext {
private final Object datapackDimensions;
private TestingWorldLoaderContext(Object datapackDimensions) {
this.datapackDimensions = datapackDimensions;
}
@SuppressWarnings("unused")
private Object datapackDimensions() {
return datapackDimensions;
}
}
}

View File

@@ -0,0 +1,89 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.Iris;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
private final CapabilitySnapshot capabilities;
PaperLikeRuntimeBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
}
@Override
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
return request.studio()
&& capabilities.serverFamily().isPaperLike()
&& capabilities.hasPaperLikeRuntime();
}
@Override
public CompletableFuture<World> create(WorldLifecycleRequest request) {
Object legacyStorageAccess = null;
try {
World existing = Bukkit.getWorld(request.worldName());
if (existing != null) {
return CompletableFuture.completedFuture(existing);
}
if (request.generator() == null) {
return CompletableFuture.failedFuture(new IllegalStateException("Runtime world creation requires a non-null chunk generator."));
}
WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider());
WorldLifecycleSupport.stageRuntimeConfiguration(request.worldName());
Iris.info("WorldLifecycle runtime LevelStem: world=" + request.worldName()
+ ", backend=paper_like_runtime, flavor=" + capabilities.paperLikeFlavor().name().toLowerCase(Locale.ROOT)
+ ", registrySource=" + WorldLifecycleSupport.runtimeLevelStemRegistrySource(request));
Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request);
Object stemKey = WorldLifecycleSupport.createRuntimeLevelStemKey(request.worldName());
if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) {
Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey);
Object loadedWorldData = capabilities.paperWorldDataMethod().invoke(null, capabilities.minecraftServer(), dimensionKey, request.worldName());
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, true);
Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData);
Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName());
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings);
} else {
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName());
Object primaryLevelData = WorldLifecycleSupport.createLegacyPrimaryLevelData(capabilities, legacyStorageAccess, request.worldName());
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(0, request.worldName(), request.environment().name().toLowerCase(Locale.ROOT), stemKey, true);
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfo, legacyStorageAccess, primaryLevelData);
}
World loadedWorld = Bukkit.getWorld(request.worldName());
if (loadedWorld == null) {
return CompletableFuture.failedFuture(new IllegalStateException("Paper-like runtime backend did not load world \"" + request.worldName() + "\"."));
}
return CompletableFuture.completedFuture(loadedWorld);
} catch (Throwable e) {
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
} finally {
WorldLifecycleStaging.clearGenerator(request.worldName());
WorldLifecycleSupport.closeLevelStorageAccess(legacyStorageAccess);
}
}
@Override
public boolean unload(World world, boolean save) {
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
}
@Override
public String backendName() {
return "paper_like_runtime";
}
@Override
public String describeSelectionReason() {
return "server family " + capabilities.serverFamily().id() + " exposes paper-like runtime world lifecycle capabilities";
}
}

View File

@@ -0,0 +1,21 @@
package art.arcane.iris.core.lifecycle;
import java.util.Locale;
public enum ServerFamily {
BUKKIT,
SPIGOT,
PAPER,
PURPUR,
FOLIA,
CANVAS,
UNKNOWN;
public boolean isPaperLike() {
return this == PAPER || this == PURPUR || this == FOLIA || this == CANVAS;
}
public String id() {
return name().toLowerCase(Locale.ROOT);
}
}

View File

@@ -0,0 +1,17 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.World;
import java.util.concurrent.CompletableFuture;
public interface WorldLifecycleBackend {
boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities);
CompletableFuture<World> create(WorldLifecycleRequest request);
boolean unload(World world, boolean save);
String backendName();
String describeSelectionReason();
}

View File

@@ -0,0 +1,7 @@
package art.arcane.iris.core.lifecycle;
public enum WorldLifecycleCaller {
STUDIO,
CREATE,
BENCHMARK
}

View File

@@ -0,0 +1,51 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
public record WorldLifecycleRequest(
String worldName,
World.Environment environment,
ChunkGenerator generator,
BiomeProvider biomeProvider,
WorldType worldType,
boolean generateStructures,
boolean hardcore,
long seed,
boolean studio,
boolean benchmark,
WorldLifecycleCaller callerKind
) {
public static WorldLifecycleRequest fromCreator(WorldCreator creator, boolean studio, boolean benchmark, WorldLifecycleCaller callerKind) {
return new WorldLifecycleRequest(
creator.name(),
creator.environment(),
creator.generator(),
creator.biomeProvider(),
creator.type(),
creator.generateStructures(),
creator.hardcore(),
creator.seed(),
studio,
benchmark,
callerKind
);
}
public WorldCreator toWorldCreator() {
WorldCreator creator = new WorldCreator(worldName)
.environment(environment)
.generateStructures(generateStructures)
.hardcore(hardcore)
.type(worldType)
.seed(seed)
.generator(generator);
if (biomeProvider != null) {
creator.biomeProvider(biomeProvider);
}
return creator;
}
}

View File

@@ -0,0 +1,178 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.Iris;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.World;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
public final class WorldLifecycleService {
private static volatile WorldLifecycleService instance;
private final CapabilitySnapshot capabilities;
private final WorldsProviderBackend worldsProviderBackend;
private final PaperLikeRuntimeBackend paperLikeRuntimeBackend;
private final BukkitPublicBackend bukkitPublicBackend;
private final List<WorldLifecycleBackend> backends;
private final Map<String, String> worldBackendByName;
public WorldLifecycleService(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
this.worldsProviderBackend = new WorldsProviderBackend(capabilities);
this.paperLikeRuntimeBackend = new PaperLikeRuntimeBackend(capabilities);
this.bukkitPublicBackend = new BukkitPublicBackend(capabilities);
this.backends = List.of(worldsProviderBackend, paperLikeRuntimeBackend, bukkitPublicBackend);
this.worldBackendByName = new ConcurrentHashMap<>();
}
public static WorldLifecycleService get() {
WorldLifecycleService current = instance;
if (current != null) {
return current;
}
synchronized (WorldLifecycleService.class) {
if (instance != null) {
return instance;
}
CapabilitySnapshot capabilities = CapabilitySnapshot.probe();
instance = new WorldLifecycleService(capabilities);
Iris.info("WorldLifecycle capabilities: %s", capabilities.describe());
return instance;
}
}
public CapabilitySnapshot capabilities() {
return capabilities;
}
public CompletableFuture<World> create(WorldLifecycleRequest request) {
WorldLifecycleBackend backend;
try {
backend = selectCreateBackend(request);
} catch (Throwable e) {
Iris.reportError("WorldLifecycle create backend selection failed for world=\"" + request.worldName()
+ "\", caller=" + request.callerKind().name().toLowerCase() + ".", e);
return CompletableFuture.failedFuture(e);
}
Iris.info("WorldLifecycle create: world=%s, caller=%s, backend=%s, reason=%s",
request.worldName(),
request.callerKind().name().toLowerCase(),
backend.backendName(),
backend.describeSelectionReason());
return backend.create(request).whenComplete((world, throwable) -> {
if (throwable != null) {
Throwable cause = WorldLifecycleSupport.unwrap(throwable);
Iris.reportError("WorldLifecycle create failed: world=\"" + request.worldName()
+ "\", caller=" + request.callerKind().name().toLowerCase()
+ ", backend=" + backend.backendName()
+ ", family=" + capabilities.serverFamily().id() + ".", cause);
return;
}
if (world != null) {
worldBackendByName.put(world.getName(), backend.backendName());
}
});
}
public World createBlocking(WorldLifecycleRequest request) {
try {
return create(request).join();
} catch (CompletionException e) {
throw new IllegalStateException(WorldLifecycleSupport.unwrap(e));
}
}
public boolean unload(World world, boolean save) {
if (!J.isPrimaryThread()) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
J.s(() -> {
try {
future.complete(unloadDirect(world, save));
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
return future.join();
}
return unloadDirect(world, save);
}
private boolean unloadDirect(World world, boolean save) {
WorldLifecycleBackend backend = selectUnloadBackend(world.getName());
Iris.info("WorldLifecycle unload: world=%s, backend=%s, reason=%s",
world.getName(),
backend.backendName(),
backend.describeSelectionReason());
boolean unloaded;
try {
unloaded = backend.unload(world, save);
} catch (Throwable e) {
Iris.reportError("WorldLifecycle unload failed: world=\"" + world.getName()
+ "\", backend=" + backend.backendName()
+ ", family=" + capabilities.serverFamily().id() + ".", e);
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e instanceof Error error) {
throw error;
}
throw new IllegalStateException(e);
}
if (unloaded) {
worldBackendByName.remove(world.getName());
}
return unloaded;
}
public String backendNameForWorld(String worldName) {
return selectUnloadBackend(worldName).backendName();
}
WorldLifecycleBackend selectCreateBackend(WorldLifecycleRequest request) {
if (worldsProviderBackend.supports(request, capabilities)) {
return worldsProviderBackend;
}
if (request.studio() && capabilities.serverFamily().isPaperLike()) {
if (!paperLikeRuntimeBackend.supports(request, capabilities)) {
throw new IllegalStateException("World lifecycle backend paper_like_runtime is unavailable for studio create on "
+ capabilities.serverFamily().id() + ": " + capabilities.paperLikeResolution());
}
return paperLikeRuntimeBackend;
}
for (WorldLifecycleBackend backend : backends) {
if (backend.supports(request, capabilities)) {
return backend;
}
}
throw new IllegalStateException("No world lifecycle backend supports request for \"" + request.worldName() + "\".");
}
WorldLifecycleBackend selectUnloadBackend(String worldName) {
String backendName = worldBackendByName.get(worldName);
if (backendName == null) {
return bukkitPublicBackend;
}
for (WorldLifecycleBackend backend : backends) {
if (backend.backendName().equals(backendName)) {
return backend;
}
}
return bukkitPublicBackend;
}
void rememberBackend(String worldName, String backendName) {
worldBackendByName.put(worldName, backendName);
}
}

View File

@@ -0,0 +1,60 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class WorldLifecycleStaging {
private static final Map<String, ChunkGenerator> stagedGenerators = new ConcurrentHashMap<>();
private static final Map<String, BiomeProvider> stagedBiomeProviders = new ConcurrentHashMap<>();
private static final Map<String, ChunkGenerator> stagedStemGenerators = new ConcurrentHashMap<>();
private WorldLifecycleStaging() {
}
public static void stageGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator, @Nullable BiomeProvider biomeProvider) {
stagedGenerators.put(worldName, generator);
if (biomeProvider != null) {
stagedBiomeProviders.put(worldName, biomeProvider);
} else {
stagedBiomeProviders.remove(worldName);
}
}
public static void stageStemGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator) {
stagedStemGenerators.put(worldName, generator);
}
@Nullable
public static ChunkGenerator consumeGenerator(@NotNull String worldName) {
return stagedGenerators.remove(worldName);
}
@Nullable
public static BiomeProvider consumeBiomeProvider(@NotNull String worldName) {
return stagedBiomeProviders.remove(worldName);
}
@Nullable
public static ChunkGenerator consumeStemGenerator(@NotNull String worldName) {
return stagedStemGenerators.remove(worldName);
}
public static void clearGenerator(@NotNull String worldName) {
stagedGenerators.remove(worldName);
stagedBiomeProviders.remove(worldName);
}
public static void clearStem(@NotNull String worldName) {
stagedStemGenerators.remove(worldName);
}
public static void clearAll(@NotNull String worldName) {
clearGenerator(worldName);
clearStem(worldName);
}
}

View File

@@ -0,0 +1,520 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.Iris;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.generator.ChunkGenerator;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
final class WorldLifecycleSupport {
private WorldLifecycleSupport() {
}
static Throwable unwrap(Throwable throwable) {
if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) {
return unwrap(invocationTargetException.getCause());
}
if (throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null) {
return unwrap(completionException.getCause());
}
if (throwable instanceof ExecutionException executionException && executionException.getCause() != null) {
return unwrap(executionException.getCause());
}
return throwable;
}
static Object invoke(Method method, Object target, Object... args) throws ReflectiveOperationException {
return method.invoke(target, args);
}
static Object invokeNamed(Object target, String methodName, Class<?>[] parameterTypes, Object... args) throws ReflectiveOperationException {
Method method = target.getClass().getMethod(methodName, parameterTypes);
return method.invoke(target, args);
}
static Object read(Field field, Object target) throws IllegalAccessException {
return field.get(target);
}
static void stageRuntimeConfiguration(String worldName) throws ReflectiveOperationException {
Object bukkitServer = Bukkit.getServer();
if (bukkitServer == null) {
throw new IllegalStateException("Bukkit server is unavailable.");
}
Field configurationField = CapabilityResolution.resolveField(bukkitServer.getClass(), "configuration");
Object rawConfiguration = configurationField.get(bukkitServer);
if (!(rawConfiguration instanceof YamlConfiguration configuration)) {
throw new IllegalStateException("CraftServer configuration field is unavailable.");
}
ConfigurationSection worldsSection = configuration.getConfigurationSection("worlds");
if (worldsSection == null) {
worldsSection = configuration.createSection("worlds");
}
ConfigurationSection worldSection = worldsSection.getConfigurationSection(worldName);
if (worldSection == null) {
worldSection = worldsSection.createSection(worldName);
}
worldSection.set("generator", "Iris:runtime");
}
static Object getRuntimeDatapackDimensions(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
Method datapackDimensionsMethod = CapabilityResolution.resolveMethod(worldLoaderContext.getClass(), "datapackDimensions", method -> method.getParameterCount() == 0);
if (datapackDimensionsMethod == null) {
throw new IllegalStateException("DataLoadContext does not expose datapackDimensions().");
}
Object datapackDimensions = datapackDimensionsMethod.invoke(worldLoaderContext);
if (datapackDimensions == null) {
throw new IllegalStateException("DataLoadContext.datapackDimensions() returned null.");
}
return datapackDimensions;
}
static Object getRuntimeServerRegistryAccess(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
Method registryAccessMethod = capabilities.serverRegistryAccessMethod();
if (registryAccessMethod == null) {
throw new IllegalStateException("MinecraftServer does not expose registryAccess().");
}
Object registryAccess = registryAccessMethod.invoke(capabilities.minecraftServer());
if (registryAccess == null) {
throw new IllegalStateException("MinecraftServer.registryAccess() returned null.");
}
return registryAccess;
}
static Object getRuntimeLevelStemRegistry(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
Object datapackDimensions = getRuntimeDatapackDimensions(capabilities);
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("LEVEL_STEM")
.get(null);
Method lookupMethod = CapabilityResolution.resolveMethod(datapackDimensions.getClass(), "lookupOrThrow", method -> method.getParameterCount() == 1);
if (lookupMethod == null) {
throw new IllegalStateException("Registry access does not expose lookupOrThrow(...).");
}
return lookupMethod.invoke(datapackDimensions, levelStemRegistryKey);
}
static Object createRuntimeLevelStemKey(String worldName) throws ReflectiveOperationException {
String sanitized = worldName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/_-]", "_");
String path = "runtime/" + sanitized;
Identifier identifier = new Identifier("iris", path);
Object rawIdentifier = Class.forName("net.minecraft.resources.Identifier")
.getMethod("fromNamespaceAndPath", String.class, String.class)
.invoke(null, identifier.namespace(), identifier.key());
Object registryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("LEVEL_STEM")
.get(null);
Method createMethod = Class.forName("net.minecraft.resources.ResourceKey")
.getMethod("create", registryKey.getClass(), rawIdentifier.getClass());
return createMethod.invoke(null, registryKey, rawIdentifier);
}
static Object createDimensionKey(Object stemKey) throws ReflectiveOperationException {
Class<?> resourceKeyClass = Class.forName("net.minecraft.resources.ResourceKey");
Method identifierMethod = CapabilityResolution.resolveMethod(resourceKeyClass, "identifier", method -> method.getParameterCount() == 0);
Object identifier = identifierMethod.invoke(stemKey);
Object dimensionRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("DIMENSION")
.get(null);
Method createMethod = resourceKeyClass.getMethod("create", dimensionRegistryKey.getClass(), identifier.getClass());
return createMethod.invoke(null, dimensionRegistryKey, identifier);
}
static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request) throws ReflectiveOperationException {
return resolveRuntimeLevelStem(capabilities, request, INMS.get());
}
static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request, INMSBinding binding) throws ReflectiveOperationException {
ChunkGenerator generator = request.generator();
if (generator instanceof PlatformChunkGenerator) {
Object registryAccess = getRuntimeServerRegistryAccess(capabilities);
try {
Object levelStem = binding.createRuntimeLevelStem(registryAccess, generator);
if (levelStem == null) {
throw new IllegalStateException("Iris NMS binding returned null runtime LevelStem.");
}
return levelStem;
} catch (Throwable e) {
throw new IllegalStateException("Failed to create runtime LevelStem from full server registry access for world \"" + request.worldName() + "\".", unwrap(e));
}
}
try {
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
Object overworldKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
.getField("OVERWORLD")
.get(null);
Method getValueMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "getValue", method -> method.getParameterCount() == 1);
if (getValueMethod != null) {
Object resolved = getValueMethod.invoke(levelStemRegistry, overworldKey);
if (resolved != null) {
return resolved;
}
}
Method getMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "get", method -> method.getParameterCount() == 1);
if (getMethod == null) {
throw new IllegalStateException("Unable to resolve OVERWORLD LevelStem from registry.");
}
Object raw = getMethod.invoke(levelStemRegistry, overworldKey);
return extractRegistryValue(raw);
} catch (Throwable e) {
throw new IllegalStateException("Failed to resolve fallback OVERWORLD LevelStem from datapack registry access for world \"" + request.worldName() + "\".", unwrap(e));
}
}
static String runtimeLevelStemRegistrySource(WorldLifecycleRequest request) {
if (request.generator() instanceof PlatformChunkGenerator) {
return "full_server_registry";
}
return "datapack_level_stem_registry";
}
static Object extractRegistryValue(Object raw) throws ReflectiveOperationException {
if (raw == null) {
return null;
}
if (raw instanceof Optional<?> optional) {
Object nested = optional.orElse(null);
if (nested == null) {
return null;
}
return extractRegistryValue(nested);
}
Method valueMethod = CapabilityResolution.resolveMethod(raw.getClass(), "value", method -> method.getParameterCount() == 0);
if (valueMethod != null) {
return valueMethod.invoke(raw);
}
return raw;
}
static void applyWorldDataNameAndModInfo(CapabilitySnapshot capabilities, Object worldDataAndGenSettings, String worldName) throws ReflectiveOperationException {
Method dataMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "data", method -> method.getParameterCount() == 0);
if (dataMethod == null) {
return;
}
Object worldData = dataMethod.invoke(worldDataAndGenSettings);
if (worldData == null) {
return;
}
Method checkNameMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "checkName", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && String.class.equals(params[0]);
});
if (checkNameMethod != null) {
checkNameMethod.invoke(worldData, worldName);
}
Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0);
Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0);
if (getModdedStatusMethod == null || getServerModNameMethod == null) {
return;
}
Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer());
Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0);
Method setModdedInfoMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "setModdedInfo", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]);
});
if (shouldReportAsModifiedMethod == null || setModdedInfoMethod == null) {
return;
}
boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck));
String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer());
setModdedInfoMethod.invoke(worldData, modName, modified);
}
static Object createCurrentWorldDataAndSettings(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer()));
Object options = read(capabilities.optionsField(), capabilities.minecraftServer());
Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && String.class.equals(params[0]);
});
boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest"));
Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0);
if (cookieMethod == null) {
throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie().");
}
Object worldDataAndGenSettings = cookieMethod.invoke(dataLoadOutput);
applyWorldDataNameAndModInfo(capabilities, worldDataAndGenSettings, worldName);
return worldDataAndGenSettings;
}
static Object createLegacyPrimaryLevelData(CapabilitySnapshot capabilities, Object levelStorageAccess, String worldName) throws ReflectiveOperationException {
Object levelDataResult = capabilities.paperWorldDataMethod().invoke(null, levelStorageAccess);
Method fatalErrorMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "fatalError", method -> method.getParameterCount() == 0);
Method dataTagMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "dataTag", method -> method.getParameterCount() == 0);
if (fatalErrorMethod != null && Boolean.TRUE.equals(fatalErrorMethod.invoke(levelDataResult))) {
throw new IllegalStateException("Paper runtime world-data helper reported a fatal error for \"" + worldName + "\".");
}
if (dataTagMethod != null && dataTagMethod.invoke(levelDataResult) != null) {
throw new IllegalStateException("Runtime world \"" + worldName + "\" already contains level data.");
}
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer()));
Object options = read(capabilities.optionsField(), capabilities.minecraftServer());
Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && String.class.equals(params[0]);
});
boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest"));
Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0);
if (cookieMethod == null) {
throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie().");
}
Object primaryLevelData = cookieMethod.invoke(dataLoadOutput);
Method checkNameMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "checkName", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 1 && String.class.equals(params[0]);
});
if (checkNameMethod != null) {
checkNameMethod.invoke(primaryLevelData, worldName);
}
Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0);
Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0);
if (getModdedStatusMethod != null && getServerModNameMethod != null) {
Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer());
Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0);
Method setModdedInfoMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "setModdedInfo", method -> {
Class<?>[] params = method.getParameterTypes();
return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]);
});
if (shouldReportAsModifiedMethod != null && setModdedInfoMethod != null) {
boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck));
String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer());
setModdedInfoMethod.invoke(primaryLevelData, modName, modified);
}
}
return primaryLevelData;
}
static Object createLegacyStorageAccess(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
Method createDefaultMethod = levelStorageSourceClass.getMethod("createDefault", Path.class);
Object levelStorageSource = createDefaultMethod.invoke(null, Bukkit.getWorldContainer().toPath());
Method storageAccessMethod = capabilities.levelStorageAccessMethod();
if (storageAccessMethod.getParameterCount() == 1) {
return storageAccessMethod.invoke(levelStorageSource, worldName);
}
Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
.getField("OVERWORLD")
.get(null);
return storageAccessMethod.invoke(levelStorageSource, worldName, overworldStemKey);
}
static void closeLevelStorageAccess(Object levelStorageAccess) {
if (levelStorageAccess == null) {
return;
}
try {
Method closeMethod = levelStorageAccess.getClass().getMethod("close");
closeMethod.invoke(levelStorageAccess);
} catch (Throwable ignored) {
}
}
static boolean unloadWorld(CapabilitySnapshot capabilities, World world, boolean save) {
if (world == null) {
return false;
}
CompletableFuture<Boolean> asyncUnload = unloadWorldViaAsyncApi(capabilities, world, save);
if (asyncUnload != null) {
return resolveAsyncUnload(asyncUnload);
}
try {
return Bukkit.unloadWorld(world, save);
} catch (UnsupportedOperationException unsupported) {
if (capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null) {
throw unsupported;
}
}
try {
if (save) {
world.save();
}
Method getHandleMethod = world.getClass().getMethod("getHandle");
Object serverLevel = getHandleMethod.invoke(world);
closeServerLevel(world, serverLevel);
detachServerLevel(capabilities, serverLevel, world.getName());
return Bukkit.getWorld(world.getName()) == null;
} catch (Throwable e) {
throw new IllegalStateException("Failed to unload world \"" + world.getName() + "\" through the selected world lifecycle backend.", unwrap(e));
}
}
private static CompletableFuture<Boolean> unloadWorldViaAsyncApi(CapabilitySnapshot capabilities, World world, boolean save) {
if (capabilities.unloadWorldAsyncMethod() == null || capabilities.bukkitServer() == null) {
return null;
}
CompletableFuture<Boolean> callbackFuture = new CompletableFuture<>();
Runnable invokeTask = () -> {
Consumer<Boolean> callback = result -> callbackFuture.complete(Boolean.TRUE.equals(result));
try {
capabilities.unloadWorldAsyncMethod().invoke(capabilities.bukkitServer(), world, save, callback);
} catch (Throwable e) {
callbackFuture.completeExceptionally(unwrap(e));
}
};
if (J.isFolia() && !isGlobalTickThread()) {
CompletableFuture<Void> scheduled = J.sfut(invokeTask);
if (scheduled == null) {
callbackFuture.completeExceptionally(new IllegalStateException("Failed to schedule global unload task."));
return callbackFuture;
}
scheduled.whenComplete((unused, throwable) -> {
if (throwable != null) {
callbackFuture.completeExceptionally(unwrap(throwable));
}
});
return callbackFuture;
}
invokeTask.run();
return callbackFuture;
}
private static boolean resolveAsyncUnload(CompletableFuture<Boolean> asyncUnload) {
if (J.isPrimaryThread()) {
if (!asyncUnload.isDone()) {
return true;
}
try {
return Boolean.TRUE.equals(asyncUnload.join());
} catch (Throwable e) {
throw new IllegalStateException("Failed to consume async world unload result.", unwrap(e));
}
}
try {
return Boolean.TRUE.equals(asyncUnload.get(120, TimeUnit.SECONDS));
} catch (Throwable e) {
throw new IllegalStateException("Failed while waiting for async world unload result.", unwrap(e));
}
}
private static void closeServerLevel(World world, Object serverLevel) throws Throwable {
Method closeMethod = CapabilityResolution.resolveMethod(serverLevel.getClass(), "close", method -> method.getParameterCount() == 0);
if (closeMethod == null) {
return;
}
if (!J.isFolia()) {
closeMethod.invoke(serverLevel);
return;
}
Location spawn = world.getSpawnLocation();
int chunkX = spawn == null ? 0 : spawn.getBlockX() >> 4;
int chunkZ = spawn == null ? 0 : spawn.getBlockZ() >> 4;
CompletableFuture<Void> closeFuture = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
closeMethod.invoke(serverLevel);
closeFuture.complete(null);
} catch (Throwable e) {
closeFuture.completeExceptionally(unwrap(e));
}
});
if (!scheduled) {
throw new IllegalStateException("Failed to schedule region close task for world \"" + world.getName() + "\".");
}
closeFuture.get(90, TimeUnit.SECONDS);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static void removeWorldFromCraftServerMap(String worldName) throws ReflectiveOperationException {
Object bukkitServer = Bukkit.getServer();
if (bukkitServer == null) {
return;
}
Field worldsField = CapabilityResolution.resolveField(bukkitServer.getClass(), "worlds");
Object rawWorlds = worldsField.get(bukkitServer);
if (rawWorlds instanceof Map map) {
map.remove(worldName);
map.remove(worldName.toLowerCase(Locale.ROOT));
}
}
private static void detachServerLevel(CapabilitySnapshot capabilities, Object serverLevel, String worldName) throws Throwable {
Runnable detachTask = () -> {
try {
capabilities.removeLevelMethod().invoke(capabilities.minecraftServer(), serverLevel);
removeWorldFromCraftServerMap(worldName);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
if (!J.isFolia() || isGlobalTickThread()) {
detachTask.run();
return;
}
CompletableFuture<Void> detachFuture = J.sfut(detachTask);
if (detachFuture == null) {
throw new IllegalStateException("Failed to schedule global detach task for world \"" + worldName + "\".");
}
detachFuture.get(15, TimeUnit.SECONDS);
}
static boolean isGlobalTickThread() {
Object server = Bukkit.getServer();
if (server == null) {
return false;
}
try {
Method method = server.getClass().getMethod("isGlobalTickThread");
return Boolean.TRUE.equals(method.invoke(server));
} catch (Throwable ignored) {
return false;
}
}
}

View File

@@ -0,0 +1,95 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.WorldType;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
final class WorldsProviderBackend implements WorldLifecycleBackend {
private final CapabilitySnapshot capabilities;
WorldsProviderBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
}
@Override
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
return request.studio() && capabilities.hasWorldsProvider();
}
@Override
@SuppressWarnings("unchecked")
public CompletableFuture<World> create(WorldLifecycleRequest request) {
try {
Path worldPath = new File(Bukkit.getWorldContainer(), request.worldName()).toPath();
Object builder = WorldLifecycleSupport.invokeNamed(capabilities.worldsProvider(), "levelBuilder", new Class[]{Path.class}, worldPath);
builder = WorldLifecycleSupport.invokeNamed(builder, "name", new Class[]{String.class}, request.worldName());
builder = WorldLifecycleSupport.invokeNamed(builder, "seed", new Class[]{long.class}, request.seed());
builder = WorldLifecycleSupport.invokeNamed(builder, "levelStem", new Class[]{capabilities.worldsLevelStemClass()}, resolveLevelStem(request.environment()));
builder = WorldLifecycleSupport.invokeNamed(builder, "chunkGenerator", new Class[]{org.bukkit.generator.ChunkGenerator.class}, request.generator());
builder = WorldLifecycleSupport.invokeNamed(builder, "biomeProvider", new Class[]{org.bukkit.generator.BiomeProvider.class}, request.biomeProvider());
builder = WorldLifecycleSupport.invokeNamed(builder, "generatorType", new Class[]{capabilities.worldsGeneratorTypeClass()}, resolveGeneratorType(request.worldType()));
builder = WorldLifecycleSupport.invokeNamed(builder, "structures", new Class[]{boolean.class}, request.generateStructures());
builder = WorldLifecycleSupport.invokeNamed(builder, "hardcore", new Class[]{boolean.class}, request.hardcore());
Object levelBuilder = WorldLifecycleSupport.invokeNamed(builder, "build", new Class[0]);
Object async = WorldLifecycleSupport.invokeNamed(levelBuilder, "createAsync", new Class[0]);
if (async instanceof CompletableFuture<?> future) {
return future.thenApply(world -> (World) world);
}
return CompletableFuture.failedFuture(new IllegalStateException("Worlds provider createAsync did not return CompletableFuture."));
} catch (Throwable e) {
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
}
}
@Override
public boolean unload(World world, boolean save) {
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
}
@Override
public String backendName() {
return "worlds_provider";
}
@Override
public String describeSelectionReason() {
return "external Worlds provider is registered and healthy";
}
@SuppressWarnings({"rawtypes", "unchecked"})
private Object resolveLevelStem(World.Environment environment) {
String key;
if (environment == World.Environment.NETHER) {
key = "NETHER";
} else if (environment == World.Environment.THE_END) {
key = "END";
} else {
key = "OVERWORLD";
}
Class<? extends Enum> enumClass = capabilities.worldsLevelStemClass().asSubclass(Enum.class);
return Enum.valueOf(enumClass, key);
}
@SuppressWarnings({"rawtypes", "unchecked"})
private Object resolveGeneratorType(WorldType worldType) {
String typeName = worldType == null ? "NORMAL" : worldType.getName();
String key;
if ("FLAT".equalsIgnoreCase(typeName)) {
key = "FLAT";
} else if ("AMPLIFIED".equalsIgnoreCase(typeName)) {
key = "AMPLIFIED";
} else if ("LARGE_BIOMES".equalsIgnoreCase(typeName) || "LARGEBIOMES".equalsIgnoreCase(typeName)) {
key = "LARGE_BIOMES";
} else {
key = "NORMAL";
}
Class<? extends Enum> enumClass = capabilities.worldsGeneratorTypeClass().asSubclass(Enum.class);
return Enum.valueOf(enumClass, key.toUpperCase(Locale.ROOT));
}
}

View File

@@ -157,7 +157,7 @@ public abstract class ExternalDataProvider implements Listener {
protected static List<BlockProperty> YAW_FACE_BIOME_PROPERTIES = List.of(
BlockProperty.ofEnum(BiomeColor.class, "matchBiome", null),
BlockProperty.ofBoolean("randomYaw", false),
BlockProperty.ofFloat("yaw", 0, 0, 360f, false, true),
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
BlockProperty.ofBoolean("randomFace", true),
new BlockProperty(
"face",

View File

@@ -1,841 +0,0 @@
package art.arcane.iris.core.link;
import art.arcane.iris.Iris;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.World;
import org.bukkit.WorldCreator;
import org.bukkit.WorldType;
import org.bukkit.generator.ChunkGenerator;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
public class FoliaWorldsLink {
private static volatile FoliaWorldsLink instance;
private final Object provider;
private final Class<?> levelStemClass;
private final Class<?> generatorTypeClass;
private final Object minecraftServer;
private final Method minecraftServerCreateLevelMethod;
private FoliaWorldsLink(
Object provider,
Class<?> levelStemClass,
Class<?> generatorTypeClass,
Object minecraftServer,
Method minecraftServerCreateLevelMethod
) {
this.provider = provider;
this.levelStemClass = levelStemClass;
this.generatorTypeClass = generatorTypeClass;
this.minecraftServer = minecraftServer;
this.minecraftServerCreateLevelMethod = minecraftServerCreateLevelMethod;
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static FoliaWorldsLink get() {
FoliaWorldsLink current = instance;
if (current != null && current.isActive()) {
return current;
}
synchronized (FoliaWorldsLink.class) {
if (instance != null && instance.isActive()) {
return instance;
}
Object loadedProvider = null;
Class<?> loadedLevelStemClass = null;
Class<?> loadedGeneratorTypeClass = null;
Object loadedMinecraftServer = null;
Method loadedMinecraftServerCreateLevelMethod = null;
try {
Server.class.getDeclaredMethod("isGlobalTickThread");
try {
Class<?> worldsProviderClass = Class.forName("net.thenextlvl.worlds.api.WorldsProvider");
loadedLevelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem");
loadedGeneratorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType");
loadedProvider = Bukkit.getServicesManager().load((Class) worldsProviderClass);
} catch (Throwable ignored) {
Object[] resolved = resolveProviderFromServices();
loadedProvider = resolved[0];
loadedLevelStemClass = (Class<?>) resolved[1];
loadedGeneratorTypeClass = (Class<?>) resolved[2];
}
} catch (Throwable ignored) {
}
try {
Object bukkitServer = Bukkit.getServer();
if (bukkitServer != null) {
Method getServerMethod = bukkitServer.getClass().getMethod("getServer");
Object candidateMinecraftServer = getServerMethod.invoke(bukkitServer);
Class<?> minecraftServerClass = Class.forName("net.minecraft.server.MinecraftServer");
if (minecraftServerClass.isInstance(candidateMinecraftServer)) {
loadedMinecraftServerCreateLevelMethod = minecraftServerClass.getMethod(
"createLevel",
Class.forName("net.minecraft.world.level.dimension.LevelStem"),
Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo"),
Class.forName("net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess"),
Class.forName("net.minecraft.world.level.storage.PrimaryLevelData")
);
loadedMinecraftServer = candidateMinecraftServer;
}
}
} catch (Throwable ignored) {
}
instance = new FoliaWorldsLink(
loadedProvider,
loadedLevelStemClass,
loadedGeneratorTypeClass,
loadedMinecraftServer,
loadedMinecraftServerCreateLevelMethod
);
return instance;
}
}
public boolean isActive() {
if (!J.isFolia()) {
return false;
}
return isWorldsProviderActive() || isPaperWorldLoaderActive();
}
public CompletableFuture<World> createWorld(WorldCreator creator) {
if (isWorldsProviderActive()) {
CompletableFuture<World> providerFuture = createWorldViaProvider(creator);
if (providerFuture != null) {
return providerFuture;
}
}
if (isPaperWorldLoaderActive()) {
return createWorldViaPaperWorldLoader(creator);
}
return null;
}
public boolean unloadWorld(World world, boolean save) {
if (world == null) {
return false;
}
CompletableFuture<Boolean> asyncWorldUnload = unloadWorldViaAsyncApi(world, save);
if (asyncWorldUnload != null) {
return resolveAsyncUnload(asyncWorldUnload);
}
try {
return Bukkit.unloadWorld(world, save);
} catch (UnsupportedOperationException unsupported) {
if (minecraftServer == null) {
throw unsupported;
}
}
try {
if (save) {
world.save();
}
Object serverLevel = invoke(world, "getHandle");
closeServerLevel(world, serverLevel);
detachServerLevel(serverLevel, world.getName());
return Bukkit.getWorld(world.getName()) == null;
} catch (Throwable e) {
throw new IllegalStateException("Failed to unload world \"" + world.getName() + "\" via Folia runtime world-loader bridge.", unwrap(e));
}
}
private boolean resolveAsyncUnload(CompletableFuture<Boolean> asyncWorldUnload) {
if (J.isPrimaryThread()) {
if (!asyncWorldUnload.isDone()) {
return true;
}
try {
return Boolean.TRUE.equals(asyncWorldUnload.join());
} catch (Throwable e) {
throw new IllegalStateException("Failed to consume async world unload result.", unwrap(e));
}
}
try {
return Boolean.TRUE.equals(asyncWorldUnload.get(120, TimeUnit.SECONDS));
} catch (Throwable e) {
throw new IllegalStateException("Failed while waiting for async world unload result.", unwrap(e));
}
}
private CompletableFuture<Boolean> unloadWorldViaAsyncApi(World world, boolean save) {
Object bukkitServer = Bukkit.getServer();
if (bukkitServer == null) {
return null;
}
Method unloadWorldAsyncMethod;
try {
unloadWorldAsyncMethod = bukkitServer.getClass().getMethod("unloadWorldAsync", World.class, boolean.class, Consumer.class);
} catch (Throwable ignored) {
return null;
}
CompletableFuture<Boolean> callbackFuture = new CompletableFuture<>();
Runnable invokeTask = () -> {
Consumer<Boolean> callback = result -> callbackFuture.complete(Boolean.TRUE.equals(result));
try {
unloadWorldAsyncMethod.invoke(bukkitServer, world, save, callback);
} catch (Throwable e) {
callbackFuture.completeExceptionally(unwrap(e));
}
};
if (J.isFolia() && !isGlobalTickThread()) {
CompletableFuture<Void> scheduled = J.sfut(invokeTask);
if (scheduled == null) {
callbackFuture.completeExceptionally(new IllegalStateException("Failed to schedule global world-unload task."));
return callbackFuture;
}
scheduled.whenComplete((unused, throwable) -> {
if (throwable != null) {
callbackFuture.completeExceptionally(unwrap(throwable));
}
});
} else {
invokeTask.run();
}
return callbackFuture;
}
private boolean isWorldsProviderActive() {
return provider != null && levelStemClass != null && generatorTypeClass != null;
}
private boolean isPaperWorldLoaderActive() {
return minecraftServer != null && minecraftServerCreateLevelMethod != null;
}
private CompletableFuture<World> createWorldViaProvider(WorldCreator creator) {
try {
Path worldPath = new File(Bukkit.getWorldContainer(), creator.name()).toPath();
Object builder = invoke(provider, "levelBuilder", worldPath);
builder = invoke(builder, "name", creator.name());
builder = invoke(builder, "seed", creator.seed());
builder = invoke(builder, "levelStem", resolveLevelStem(creator.environment()));
builder = invoke(builder, "chunkGenerator", creator.generator());
builder = invoke(builder, "biomeProvider", creator.biomeProvider());
builder = invoke(builder, "generatorType", resolveGeneratorType(creator.type()));
builder = invoke(builder, "structures", creator.generateStructures());
builder = invoke(builder, "hardcore", creator.hardcore());
Object levelBuilder = invoke(builder, "build");
Object async = invoke(levelBuilder, "createAsync");
if (async instanceof CompletableFuture<?> future) {
return future.thenApply(world -> (World) world);
}
return CompletableFuture.failedFuture(new IllegalStateException("Worlds provider createAsync did not return CompletableFuture."));
} catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
}
private CompletableFuture<World> createWorldViaPaperWorldLoader(WorldCreator creator) {
Object levelStorageAccess = null;
try {
if (creator.environment() != World.Environment.NORMAL) {
return CompletableFuture.failedFuture(new UnsupportedOperationException("PaperWorldLoader fallback only supports OVERWORLD worlds."));
}
World existing = Bukkit.getWorld(creator.name());
if (existing != null) {
return CompletableFuture.completedFuture(existing);
}
stageRuntimeGenerator(creator);
levelStorageAccess = createRuntimeStorageAccess(creator.name());
Object primaryLevelData = createPrimaryLevelData(levelStorageAccess, creator.name());
Object runtimeStemKey = createRuntimeLevelStemKey(creator.name());
Object worldLoadingInfo = createWorldLoadingInfo(creator.name(), runtimeStemKey);
Object levelStem = resolveCreateLevelStem(creator);
Object[] createLevelArgs = new Object[]{levelStem, worldLoadingInfo, levelStorageAccess, primaryLevelData};
Method createLevelMethod = minecraftServerCreateLevelMethod;
if (createLevelMethod == null || !matches(createLevelMethod.getParameterTypes(), createLevelArgs)) {
createLevelMethod = resolveMethod(minecraftServer.getClass(), "createLevel", createLevelArgs);
}
try {
createLevelMethod.invoke(minecraftServer, createLevelArgs);
} catch (IllegalArgumentException exception) {
throw new IllegalStateException("createLevel argument mismatch. Method=" + formatMethod(createLevelMethod) + " Args=" + formatArgs(createLevelArgs), exception);
}
World loaded = Bukkit.getWorld(creator.name());
if (loaded == null) {
Iris.clearStagedRuntimeWorldGenerator(creator.name());
closeLevelStorageAccess(levelStorageAccess);
return CompletableFuture.failedFuture(new IllegalStateException("PaperWorldLoader did not load world \"" + creator.name() + "\"."));
}
Iris.clearStagedRuntimeWorldGenerator(creator.name());
return CompletableFuture.completedFuture(loaded);
} catch (Throwable e) {
Iris.clearStagedRuntimeWorldGenerator(creator.name());
closeLevelStorageAccess(levelStorageAccess);
return CompletableFuture.failedFuture(unwrap(e));
}
}
private Object createRuntimeStorageAccess(String worldName) throws ReflectiveOperationException {
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
Object levelStorageSource = levelStorageSourceClass
.getMethod("createDefault", Path.class)
.invoke(null, Bukkit.getWorldContainer().toPath());
Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
.getField("OVERWORLD")
.get(null);
Method validateAndCreateAccess = resolveMethod(levelStorageSourceClass, "validateAndCreateAccess", worldName, overworldStemKey);
return validateAndCreateAccess.invoke(levelStorageSource, worldName, overworldStemKey);
}
private Object createPrimaryLevelData(Object levelStorageAccess, String worldName) throws ReflectiveOperationException {
Class<?> paperWorldLoaderClass = Class.forName("io.papermc.paper.world.PaperWorldLoader");
Class<?> levelStorageAccessClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess");
Object levelDataResult = paperWorldLoaderClass
.getMethod("getLevelData", levelStorageAccessClass)
.invoke(null, levelStorageAccess);
boolean fatalError = (boolean) invoke(levelDataResult, "fatalError");
if (fatalError) {
throw new IllegalStateException("PaperWorldLoader reported a fatal world-data error for \"" + worldName + "\".");
}
Object dataTag = invoke(levelDataResult, "dataTag");
if (dataTag != null) {
throw new IllegalStateException("Runtime studio world folder \"" + worldName + "\" already contains level data.");
}
Object worldLoaderContext = getPublicField(minecraftServer, "worldLoaderContext");
Object datapackDimensions = invoke(worldLoaderContext, "datapackDimensions");
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("LEVEL_STEM")
.get(null);
Object levelStemRegistry = invoke(datapackDimensions, "lookupOrThrow", levelStemRegistryKey);
Object dedicatedSettings = getPublicField(minecraftServer, "settings");
boolean demo = (boolean) invoke(minecraftServer, "isDemo");
Object options = getPublicField(minecraftServer, "options");
boolean bonusChest = (boolean) invoke(options, "has", "bonusChest");
Class<?> mainClass = Class.forName("net.minecraft.server.Main");
Method createNewWorldDataMethod = resolveMethod(mainClass, "createNewWorldData", dedicatedSettings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
Object dataLoadOutput = createNewWorldDataMethod.invoke(null, dedicatedSettings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
Object primaryLevelData = invoke(dataLoadOutput, "cookie");
invoke(primaryLevelData, "checkName", worldName);
Object modCheck = invoke(minecraftServer, "getModdedStatus");
boolean modified = (boolean) invoke(modCheck, "shouldReportAsModified");
String modName = (String) invoke(minecraftServer, "getServerModName");
invoke(primaryLevelData, "setModdedInfo", modName, modified);
return primaryLevelData;
}
private Object createWorldLoadingInfo(String worldName, Object runtimeStemKey) throws ReflectiveOperationException {
Class<?> worldLoadingInfoClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo");
Constructor<?> constructor = resolveConstructor(worldLoadingInfoClass, 0, worldName, "normal", runtimeStemKey, true);
return constructor.newInstance(0, worldName, "normal", runtimeStemKey, true);
}
private Object createRuntimeLevelStemKey(String worldName) throws ReflectiveOperationException {
String sanitized = worldName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/_-]", "_");
String path = "runtime/" + sanitized;
Object identifier = Class.forName("net.minecraft.resources.Identifier")
.getMethod("fromNamespaceAndPath", String.class, String.class)
.invoke(null, "iris", path);
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("LEVEL_STEM")
.get(null);
Class<?> resourceKeyClass = Class.forName("net.minecraft.resources.ResourceKey");
Method createMethod = resolveMethod(resourceKeyClass, "create", levelStemRegistryKey, identifier);
return createMethod.invoke(null, levelStemRegistryKey, identifier);
}
private Object resolveCreateLevelStem(WorldCreator creator) throws ReflectiveOperationException {
Object irisLevelStem = resolveIrisLevelStem(creator);
if (irisLevelStem != null) {
return irisLevelStem;
}
return getOverworldLevelStem();
}
private Object resolveIrisLevelStem(WorldCreator creator) throws ReflectiveOperationException {
ChunkGenerator generator = creator.generator();
if (!(generator instanceof PlatformChunkGenerator)) {
return null;
}
Object registryAccess = invoke(minecraftServer, "registryAccess");
Object binding = INMS.get();
Method levelStemMethod;
try {
levelStemMethod = resolveMethod(binding.getClass(), "levelStem", registryAccess, generator);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Iris NMS binding does not expose levelStem(RegistryAccess, ChunkGenerator) for runtime world \"" + creator.name() + "\".", e);
}
Object levelStem;
try {
levelStem = levelStemMethod.invoke(binding, registryAccess, generator);
} catch (InvocationTargetException e) {
Throwable cause = unwrap(e);
throw new IllegalStateException("Iris failed to resolve runtime level stem for world \"" + creator.name() + "\".", cause);
}
if (levelStem == null) {
throw new IllegalStateException("Iris resolved a null runtime level stem for world \"" + creator.name() + "\".");
}
return levelStem;
}
private Object getOverworldLevelStem() throws ReflectiveOperationException {
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
.getField("LEVEL_STEM")
.get(null);
Object registryAccess = invoke(minecraftServer, "registryAccess");
Object levelStemRegistry = invoke(registryAccess, "lookupOrThrow", levelStemRegistryKey);
Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
.getField("OVERWORLD")
.get(null);
Object levelStem;
try {
levelStem = invoke(levelStemRegistry, "getValue", overworldStemKey);
} catch (NoSuchMethodException ignored) {
Object rawLevelStem = invoke(levelStemRegistry, "get", overworldStemKey);
levelStem = extractRegistryValue(rawLevelStem);
}
if (levelStem == null) {
throw new IllegalStateException("Unable to resolve OVERWORLD LevelStem from registry.");
}
return levelStem;
}
private static Object extractRegistryValue(Object rawValue) throws ReflectiveOperationException {
if (rawValue == null) {
return null;
}
if (rawValue instanceof java.util.Optional<?> optionalValue) {
Object nestedValue = optionalValue.orElse(null);
if (nestedValue == null) {
return null;
}
return extractRegistryValue(nestedValue);
}
try {
Method valueMethod = rawValue.getClass().getMethod("value");
return valueMethod.invoke(rawValue);
} catch (NoSuchMethodException ignored) {
return rawValue;
}
}
private static Object getPublicField(Object target, String fieldName) throws ReflectiveOperationException {
Field field = target.getClass().getField(fieldName);
return field.get(target);
}
private static void closeLevelStorageAccess(Object levelStorageAccess) {
if (levelStorageAccess == null) {
return;
}
try {
Method close = levelStorageAccess.getClass().getMethod("close");
close.invoke(levelStorageAccess);
} catch (Throwable ignored) {
}
}
private static void stageRuntimeGenerator(WorldCreator creator) throws ReflectiveOperationException {
ChunkGenerator generator = creator.generator();
if (generator == null) {
throw new IllegalStateException("Runtime world creation requires a non-null chunk generator.");
}
Iris.stageRuntimeWorldGenerator(creator.name(), generator, creator.biomeProvider());
Object bukkitServer = Bukkit.getServer();
if (bukkitServer == null) {
throw new IllegalStateException("Bukkit server is unavailable.");
}
Field configurationField = bukkitServer.getClass().getDeclaredField("configuration");
configurationField.setAccessible(true);
Object rawConfiguration = configurationField.get(bukkitServer);
if (!(rawConfiguration instanceof YamlConfiguration configuration)) {
throw new IllegalStateException("CraftServer configuration field is unavailable.");
}
ConfigurationSection worldsSection = configuration.getConfigurationSection("worlds");
if (worldsSection == null) {
worldsSection = configuration.createSection("worlds");
}
ConfigurationSection worldSection = worldsSection.getConfigurationSection(creator.name());
if (worldSection == null) {
worldSection = worldsSection.createSection(creator.name());
}
worldSection.set("generator", "Iris:runtime");
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static void removeWorldFromCraftServerMap(String worldName) throws ReflectiveOperationException {
Object bukkitServer = Bukkit.getServer();
if (bukkitServer == null) {
return;
}
Field worldsField = bukkitServer.getClass().getDeclaredField("worlds");
worldsField.setAccessible(true);
Object worldsRaw = worldsField.get(bukkitServer);
if (worldsRaw instanceof Map worldsMap) {
worldsMap.remove(worldName);
worldsMap.remove(worldName.toLowerCase(Locale.ROOT));
}
}
private static void closeServerLevel(World world, Object serverLevel) throws Throwable {
Method closeLevelMethod = resolveMethod(serverLevel.getClass(), "close");
if (!J.isFolia()) {
closeLevelMethod.invoke(serverLevel);
return;
}
Location spawn = world.getSpawnLocation();
int chunkX = spawn == null ? 0 : spawn.getBlockX() >> 4;
int chunkZ = spawn == null ? 0 : spawn.getBlockZ() >> 4;
CompletableFuture<Void> closeFuture = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
closeLevelMethod.invoke(serverLevel);
closeFuture.complete(null);
} catch (Throwable e) {
closeFuture.completeExceptionally(unwrap(e));
}
});
if (!scheduled) {
throw new IllegalStateException("Failed to schedule region close task for world \"" + world.getName() + "\".");
}
closeFuture.get(90, TimeUnit.SECONDS);
}
private void detachServerLevel(Object serverLevel, String worldName) throws Throwable {
Runnable detachTask = () -> {
try {
Method removeLevelMethod = resolveMethod(minecraftServer.getClass(), "removeLevel", serverLevel);
removeLevelMethod.invoke(minecraftServer, serverLevel);
removeWorldFromCraftServerMap(worldName);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
if (!J.isFolia()) {
detachTask.run();
return;
}
if (isGlobalTickThread()) {
detachTask.run();
return;
}
CompletableFuture<Void> detachFuture = J.sfut(() -> detachTask.run());
if (detachFuture == null) {
throw new IllegalStateException("Failed to schedule global detach task for world \"" + worldName + "\".");
}
detachFuture.get(15, TimeUnit.SECONDS);
}
private static boolean isGlobalTickThread() {
Server server = Bukkit.getServer();
if (server == null) {
return false;
}
try {
Method isGlobalTickThreadMethod = server.getClass().getMethod("isGlobalTickThread");
return Boolean.TRUE.equals(isGlobalTickThreadMethod.invoke(server));
} catch (Throwable ignored) {
return false;
}
}
private static Throwable unwrap(Throwable throwable) {
if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) {
return unwrap(invocationTargetException.getCause());
}
if (throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null) {
return unwrap(completionException.getCause());
}
if (throwable instanceof java.util.concurrent.ExecutionException executionException && executionException.getCause() != null) {
return unwrap(executionException.getCause());
}
return throwable;
}
private static Object[] resolveProviderFromServices() {
Object provider = null;
Class<?> levelStem = null;
Class<?> generatorType = null;
try {
Collection<Class<?>> knownServices = Bukkit.getServicesManager().getKnownServices();
for (Class<?> serviceClass : knownServices) {
if (!"net.thenextlvl.worlds.api.WorldsProvider".equals(serviceClass.getName())) {
continue;
}
RegisteredServiceProvider<?> registration = Bukkit.getServicesManager().getRegistration((Class) serviceClass);
if (registration == null) {
continue;
}
provider = registration.getProvider();
ClassLoader loader = serviceClass.getClassLoader();
if (loader == null && provider != null) {
loader = provider.getClass().getClassLoader();
}
if (loader != null) {
levelStem = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem", false, loader);
generatorType = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType", false, loader);
}
break;
}
} catch (Throwable ignored) {
}
return new Object[]{provider, levelStem, generatorType};
}
private Object resolveLevelStem(World.Environment environment) {
String key;
if (environment == World.Environment.NETHER) {
key = "NETHER";
} else if (environment == World.Environment.THE_END) {
key = "END";
} else {
key = "OVERWORLD";
}
return enumValue(levelStemClass, key);
}
private Object resolveGeneratorType(WorldType worldType) {
String typeName = worldType == null ? "NORMAL" : worldType.getName();
String key;
if ("FLAT".equalsIgnoreCase(typeName)) {
key = "FLAT";
} else if ("AMPLIFIED".equalsIgnoreCase(typeName)) {
key = "AMPLIFIED";
} else if ("LARGE_BIOMES".equalsIgnoreCase(typeName) || "LARGEBIOMES".equalsIgnoreCase(typeName)) {
key = "LARGE_BIOMES";
} else {
key = "NORMAL";
}
return enumValue(generatorTypeClass, key);
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static Object enumValue(Class<?> enumClass, String key) {
Class<? extends Enum> typed = enumClass.asSubclass(Enum.class);
return Enum.valueOf(typed, key);
}
private static Method resolveMethod(Class<?> owner, String methodName, Object... args) throws NoSuchMethodException {
Method selected = findMatchingMethod(owner.getMethods(), methodName, args);
if (selected != null) {
return selected;
}
Class<?> current = owner;
while (current != null) {
selected = findMatchingMethod(current.getDeclaredMethods(), methodName, args);
if (selected != null) {
selected.setAccessible(true);
return selected;
}
current = current.getSuperclass();
}
throw new NoSuchMethodException(owner.getName() + "#" + methodName);
}
private static Constructor<?> resolveConstructor(Class<?> owner, Object... args) throws NoSuchMethodException {
Constructor<?> selected = findMatchingConstructor(owner.getConstructors(), args);
if (selected != null) {
return selected;
}
selected = findMatchingConstructor(owner.getDeclaredConstructors(), args);
if (selected != null) {
selected.setAccessible(true);
return selected;
}
throw new NoSuchMethodException(owner.getName() + "#<init>");
}
private static Method findMatchingMethod(Method[] methods, String methodName, Object... args) {
Method selected = null;
for (Method method : methods) {
if (!method.getName().equals(methodName)) {
continue;
}
Class<?>[] params = method.getParameterTypes();
if (params.length != args.length) {
continue;
}
if (matches(params, args)) {
selected = method;
break;
}
}
return selected;
}
private static String formatMethod(Method method) {
if (method == null) {
return "<null>";
}
StringBuilder builder = new StringBuilder();
builder.append(method.getDeclaringClass().getName())
.append("#")
.append(method.getName())
.append("(");
Class<?>[] parameterTypes = method.getParameterTypes();
for (int i = 0; i < parameterTypes.length; i++) {
if (i > 0) {
builder.append(", ");
}
builder.append(parameterTypes[i].getName());
}
builder.append(")");
return builder.toString();
}
private static String formatArgs(Object... args) {
if (args == null) {
return "<null>";
}
StringBuilder builder = new StringBuilder();
builder.append("[");
for (int i = 0; i < args.length; i++) {
if (i > 0) {
builder.append(", ");
}
Object argument = args[i];
builder.append(argument == null ? "null" : argument.getClass().getName());
}
builder.append("]");
return builder.toString();
}
private static Constructor<?> findMatchingConstructor(Constructor<?>[] constructors, Object... args) {
Constructor<?> selected = null;
for (Constructor<?> constructor : constructors) {
Class<?>[] params = constructor.getParameterTypes();
if (params.length != args.length) {
continue;
}
if (matches(params, args)) {
selected = constructor;
break;
}
}
return selected;
}
private static Object invoke(Object target, String methodName, Object... args) throws ReflectiveOperationException {
Method selected = resolveMethod(target.getClass(), methodName, args);
return selected.invoke(target, args);
}
private static boolean matches(Class<?>[] params, Object[] args) {
for (int i = 0; i < params.length; i++) {
Object arg = args[i];
Class<?> parameterType = params[i];
if (arg == null) {
if (parameterType.isPrimitive()) {
return false;
}
continue;
}
Class<?> boxedParameterType = box(parameterType);
if (!boxedParameterType.isAssignableFrom(arg.getClass())) {
return false;
}
}
return true;
}
private static Class<?> box(Class<?> type) {
if (!type.isPrimitive()) {
return type;
}
if (type == boolean.class) {
return Boolean.class;
}
if (type == byte.class) {
return Byte.class;
}
if (type == short.class) {
return Short.class;
}
if (type == int.class) {
return Integer.class;
}
if (type == long.class) {
return Long.class;
}
if (type == float.class) {
return Float.class;
}
if (type == double.class) {
return Double.class;
}
if (type == char.class) {
return Character.class;
}
return Void.class;
}
}

View File

@@ -0,0 +1,182 @@
package art.arcane.iris.core.link.data;
import art.arcane.iris.core.link.ExternalDataProvider;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.properties.BooleanProperty;
import net.momirealms.craftengine.core.block.properties.IntegerProperty;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.function.Function;
import java.util.stream.Stream;
public class CraftEngineDataProvider extends ExternalDataProvider {
private static final BlockProperty[] FURNITURE_PROPERTIES = new BlockProperty[]{
BlockProperty.ofBoolean("randomYaw", false),
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
BlockProperty.ofBoolean("randomPitch", false),
BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true),
};
public CraftEngineDataProvider() {
super("CraftEngine");
}
@Override
public void init() {
}
@Override
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
Key key = Key.of(blockId.namespace(), blockId.key());
net.momirealms.craftengine.core.block.CustomBlock block = CraftEngineBlocks.byId(key);
if (block != null) {
return block.properties().stream().map(CraftEngineDataProvider::convert).toList();
}
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
if (furniture != null) {
BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5);
properties[4] = new BlockProperty(
"variant",
String.class,
furniture.anyVariantName(),
furniture.variants().keySet(),
Function.identity()
);
return List.of(properties);
}
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
}
@Override
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
net.momirealms.craftengine.core.item.CustomItem<ItemStack> item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
if (item == null) {
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
}
return item.buildItemStack();
}
@Override
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
Key key = Key.of(blockId.namespace(), blockId.key());
if (CraftEngineBlocks.byId(key) == null && CraftEngineFurniture.byId(key) == null) {
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
}
return IrisCustomData.of(B.getAir(), ExternalDataSVC.buildState(blockId, state));
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
art.arcane.iris.core.nms.container.Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
Identifier baseBlockId = statePair.getA();
KMap<String, String> state = statePair.getB();
Key key = Key.of(baseBlockId.namespace(), baseBlockId.key());
net.momirealms.craftengine.core.block.CustomBlock customBlock = CraftEngineBlocks.byId(key);
if (customBlock != null) {
ImmutableBlockState blockState = customBlock.defaultState();
for (Map.Entry<String, String> entry : state.entrySet()) {
Property<?> property = customBlock.getProperty(entry.getKey());
if (property == null) {
continue;
}
Comparable<?> tag = property.optional(entry.getValue()).orElse(null);
if (tag == null) {
continue;
}
blockState = ImmutableBlockState.with(blockState, property, tag);
}
CraftEngineBlocks.place(block.getLocation(), blockState, false);
return;
}
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
if (furniture == null) {
return;
}
Location location = parseYawAndPitch(engine, block, state);
String variant = state.getOrDefault("variant", furniture.anyVariantName());
CraftEngineFurniture.place(location, furniture, variant, false);
}
private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map<String, String> state) {
Location location = block.getLocation();
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
RNG rng = new RNG(seed);
if ("true".equals(state.get("randomYaw"))) {
location.setYaw(rng.f(0, 360));
} else if (state.containsKey("yaw")) {
location.setYaw(Float.parseFloat(state.get("yaw")));
}
if ("true".equals(state.get("randomPitch"))) {
location.setPitch(rng.f(0, 360));
} else if (state.containsKey("pitch")) {
location.setPitch(Float.parseFloat(state.get("pitch")));
}
return location;
}
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
Stream<Key> keys = switch (dataType) {
case ENTITY -> Stream.<Key>empty();
case ITEM -> CraftEngineItems.loadedItems().keySet().stream();
case BLOCK -> Stream.concat(CraftEngineBlocks.loadedBlocks().keySet().stream(),
CraftEngineFurniture.loadedFurniture().keySet().stream());
};
return keys.map(key -> new Identifier(key.namespace(), key.value())).toList();
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
Key key = Key.of(id.namespace(), id.key());
return switch (dataType) {
case ENTITY -> false;
case ITEM -> CraftEngineItems.byId(key) != null;
case BLOCK -> CraftEngineBlocks.byId(key) != null || CraftEngineFurniture.byId(key) != null;
};
}
private static <T extends Comparable<T>> BlockProperty convert(Property<T> raw) {
return switch (raw) {
case BooleanProperty property -> BlockProperty.ofBoolean(property.name(), property.defaultValue());
case IntegerProperty property -> BlockProperty.ofLong(property.name(), property.defaultValue(), property.min, property.max, false, false);
default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName);
};
}
}

View File

@@ -23,7 +23,9 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
import org.bukkit.Bukkit;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class INMS {
private static final Version CURRENT = Boolean.getBoolean("iris.no-version-limit") ?
@@ -69,51 +71,42 @@ public class INMS {
private static INMSBinding bind() {
String code = getNMSTag();
Iris.info("Locating NMS Binding for " + code);
try {
Class<?> clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding");
try {
Object b = clazz.getConstructor().newInstance();
if (b instanceof INMSBinding binding) {
Iris.info("Craftbukkit " + code + " <-> " + b.getClass().getSimpleName() + " Successfully Bound");
return binding;
}
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
}
} catch (ClassNotFoundException | NoClassDefFoundError classNotFoundException) {
Iris.warn("Failed to load NMS binding class for " + code + ": " + classNotFoundException.getMessage());
boolean disableNms = IrisSettings.get().getGeneral().isDisableNMS();
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes(code, disableNms, getFallbackBindingCodes());
if ("BUKKIT".equals(code) && !disableNms) {
Iris.info("NMS tag resolution fell back to Bukkit; probing supported revision bindings.");
}
if (IrisSettings.get().getGeneral().isDisableNMS()) {
for (int i = 0; i < probeCodes.size(); i++) {
INMSBinding resolvedBinding = tryBind(probeCodes.get(i), i == 0);
if (resolvedBinding != null) {
return resolvedBinding;
}
}
if (disableNms) {
Iris.info("Craftbukkit " + code + " <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
Iris.warn("Note: NMS support is disabled. Iris is running in limited Bukkit fallback mode.");
return new NMSBinding1X();
}
String serverVersion = Bukkit.getServer().getBukkitVersion().split("-")[0];
MinecraftVersion detectedVersion = getMinecraftVersion();
String serverVersion = detectedVersion == null ? Bukkit.getServer().getVersion() : detectedVersion.value();
throw new IllegalStateException("Iris requires Minecraft 1.21.11 or newer. Detected server version: " + serverVersion);
}
private static String getTag(List<Version> versions, String def) {
String[] version = Bukkit.getServer().getBukkitVersion().split("-")[0].split("\\.", 3);
int major = 0;
int minor = 0;
if (version.length > 2) {
major = Integer.parseInt(version[1]);
minor = Integer.parseInt(version[2]);
} else if (version.length == 2) {
major = Integer.parseInt(version[1]);
MinecraftVersion detectedVersion = getMinecraftVersion();
if (detectedVersion == null) {
return def;
}
if (CURRENT.major < major || CURRENT.minor < minor) {
if (detectedVersion.isNewerThan(CURRENT.major, CURRENT.minor)) {
return versions.getFirst().tag;
}
for (Version p : versions) {
if (p.major > major || p.minor > minor) {
if (!detectedVersion.isAtLeast(p.major, p.minor)) {
continue;
}
return p.tag;
@@ -121,5 +114,50 @@ public class INMS {
return def;
}
private static MinecraftVersion getMinecraftVersion() {
try {
return MinecraftVersion.detect(Bukkit.getServer());
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Failed to determine server minecraft version!");
e.printStackTrace();
return null;
}
}
private static INMSBinding tryBind(String code, boolean announce) {
if (announce) {
Iris.info("Locating NMS Binding for " + code);
} else {
Iris.info("Probing NMS Binding for " + code);
}
try {
Class<?> clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding");
Object candidate = clazz.getConstructor().newInstance();
if (candidate instanceof INMSBinding binding) {
Iris.info("Craftbukkit " + code + " <-> " + candidate.getClass().getSimpleName() + " Successfully Bound");
return binding;
}
} catch (ClassNotFoundException | NoClassDefFoundError classNotFoundException) {
Iris.warn("Failed to load NMS binding class for " + code + ": " + classNotFoundException.getMessage());
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
}
return null;
}
private static Set<String> getFallbackBindingCodes() {
Set<String> codes = new LinkedHashSet<>();
for (Version version : REVISION) {
if (version.tag != null && !version.tag.isBlank()) {
codes.add(version.tag);
}
}
return codes;
}
private record Version(int major, int minor, String tag) {}
}

View File

@@ -18,8 +18,10 @@
package art.arcane.iris.core.nms;
import art.arcane.iris.core.link.FoliaWorldsLink;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
import art.arcane.iris.core.lifecycle.WorldLifecycleRequest;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.nms.container.BiomeColor;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.nms.container.StructurePlacement;
@@ -40,6 +42,7 @@ import org.bukkit.block.Biome;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.inventory.ItemStack;
import java.awt.Color;
@@ -96,34 +99,33 @@ public interface INMSBinding {
MCABiomeContainer newBiomeContainer(int min, int max);
default World createWorld(WorldCreator c) {
if (c.generator() instanceof PlatformChunkGenerator gen
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey()))
throw new IllegalStateException("Missing dimension types to create world");
return c.createWorld();
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(c, false, false, WorldLifecycleCaller.CREATE);
return createWorld(c, request);
}
default CompletableFuture<World> createWorldAsync(WorldCreator c) {
try {
if (c.generator() instanceof PlatformChunkGenerator gen
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) {
return CompletableFuture.failedFuture(new IllegalStateException("Missing dimension types to create world"));
}
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(c, false, false, WorldLifecycleCaller.CREATE);
return createWorldAsync(c, request);
}
if (J.isFolia()) {
FoliaWorldsLink link = FoliaWorldsLink.get();
if (link.isActive()) {
CompletableFuture<World> future = link.createWorld(c);
if (future != null) {
return future;
}
}
}
return CompletableFuture.completedFuture(createWorld(c));
default World createWorld(WorldCreator c, WorldLifecycleRequest request) {
validateDimensionTypes(c);
return WorldLifecycleService.get().createBlocking(request);
}
default CompletableFuture<World> createWorldAsync(WorldCreator c, WorldLifecycleRequest request) {
try {
validateDimensionTypes(c);
return WorldLifecycleService.get().create(request);
} catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
}
default Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) {
throw new UnsupportedOperationException("Active NMS binding does not support runtime LevelStem creation.");
}
int countCustomBiomes();
default boolean supportsDataPacks() {
@@ -169,4 +171,11 @@ public interface INMSBinding {
void placeStructures(Chunk chunk);
KMap<Identifier, StructurePlacement> collectStructures();
private void validateDimensionTypes(WorldCreator c) {
if (c.generator() instanceof PlatformChunkGenerator gen
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) {
throw new IllegalStateException("Missing dimension types to create world");
}
}
}

View File

@@ -0,0 +1,114 @@
package art.arcane.iris.core.nms;
import org.bukkit.Server;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class MinecraftVersion {
private static final Pattern DECORATED_VERSION_PATTERN = Pattern.compile("\\(MC: ([0-9]+(?:\\.[0-9]+){0,2})\\)");
private final String value;
private final int major;
private final int minor;
private MinecraftVersion(String value, int major, int minor) {
this.value = value;
this.major = major;
this.minor = minor;
}
public static MinecraftVersion detect(Server server) {
if (server == null) {
return null;
}
MinecraftVersion runtimeVersion = fromRuntimeMinecraftVersion(server);
if (runtimeVersion != null) {
return runtimeVersion;
}
MinecraftVersion decoratedVersion = fromDecoratedVersion(server.getVersion());
if (decoratedVersion != null) {
return decoratedVersion;
}
return fromBukkitVersion(server.getBukkitVersion());
}
static MinecraftVersion fromRuntimeMinecraftVersion(Server server) {
try {
Method method = server.getClass().getMethod("getMinecraftVersion");
Object value = method.invoke(server);
if (value instanceof String version) {
return fromVersionToken(version);
}
} catch (ReflectiveOperationException ignored) {
return null;
}
return null;
}
static MinecraftVersion fromDecoratedVersion(String input) {
if (input == null || input.isBlank()) {
return null;
}
Matcher matcher = DECORATED_VERSION_PATTERN.matcher(input);
if (!matcher.find()) {
return null;
}
return fromVersionToken(matcher.group(1));
}
static MinecraftVersion fromBukkitVersion(String input) {
if (input == null || input.isBlank()) {
return null;
}
String versionToken = input.split("-", 2)[0].trim();
return fromVersionToken(versionToken);
}
private static MinecraftVersion fromVersionToken(String input) {
if (input == null || input.isBlank()) {
return null;
}
String[] parts = input.split("\\.");
if (parts.length < 2 || !"1".equals(parts[0])) {
return null;
}
try {
int major = Integer.parseInt(parts[1]);
int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
return new MinecraftVersion(input, major, minor);
} catch (NumberFormatException ignored) {
return null;
}
}
public String value() {
return value;
}
public int major() {
return major;
}
public int minor() {
return minor;
}
public boolean isAtLeast(int major, int minor) {
return this.major > major || (this.major == major && this.minor >= minor);
}
public boolean isNewerThan(int major, int minor) {
return this.major > major || (this.major == major && this.minor > minor);
}
}

View File

@@ -0,0 +1,29 @@
package art.arcane.iris.core.nms;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
final class NmsBindingProbeSupport {
private NmsBindingProbeSupport() {
}
static List<String> getBindingProbeCodes(String code, boolean disableNms, Collection<String> fallbackCodes) {
List<String> probeCodes = new ArrayList<>();
if (code == null || code.isBlank()) {
return probeCodes;
}
if (!"BUKKIT".equals(code)) {
probeCodes.add(code);
return probeCodes;
}
if (disableNms || fallbackCodes == null) {
return probeCodes;
}
probeCodes.addAll(fallbackCodes);
return probeCodes;
}
}

View File

@@ -17,7 +17,7 @@ public class BlockProperty {
private final Function<Object, String> nameFunction;
private final Function<Object, Object> jsonFunction;
public <T extends Comparable<T>> BlockProperty(
public <T extends Comparable<T>> BlockProperty(
String name,
Class<T> type,
T defaultValue,
@@ -42,7 +42,7 @@ public class BlockProperty {
);
}
public static BlockProperty ofFloat(String name, float defaultValue, float min, float max, boolean exclusiveMin, boolean exclusiveMax) {
public static BlockProperty ofDouble(String name, float defaultValue, float min, float max, boolean exclusiveMin, boolean exclusiveMax) {
return new BoundedDouble(
name,
defaultValue,
@@ -54,6 +54,18 @@ public class BlockProperty {
);
}
public static BlockProperty ofLong(String name, long defaultValue, long min, long max, boolean exclusiveMin, boolean exclusiveMax) {
return new BoundedLong(
name,
defaultValue,
min,
max,
exclusiveMin,
exclusiveMax,
value -> Long.toString(value)
);
}
public static BlockProperty ofBoolean(String name, boolean defaultValue) {
return new BlockProperty(
name,
@@ -122,6 +134,38 @@ public class BlockProperty {
return Objects.hash(name, values, type);
}
private static class BoundedLong extends BlockProperty {
private final long min;
private final long max;
private final boolean exclusiveMin;
private final boolean exclusiveMax;
public BoundedLong(
String name,
long defaultValue,
long min,
long max,
boolean exclusiveMin,
boolean exclusiveMax,
Function<Long, String> nameFunction
) {
super(name, Long.class, defaultValue, List.of(), nameFunction);
this.min = min;
this.max = max;
this.exclusiveMin = exclusiveMin;
this.exclusiveMax = exclusiveMax;
}
@Override
public JSONObject buildJson() {
return super.buildJson()
.put("minimum", min)
.put("maximum", max)
.put("exclusiveMinimum", exclusiveMin)
.put("exclusiveMaximum", exclusiveMax);
}
}
private static class BoundedDouble extends BlockProperty {
private final double min, max;
private final boolean exclusiveMin, exclusiveMax;

View File

@@ -12,6 +12,7 @@ public class DataFixerV1217 extends DataFixerV1213 {
Dimension.OVERWORLD, """
{
"ambient_light": 0.0,
"has_ender_dragon_fight": false,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
@@ -42,6 +43,7 @@ public class DataFixerV1217 extends DataFixerV1213 {
Dimension.NETHER, """
{
"ambient_light": 0.1,
"has_ender_dragon_fight": false,
"attributes": {
"minecraft:gameplay/sky_light_level": 4.0,
"minecraft:gameplay/snow_golem_melts": true,
@@ -57,6 +59,7 @@ public class DataFixerV1217 extends DataFixerV1213 {
Dimension.END, """
{
"ambient_light": 0.25,
"has_ender_dragon_fight": true,
"attributes": {
"minecraft:audio/ambient_sounds": {
"mood": {
@@ -96,9 +99,9 @@ public class DataFixerV1217 extends DataFixerV1213 {
JSONObject particle = (JSONObject) effects.remove("particle");
if (particle != null) {
particle.put("particle", particle.remove("options"));
attributes.put("minecraft:visual/ambient_particles", new JSONArray()
.put(particle.getJSONObject("options")
.put("probability", particle.get("probability"))));
.put(particle));
}
json.put("attributes", attributes);

View File

@@ -21,10 +21,11 @@ package art.arcane.iris.core.project;
import com.google.gson.Gson;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.link.FoliaWorldsLink;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
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.core.runtime.StudioOpenCoordinator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.*;
import art.arcane.iris.engine.object.annotations.Snippet;
@@ -63,6 +64,8 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@@ -228,209 +231,117 @@ public class IrisProject {
return foundWork;
}
public void open(VolmitSender sender, long seed, Consumer<World> onDone) throws IrisException {
public CompletableFuture<StudioOpenCoordinator.StudioOpenResult> open(VolmitSender sender, long seed, Consumer<World> onDone) throws IrisException {
if (isOpen()) {
close();
return close().thenCompose(ignored -> openInternal(sender, seed, onDone));
}
return openInternal(sender, seed, onDone);
}
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> openInternal(VolmitSender sender, long seed, Consumer<World> onDone) {
AtomicReference<String> stage = new AtomicReference<>("Queued");
AtomicReference<Double> progress = new AtomicReference<>(0.01D);
AtomicBoolean complete = new AtomicBoolean(false);
AtomicBoolean failed = new AtomicBoolean(false);
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> future = StudioOpenCoordinator.get().open(
StudioOpenCoordinator.StudioOpenRequest.studioProject(
this,
sender,
seed,
update -> {
if (update.stage() != null && !update.stage().isBlank()) {
stage.set(update.stage());
}
progress.set(Math.max(0D, Math.min(0.99D, update.progress())));
},
onDone
)
);
startStudioOpenReporter(sender, stage, progress, complete, failed);
J.a(() -> {
future.whenComplete((result, throwable) -> {
World maintenanceWorld = null;
boolean maintenanceActive = false;
try {
stage.set("Loading dimension");
progress.set(0.05D);
IrisDimension d = IrisData.loadAnyDimension(getName(), null);
if (d == null) {
if (throwable != null) {
failed.set(true);
sender.sendMessage(C.RED + "Can't find dimension: " + getName());
Throwable error = throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null
? completionException.getCause()
: throwable;
Iris.reportError("Studio open failed for project \"" + getName() + "\".", error);
sender.sendMessage(C.RED + "Studio open failed: " + error.getMessage());
return;
} else if (sender.isPlayer()) {
}
if (sender.isPlayer() && sender.player() != null) {
J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.SPECTATOR));
}
stage.set("Creating world");
progress.set(0.12D);
activeProvider = (PlatformChunkGenerator) IrisToolbelt.createWorld()
.seed(seed)
.sender(sender)
.studio(true)
.name("iris-" + UUID.randomUUID())
.dimension(d.getLoadKey())
.studioProgressConsumer((value, currentStage) -> {
if (currentStage != null && !currentStage.isBlank()) {
stage.set(currentStage);
}
progress.set(Math.max(0D, Math.min(0.99D, value)));
})
.create().getGenerator();
if (activeProvider != null) {
maintenanceWorld = activeProvider.getTarget().getWorld().realWorld();
if (maintenanceWorld != null) {
IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open");
maintenanceActive = true;
}
onDone.accept(maintenanceWorld);
activeProvider = IrisToolbelt.access(result.world());
maintenanceWorld = result.world();
if (maintenanceWorld != null) {
IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open");
maintenanceActive = true;
}
} catch (IrisException e) {
failed.set(true);
Iris.reportError(e);
sender.sendMessage(C.RED + "Failed to open studio world: " + e.getMessage());
} catch (Throwable e) {
failed.set(true);
Iris.reportError(e);
sender.sendMessage(C.RED + "Studio open failed: " + e.getMessage());
} finally {
if (activeProvider != null) {
stage.set("Opening workspace");
progress.set(Math.max(progress.get(), 0.95D));
openVSCode(sender);
}
if (maintenanceActive && maintenanceWorld != null) {
World worldToRelease = maintenanceWorld;
J.a(() -> {
J.sleep(15000);
IrisToolbelt.endWorldMaintenance(worldToRelease, "studio-open");
});
maintenanceActive = false;
}
if (maintenanceActive && maintenanceWorld != null) {
IrisToolbelt.endWorldMaintenance(maintenanceWorld, "studio-open");
J.a(() -> IrisToolbelt.endWorldMaintenance(worldToRelease, "studio-open"), 300);
}
complete.set(true);
}
});
return future;
}
private void startStudioOpenReporter(VolmitSender sender, AtomicReference<String> stage, AtomicReference<Double> progress, AtomicBoolean complete, AtomicBoolean failed) {
J.a(() -> {
String[] spinner = {"|", "/", "-", "\\"};
int spinIndex = 0;
long nextConsoleUpdate = 0L;
String[] spinner = {"|", "/", "-", "\\"};
AtomicInteger spinIndex = new AtomicInteger(0);
AtomicLong nextConsoleUpdate = new AtomicLong(0L);
AtomicInteger taskId = new AtomicInteger(-1);
while (!complete.get()) {
double currentProgress = Math.max(0D, Math.min(0.97D, progress.get()));
String currentStage = stage.get();
String currentSpinner = spinner[spinIndex % spinner.length];
if (sender.isPlayer()) {
sender.sendProgress(currentProgress, "Studio " + currentSpinner + " " + currentStage);
} else {
long now = System.currentTimeMillis();
if (now >= nextConsoleUpdate) {
sender.sendMessage(C.WHITE + "Studio " + Form.pc(currentProgress, 0) + C.GRAY + " - " + currentStage);
nextConsoleUpdate = now + 1500L;
int scheduledTaskId = J.ar(() -> {
if (complete.get()) {
J.car(taskId.get());
if (failed.get()) {
if (sender.isPlayer()) {
sender.sendProgress(1D, "Studio open failed");
} else {
sender.sendMessage(C.RED + "Studio open failed.");
}
}
spinIndex++;
J.sleep(120);
}
if (failed.get()) {
if (sender.isPlayer()) {
sender.sendProgress(1D, "Studio open failed");
} else if (sender.isPlayer()) {
sender.sendProgress(1D, "Studio ready");
} else {
sender.sendMessage(C.RED + "Studio open failed.");
sender.sendMessage(C.GREEN + "Studio ready.");
}
return;
}
double currentProgress = Math.max(0D, Math.min(0.97D, progress.get()));
String currentStage = stage.get();
String currentSpinner = spinner[Math.floorMod(spinIndex.getAndIncrement(), spinner.length)];
if (sender.isPlayer()) {
sender.sendProgress(1D, "Studio ready");
} else {
sender.sendMessage(C.GREEN + "Studio ready.");
}
});
}
public void close() {
if (activeProvider == null) {
return;
}
Iris.debug("Closing Active Provider");
final PlatformChunkGenerator provider = activeProvider;
final World studioWorld = provider.getTarget().getWorld().realWorld();
final File folder = provider.getTarget().getWorld().worldFolder();
final String worldName = provider.getTarget().getWorld().name();
final Runnable closeTask = () -> {
IrisToolbelt.beginWorldMaintenance(studioWorld, "studio-close", true);
try {
IrisToolbelt.evacuate(studioWorld);
provider.close();
Iris.linkMultiverseCore.removeFromConfig(worldName);
boolean unloaded = FoliaWorldsLink.get().unloadWorld(studioWorld, false);
if (!unloaded) {
Iris.warn("Failed to unload studio world \"" + worldName + "\".");
}
} finally {
IrisToolbelt.endWorldMaintenance(studioWorld, "studio-close");
}
};
if (J.isPrimaryThread()) {
closeTask.run();
} else {
final CompletableFuture<Void> closeFuture = J.sfut(closeTask);
if (closeFuture != null) {
try {
closeFuture.join();
} catch (Throwable e) {
Iris.reportError(e);
e.printStackTrace();
}
} else {
closeTask.run();
}
}
J.attemptAsync(() -> deleteStudioFolderWithRetry(folder, worldName));
Iris.debug("Closed Active Provider " + worldName);
activeProvider = null;
}
private static void deleteStudioFolderWithRetry(File folder, String worldName) {
if (folder == null) {
return;
}
long unloadWaitDeadlineMs = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20);
while (Bukkit.getWorld(worldName) != null && System.currentTimeMillis() < unloadWaitDeadlineMs) {
J.sleep(100);
}
int attempts = 0;
while (folder.exists() && attempts < 40) {
IO.delete(folder);
if (!folder.exists()) {
sender.sendProgress(currentProgress, "Studio " + currentSpinner + " " + currentStage);
return;
}
attempts++;
J.sleep(250);
long now = System.currentTimeMillis();
long nextUpdate = nextConsoleUpdate.get();
if (now >= nextUpdate) {
sender.sendMessage(C.WHITE + "Studio " + Form.pc(currentProgress, 0) + C.GRAY + " - " + currentStage);
nextConsoleUpdate.set(now + 1500L);
}
}, 3);
taskId.set(scheduledTaskId);
}
public CompletableFuture<StudioOpenCoordinator.StudioCloseResult> close() {
if (activeProvider == null) {
return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null, null));
}
if (!folder.exists()) {
return;
}
try {
Iris.queueWorldDeletionOnStartup(java.util.Collections.singleton(worldName));
Iris.warn("Queued deferred deletion for studio world folder \"" + worldName + "\".");
} catch (IOException e) {
Iris.warn("Failed to queue deferred deletion for studio world folder \"" + worldName + "\".");
Iris.reportError(e);
}
return StudioOpenCoordinator.get().closeProject(this);
}
public File getCodeWorkspaceFile() {

View File

@@ -0,0 +1,77 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
import io.papermc.lib.PaperLib;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
final class BukkitPublicRuntimeControlBackend implements WorldRuntimeControlBackend {
private final CapabilitySnapshot capabilities;
BukkitPublicRuntimeControlBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
}
@Override
public String backendName() {
return "bukkit_public_runtime";
}
@Override
public String describeCapabilities() {
String chunkAsync = capabilities.chunkAtAsyncMethod() != null ? "world#getChunkAtAsync" : "paperlib";
return "time=bukkit_world#setTime, chunkAsync=" + chunkAsync + ", teleport=entity_scheduler";
}
@Override
public OptionalLong readDayTime(World world) {
if (world == null) {
return OptionalLong.empty();
}
return OptionalLong.of(world.getTime());
}
@Override
public boolean writeDayTime(World world, long dayTime) {
if (world == null) {
return false;
}
world.setTime(dayTime);
return true;
}
@Override
public void syncTime(World world) {
}
@Override
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
if (world == null) {
return CompletableFuture.failedFuture(new IllegalStateException("World is null."));
}
if (capabilities.chunkAtAsyncMethod() != null) {
try {
Object result = capabilities.chunkAtAsyncMethod().invoke(world, chunkX, chunkZ, generate);
if (result instanceof CompletableFuture<?>) {
@SuppressWarnings("unchecked")
CompletableFuture<Chunk> future = (CompletableFuture<Chunk>) result;
return future;
}
} catch (Throwable ignored) {
}
}
CompletableFuture<Chunk> future = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate);
if (future == null) {
return CompletableFuture.failedFuture(new IllegalStateException("PaperLib did not return a chunk future."));
}
return future;
}
}

View File

@@ -0,0 +1,97 @@
package art.arcane.iris.core.runtime;
import com.google.gson.GsonBuilder;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import lombok.Data;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@Data
public final class DatapackReadinessResult {
private final String requestedPackKey;
private final List<String> resolvedDatapackFolders;
private final String externalDatapackInstallResult;
private final boolean verificationPassed;
private final List<String> verifiedPaths;
private final List<String> missingPaths;
private final boolean restartRequired;
public String toJson() {
return new GsonBuilder().setPrettyPrinting().create().toJson(this);
}
public static DatapackReadinessResult installForStudioWorld(
String requestedPackKey,
String dimensionTypeKey,
File worldFolder,
boolean verifyDataPacks,
boolean includeExternalDataPacks,
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
) {
ArrayList<String> resolvedFolders = new ArrayList<>();
File datapacksFolder = ServerConfigurator.resolveDatapacksFolder(worldFolder);
resolvedFolders.add(datapacksFolder.getAbsolutePath());
if (extraWorldDatapackFoldersByPack != null) {
KList<File> extraFolders = extraWorldDatapackFoldersByPack.get(requestedPackKey);
if (extraFolders != null) {
for (File extraFolder : extraFolders) {
if (extraFolder == null) {
continue;
}
String path = extraFolder.getAbsolutePath();
if (!resolvedFolders.contains(path)) {
resolvedFolders.add(path);
}
}
}
}
String externalResult = "ok";
boolean restartRequired = false;
try {
restartRequired = ServerConfigurator.installDataPacks(verifyDataPacks, includeExternalDataPacks, extraWorldDatapackFoldersByPack);
} catch (Throwable e) {
externalResult = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
}
ArrayList<String> verifiedPaths = new ArrayList<>();
ArrayList<String> missingPaths = new ArrayList<>();
String verificationDimensionTypeKey = (dimensionTypeKey == null || dimensionTypeKey.isBlank())
? requestedPackKey
: dimensionTypeKey;
for (String folderPath : resolvedFolders) {
File folder = new File(folderPath);
collectVerificationPaths(folder, verificationDimensionTypeKey, verifiedPaths, missingPaths);
}
boolean verificationPassed = missingPaths.isEmpty() && "ok".equals(externalResult);
return new DatapackReadinessResult(
requestedPackKey,
List.copyOf(resolvedFolders),
externalResult,
verificationPassed,
List.copyOf(verifiedPaths),
List.copyOf(missingPaths),
restartRequired
);
}
static void collectVerificationPaths(File folder, String dimensionTypeKey, List<String> verifiedPaths, List<String> missingPaths) {
File packMeta = new File(folder, "iris/pack.mcmeta");
File dimensionType = new File(folder, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json");
if (packMeta.exists()) {
verifiedPaths.add(packMeta.getAbsolutePath());
} else {
missingPaths.add(packMeta.getAbsolutePath());
}
if (dimensionType.exists()) {
verifiedPaths.add(dimensionType.getAbsolutePath());
} else {
missingPaths.add(dimensionType.getAbsolutePath());
}
}
}

View File

@@ -0,0 +1,319 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.lang.reflect.Method;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
final class PaperLikeRuntimeControlBackend implements WorldRuntimeControlBackend {
private final CapabilitySnapshot capabilities;
private final AtomicReference<TimeAccessStrategy> timeAccessStrategy;
PaperLikeRuntimeControlBackend(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
this.timeAccessStrategy = new AtomicReference<>();
}
@Override
public String backendName() {
return "paper_like_runtime";
}
@Override
public String describeCapabilities() {
TimeAccessStrategy strategy = timeAccessStrategy.get();
String timeAccess = strategy == null ? "deferred" : strategy.description();
String chunkAsync = capabilities.chunkAtAsyncMethod() != null ? "world#getChunkAtAsync" : "paperlib";
return "time=" + timeAccess + ", chunkAsync=" + chunkAsync + ", teleport=entity_scheduler";
}
@Override
public OptionalLong readDayTime(World world) {
if (world == null) {
return OptionalLong.empty();
}
TimeAccessStrategy strategy = resolveTimeAccessStrategy(world);
if (strategy == null) {
return OptionalLong.empty();
}
try {
Object handle = strategy.handleMethod().invoke(world);
if (handle == null) {
return OptionalLong.empty();
}
Object value = strategy.readMethod().invoke(strategy.readOwner(handle), strategy.readArguments(handle));
if (value instanceof Long longValue) {
return OptionalLong.of(longValue.longValue());
}
if (value instanceof Number number) {
return OptionalLong.of(number.longValue());
}
} catch (Throwable ignored) {
}
return OptionalLong.empty();
}
@Override
public boolean writeDayTime(World world, long dayTime) throws ReflectiveOperationException {
if (world == null) {
return false;
}
TimeAccessStrategy strategy = resolveTimeAccessStrategy(world);
if (strategy == null || !strategy.writable()) {
return false;
}
Object handle = strategy.handleMethod().invoke(world);
if (handle == null) {
return false;
}
Object writeOwner = strategy.writeOwner(handle);
if (writeOwner == null) {
return false;
}
strategy.writeMethod().invoke(writeOwner, dayTime);
return true;
}
@Override
public void syncTime(World world) {
TimeAccessStrategy strategy = timeAccessStrategy.get();
if (strategy == null || strategy.syncMethod() == null) {
return;
}
try {
Object craftServer = Bukkit.getServer();
if (craftServer == null) {
return;
}
Object serverHandle = strategy.serverHandleMethod() == null ? null : strategy.serverHandleMethod().invoke(craftServer);
if (serverHandle == null) {
return;
}
strategy.syncMethod().invoke(serverHandle);
} catch (Throwable ignored) {
}
}
@Override
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
if (world == null) {
return CompletableFuture.failedFuture(new IllegalStateException("World is null."));
}
if (capabilities.chunkAtAsyncMethod() != null) {
try {
Object result = capabilities.chunkAtAsyncMethod().invoke(world, chunkX, chunkZ, generate);
if (result instanceof CompletableFuture<?>) {
@SuppressWarnings("unchecked")
CompletableFuture<Chunk> future = (CompletableFuture<Chunk>) result;
return future;
}
} catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
}
CompletableFuture<Chunk> future = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate);
if (future == null) {
return CompletableFuture.failedFuture(new IllegalStateException("PaperLib did not return a chunk future."));
}
return future;
}
private TimeAccessStrategy resolveTimeAccessStrategy(World world) {
TimeAccessStrategy current = timeAccessStrategy.get();
if (current != null) {
return current;
}
synchronized (timeAccessStrategy) {
current = timeAccessStrategy.get();
if (current != null) {
return current;
}
TimeAccessStrategy resolved = probeTimeAccessStrategy(world);
timeAccessStrategy.set(resolved);
return resolved;
}
}
private TimeAccessStrategy probeTimeAccessStrategy(World world) {
if (world == null) {
return TimeAccessStrategy.unsupported();
}
try {
Method handleMethod = resolveZeroArgMethod(world.getClass(), "getHandle");
if (handleMethod == null) {
return TimeAccessStrategy.unsupported();
}
Object handle = handleMethod.invoke(world);
if (handle == null) {
return TimeAccessStrategy.unsupported();
}
Method readMethod = resolveZeroArgMethod(handle.getClass(), "getDayTime");
Method writeMethod = resolveLongArgMethod(handle.getClass(), "setDayTime");
if (readMethod != null && writeMethod != null) {
return TimeAccessStrategy.forHandle(handleMethod, readMethod, writeMethod, "runtime_handle#setDayTime");
}
Method levelDataMethod = resolveZeroArgMethod(handle.getClass(), "serverLevelData");
if (levelDataMethod == null) {
levelDataMethod = resolveZeroArgMethod(handle.getClass(), "getLevelData");
}
if (levelDataMethod != null) {
Object levelData = levelDataMethod.invoke(handle);
if (levelData != null) {
Method levelDataReadMethod = resolveZeroArgMethod(levelData.getClass(), "getDayTime");
Method levelDataWriteMethod = resolveLongArgMethod(levelData.getClass(), "setDayTime");
if (levelDataReadMethod != null && levelDataWriteMethod != null) {
return TimeAccessStrategy.forLevelData(handleMethod, levelDataMethod, levelDataReadMethod, levelDataWriteMethod, "world_data#setDayTime");
}
}
}
return TimeAccessStrategy.unsupported(handleMethod);
} catch (Throwable ignored) {
return TimeAccessStrategy.unsupported();
}
}
private static Method resolveZeroArgMethod(Class<?> type, String name) {
Class<?> current = type;
while (current != null) {
try {
Method method = current.getDeclaredMethod(name);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException ignored) {
current = current.getSuperclass();
}
}
return null;
}
private static Method resolveLongArgMethod(Class<?> type, String name) {
Class<?> current = type;
while (current != null) {
try {
Method method = current.getDeclaredMethod(name, long.class);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException ignored) {
current = current.getSuperclass();
}
}
return null;
}
private record TimeAccessStrategy(
Method handleMethod,
Method levelDataMethod,
Method readMethod,
Method writeMethod,
Method serverHandleMethod,
Method syncMethod,
String description
) {
static TimeAccessStrategy forHandle(Method handleMethod, Method readMethod, Method writeMethod, String description) {
Method serverHandleMethod = resolveCraftServerMethod("getHandle");
Method syncMethod = resolveServerMethod(serverHandleMethod, "forceTimeSynchronization");
return new TimeAccessStrategy(handleMethod, null, readMethod, writeMethod, serverHandleMethod, syncMethod, description);
}
static TimeAccessStrategy forLevelData(Method handleMethod, Method levelDataMethod, Method readMethod, Method writeMethod, String description) {
Method serverHandleMethod = resolveCraftServerMethod("getHandle");
Method syncMethod = resolveServerMethod(serverHandleMethod, "forceTimeSynchronization");
return new TimeAccessStrategy(handleMethod, levelDataMethod, readMethod, writeMethod, serverHandleMethod, syncMethod, description);
}
static TimeAccessStrategy unsupported() {
return new TimeAccessStrategy(null, null, null, null, null, null, "unsupported");
}
static TimeAccessStrategy unsupported(Method handleMethod) {
return new TimeAccessStrategy(handleMethod, null, null, null, null, null, "unsupported");
}
boolean writable() {
return handleMethod != null && readMethod != null && writeMethod != null;
}
Object readOwner(Object handle) throws ReflectiveOperationException {
if (levelDataMethod == null) {
return handle;
}
return levelDataMethod.invoke(handle);
}
Object[] readArguments(Object handle) {
return new Object[0];
}
Object writeOwner(Object handle) throws ReflectiveOperationException {
if (levelDataMethod == null) {
return handle;
}
return levelDataMethod.invoke(handle);
}
private static Method resolveCraftServerMethod(String name) {
try {
Method method = Bukkit.getServer().getClass().getMethod(name);
method.setAccessible(true);
return method;
} catch (Throwable ignored) {
return null;
}
}
private static Method resolveServerMethod(Method serverHandleMethod, String name) {
if (serverHandleMethod == null) {
return null;
}
try {
Object craftServer = Bukkit.getServer();
if (craftServer == null) {
return null;
}
Object serverHandle = serverHandleMethod.invoke(craftServer);
if (serverHandle == null) {
return null;
}
Method method = serverHandle.getClass().getMethod(name);
method.setAccessible(true);
return method;
} catch (Throwable ignored) {
return null;
}
}
}
}

View File

@@ -0,0 +1,368 @@
package art.arcane.iris.core.runtime;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import art.arcane.iris.Iris;
import art.arcane.volmlib.util.io.IO;
import lombok.Data;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public final class SmokeDiagnosticsService {
private static volatile SmokeDiagnosticsService instance;
private final ConcurrentHashMap<String, SmokeRunReport> reports;
private final AtomicReference<String> latestRunId;
private final AtomicLong runCounter;
private final Gson gson;
private SmokeDiagnosticsService() {
this.reports = new ConcurrentHashMap<>();
this.latestRunId = new AtomicReference<>();
this.runCounter = new AtomicLong(1L);
this.gson = new GsonBuilder().setPrettyPrinting().create();
}
public static SmokeDiagnosticsService get() {
SmokeDiagnosticsService current = instance;
if (current != null) {
return current;
}
synchronized (SmokeDiagnosticsService.class) {
if (instance != null) {
return instance;
}
instance = new SmokeDiagnosticsService();
return instance;
}
}
public SmokeRunHandle beginRun(SmokeRunMode mode, String worldName, boolean studio, boolean headless, String playerName, boolean retainOnFailure) {
long ordinal = runCounter.getAndIncrement();
String runId = String.format("%s-%05d", mode.id(), ordinal);
SmokeRunReport report = new SmokeRunReport();
report.setRunId(runId);
report.setMode(mode.id());
report.setWorldName(worldName);
report.setStudio(studio);
report.setHeadless(headless);
report.setPlayerName(playerName);
report.setRetainOnFailure(retainOnFailure);
report.setStartedAt(System.currentTimeMillis());
report.setOutcome("running");
report.setStage("queued");
report.setLifecycleBackend(art.arcane.iris.core.lifecycle.WorldLifecycleService.get().capabilities().serverFamily().id());
report.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
reports.put(runId, report);
latestRunId.set(runId);
persist(report);
return new SmokeRunHandle(report);
}
public SmokeRunReport latest() {
String runId = latestRunId.get();
if (runId == null) {
return null;
}
return get(runId);
}
public SmokeRunReport get(String runId) {
if (runId == null || runId.isBlank()) {
return null;
}
SmokeRunReport report = reports.get(runId);
if (report != null) {
return snapshot(report);
}
return load(runId);
}
public SmokeRunReport latestPersisted() {
File latestFile = latestFile();
if (!latestFile.exists()) {
return null;
}
try {
return gson.fromJson(IO.readAll(latestFile), SmokeRunReport.class);
} catch (Throwable e) {
return null;
}
}
private SmokeRunReport load(String runId) {
File file = reportFile(runId);
if (!file.exists()) {
return null;
}
try {
return gson.fromJson(IO.readAll(file), SmokeRunReport.class);
} catch (Throwable e) {
return null;
}
}
private void persist(SmokeRunReport report) {
if (report == null || !SmokeRunMode.shouldPersist(report.getMode())) {
return;
}
try {
String json = gson.toJson(report);
File file = reportFile(report.getRunId());
IO.writeAll(file, json);
IO.writeAll(latestFile(), json);
} catch (Throwable e) {
Iris.reportError("Failed to persist smoke report \"" + report.getRunId() + "\".", e);
}
}
private SmokeRunReport snapshot(SmokeRunReport report) {
String json = gson.toJson(report);
return gson.fromJson(json, SmokeRunReport.class);
}
private File reportFile(String runId) {
if (Iris.instance == null) {
File root = new File("plugins/Iris/diagnostics/smoke");
root.mkdirs();
return new File(root, runId + ".json");
}
return Iris.instance.getDataFile("diagnostics", "smoke", runId + ".json");
}
private File latestFile() {
if (Iris.instance == null) {
File root = new File("plugins/Iris/diagnostics/smoke");
root.mkdirs();
return new File(root, "latest.json");
}
return Iris.instance.getDataFile("diagnostics", "smoke", "latest.json");
}
public enum SmokeRunMode {
FULL("full", true),
STUDIO("studio", true),
CREATE("create", true),
BENCHMARK("benchmark", true),
STUDIO_OPEN("studio_open", false),
STUDIO_CLOSE("studio_close", false);
private final String id;
private final boolean persisted;
SmokeRunMode(String id, boolean persisted) {
this.id = id;
this.persisted = persisted;
}
public String id() {
return id;
}
static boolean shouldPersist(String id) {
for (SmokeRunMode mode : values()) {
if (mode.id.equals(id)) {
return mode.persisted;
}
}
return false;
}
}
public final class SmokeRunHandle {
private final SmokeRunReport report;
private SmokeRunHandle(SmokeRunReport report) {
this.report = report;
}
public String runId() {
return report.getRunId();
}
public SmokeRunReport snapshot() {
synchronized (report) {
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
return SmokeDiagnosticsService.this.snapshot(report);
}
}
public void setWorldName(String worldName) {
synchronized (report) {
report.setWorldName(worldName);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void setLifecycleBackend(String backend) {
synchronized (report) {
report.setLifecycleBackend(backend);
persist(report);
}
}
public void setRuntimeBackend(String backend) {
synchronized (report) {
report.setRuntimeBackend(backend);
persist(report);
}
}
public void setEntryChunk(int chunkX, int chunkZ) {
synchronized (report) {
report.setEntryChunkX(chunkX);
report.setEntryChunkZ(chunkZ);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void setGenerationSession(long sessionId, int activeLeases) {
synchronized (report) {
report.setGenerationSessionId(sessionId);
report.setGenerationActiveLeases(activeLeases);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void setDatapackReadiness(DatapackReadinessResult readiness) {
synchronized (report) {
report.setDatapackReadiness(readiness);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void setCloseState(boolean unloadCompletedLive, boolean folderDeletionCompletedLive, boolean startupCleanupQueued) {
synchronized (report) {
report.setCloseUnloadCompletedLive(unloadCompletedLive);
report.setCloseFolderDeletionCompletedLive(folderDeletionCompletedLive);
report.setCloseStartupCleanupQueued(startupCleanupQueued);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void note(String text) {
synchronized (report) {
ArrayList<String> notes = new ArrayList<>(report.getNotes());
notes.add(text);
report.setNotes(List.copyOf(notes));
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void stage(String stage) {
stage(stage, null);
}
public void stage(String stage, String detail) {
synchronized (report) {
report.setStage(stage);
report.setStageDetail(detail);
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
persist(report);
}
}
public void completeSuccess(String finalStage, boolean cleanupApplied) {
synchronized (report) {
report.setStage(finalStage);
report.setOutcome("success");
report.setCleanupApplied(cleanupApplied);
report.setCompletedAt(System.currentTimeMillis());
report.setElapsedMs(report.getCompletedAt() - report.getStartedAt());
persist(report);
}
}
public void completeFailure(String finalStage, Throwable throwable, boolean cleanupApplied) {
synchronized (report) {
report.setStage(finalStage);
report.setOutcome("failed");
report.setCleanupApplied(cleanupApplied);
report.setCompletedAt(System.currentTimeMillis());
report.setElapsedMs(report.getCompletedAt() - report.getStartedAt());
if (throwable != null) {
report.setFailureType(throwable.getClass().getName());
report.setFailureMessage(String.valueOf(throwable.getMessage()));
report.setFailureChain(failureChain(throwable));
report.setFailureStacktrace(stacktrace(throwable));
}
persist(report);
}
}
private List<String> failureChain(Throwable throwable) {
ArrayList<String> chain = new ArrayList<>();
Throwable cursor = throwable;
while (cursor != null) {
chain.add(cursor.getClass().getName() + ": " + String.valueOf(cursor.getMessage()));
cursor = cursor.getCause();
}
return List.copyOf(chain);
}
private String stacktrace(Throwable throwable) {
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
throwable.printStackTrace(printWriter);
printWriter.flush();
return writer.toString();
}
}
@Data
public static final class SmokeRunReport {
private String runId;
private String mode;
private String worldName;
private String stage;
private String stageDetail;
private long startedAt;
private long completedAt;
private long elapsedMs;
private String outcome;
private String lifecycleBackend;
private String runtimeBackend;
private long generationSessionId;
private int generationActiveLeases;
private Integer entryChunkX;
private Integer entryChunkZ;
private boolean studio;
private boolean headless;
private String playerName;
private boolean retainOnFailure;
private boolean cleanupApplied;
private boolean closeUnloadCompletedLive;
private boolean closeFolderDeletionCompletedLive;
private boolean closeStartupCleanupQueued;
private DatapackReadinessResult datapackReadiness;
private String failureType;
private String failureMessage;
private List<String> failureChain = List.of();
private String failureStacktrace;
private List<String> notes = List.of();
}
}

View File

@@ -0,0 +1,418 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.Iris;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.io.IO;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public final class SmokeTestService {
private static volatile SmokeTestService instance;
private final SmokeDiagnosticsService diagnostics;
private SmokeTestService() {
this.diagnostics = SmokeDiagnosticsService.get();
}
public static SmokeTestService get() {
SmokeTestService current = instance;
if (current != null) {
return current;
}
synchronized (SmokeTestService.class) {
if (instance != null) {
return instance;
}
instance = new SmokeTestService();
return instance;
}
}
public String startCreateSmoke(VolmitSender sender, String dimensionKey, long seed, boolean retainOnFailure) {
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.CREATE,
nextWorldName("create"),
false,
true,
null,
retainOnFailure
);
J.a(() -> executeCreateSmoke(handle, sender, dimensionKey, seed, false, true));
return handle.runId();
}
public String startBenchmarkSmoke(VolmitSender sender, String dimensionKey, long seed, boolean retainOnFailure) {
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.BENCHMARK,
nextWorldName("benchmark"),
false,
true,
null,
retainOnFailure
);
J.a(() -> executeCreateSmoke(handle, sender, dimensionKey, seed, true, true));
return handle.runId();
}
public String startStudioSmoke(VolmitSender sender, String dimensionKey, long seed, String playerName, boolean retainOnFailure) {
String normalizedPlayer = normalizePlayerName(playerName);
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.STUDIO,
nextWorldName("studio"),
true,
normalizedPlayer == null,
normalizedPlayer,
retainOnFailure
);
J.a(() -> executeStudioSmoke(handle, sender, dimensionKey, seed, normalizedPlayer, retainOnFailure, true));
return handle.runId();
}
public String startFullSmoke(VolmitSender sender, String dimensionKey, long seed, String playerName, boolean retainOnFailure) {
String normalizedPlayer = normalizePlayerName(playerName);
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.FULL,
nextWorldName("full"),
false,
normalizedPlayer == null,
normalizedPlayer,
retainOnFailure
);
J.a(() -> executeFullSmoke(handle, sender, dimensionKey, seed, normalizedPlayer, retainOnFailure));
return handle.runId();
}
public SmokeDiagnosticsService.SmokeRunReport latest() {
SmokeDiagnosticsService.SmokeRunReport latest = diagnostics.latest();
if (latest != null) {
return latest;
}
return diagnostics.latestPersisted();
}
public SmokeDiagnosticsService.SmokeRunReport get(String runId) {
return diagnostics.get(runId);
}
public WorldInspection inspectWorld(String worldName) {
World world = Bukkit.getWorld(worldName);
if (world == null) {
return null;
}
PlatformChunkGenerator provider = IrisToolbelt.access(world);
boolean studio = provider != null && provider.isStudio();
boolean engineClosed = false;
boolean engineFailing = false;
long generationSessionId = 0L;
int activeLeases = 0;
if (provider != null && provider.getEngine() instanceof IrisEngine irisEngine) {
engineClosed = irisEngine.isClosed();
engineFailing = irisEngine.isFailing();
generationSessionId = irisEngine.getGenerationSessionId();
activeLeases = irisEngine.getGenerationSessions().activeLeases();
}
ArrayList<String> datapackFolders = new ArrayList<>();
File datapacksFolder = ServerConfigurator.resolveDatapacksFolder(world.getWorldFolder());
datapackFolders.add(datapacksFolder.getAbsolutePath());
return new WorldInspection(
world.getName(),
WorldLifecycleService.get().backendNameForWorld(world.getName()),
WorldRuntimeControlService.get().backendName(),
studio,
engineClosed,
engineFailing,
generationSessionId,
activeLeases,
List.copyOf(datapackFolders),
IrisToolbelt.isWorldMaintenanceActive(world)
);
}
private void executeFullSmoke(
SmokeDiagnosticsService.SmokeRunHandle handle,
VolmitSender sender,
String dimensionKey,
long seed,
String playerName,
boolean retainOnFailure
) {
try {
handle.stage("create");
executeCreateSmoke(handle, sender, dimensionKey, seed, false, false);
handle.note("create smoke complete");
handle.stage("benchmark");
executeCreateSmoke(handle, sender, dimensionKey, seed, true, false);
handle.note("benchmark smoke complete");
handle.stage("studio");
executeStudioSmoke(handle, sender, dimensionKey, seed, playerName, retainOnFailure, false);
handle.note("studio smoke complete");
handle.completeSuccess("cleanup", true);
} catch (Throwable e) {
handle.completeFailure("cleanup", e, !retainOnFailure);
}
}
private void executeCreateSmoke(
SmokeDiagnosticsService.SmokeRunHandle handle,
VolmitSender sender,
String dimensionKey,
long seed,
boolean benchmark,
boolean completeHandle
) {
String worldName = nextWorldName(benchmark ? "benchmark" : "create");
handle.setWorldName(worldName);
cleanupTransientPrefix("iris-smoke-");
World world = null;
PlatformChunkGenerator provider = null;
boolean cleanupApplied = false;
try {
IrisCreator creator = IrisToolbelt.createWorld()
.dimension(dimensionKey)
.name(worldName)
.seed(seed)
.sender(sender)
.studio(false)
.benchmark(benchmark)
.studioProgressConsumer((progress, stage) -> handle.stage(mapCreateStage(stage)));
world = creator.create();
provider = IrisToolbelt.access(world);
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
handle.setDatapackReadiness(creator.getLastDatapackReadinessResult());
captureGenerationSession(provider, handle);
if (benchmark) {
handle.stage("apply_world_rules");
WorldRuntimeControlService.get().applyStudioWorldRules(world);
}
handle.stage("cleanup");
cleanupWorld(world, worldName);
cleanupApplied = true;
if (completeHandle) {
handle.completeSuccess("cleanup", true);
}
} catch (Throwable e) {
Iris.reportError("Smoke create failed for world \"" + worldName + "\".", e);
if (!handle.snapshot().isRetainOnFailure()) {
try {
cleanupWorld(world, worldName);
cleanupApplied = true;
} catch (Throwable cleanupError) {
Iris.reportError("Smoke cleanup failed for world \"" + worldName + "\".", cleanupError);
}
}
if (completeHandle) {
handle.completeFailure("cleanup", e, cleanupApplied);
} else {
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new RuntimeException(e);
}
}
}
private void executeStudioSmoke(
SmokeDiagnosticsService.SmokeRunHandle handle,
VolmitSender sender,
String dimensionKey,
long seed,
String playerName,
boolean retainOnFailure,
boolean completeHandle
) {
String worldName = nextWorldName("studio");
handle.setWorldName(worldName);
cleanupTransientPrefix("iris-smoke-");
World world = null;
boolean cleanupApplied = false;
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> future = StudioOpenCoordinator.get().open(
new StudioOpenCoordinator.StudioOpenRequest(
dimensionKey,
null,
sender,
seed,
worldName,
playerName,
false,
retainOnFailure,
SmokeDiagnosticsService.SmokeRunMode.STUDIO,
handle,
completeHandle,
update -> handle.stage(update.stage()),
openedWorld -> {
}
)
);
try {
StudioOpenCoordinator.StudioOpenResult result = future.join();
world = result == null ? null : result.world();
handle.stage("cleanup");
cleanupWorld(world, worldName);
cleanupApplied = true;
if (completeHandle) {
handle.completeSuccess("cleanup", true);
}
} catch (Throwable e) {
if (world != null && !cleanupApplied) {
try {
cleanupWorld(world, worldName);
cleanupApplied = true;
} catch (Throwable cleanupError) {
Iris.reportError("Smoke cleanup failed for world \"" + worldName + "\".", cleanupError);
}
}
if (completeHandle && !"failed".equalsIgnoreCase(handle.snapshot().getOutcome())) {
handle.completeFailure("cleanup", e, cleanupApplied);
}
if (!completeHandle) {
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw new RuntimeException(e);
}
}
}
private void captureGenerationSession(PlatformChunkGenerator provider, SmokeDiagnosticsService.SmokeRunHandle handle) {
if (provider == null || provider.getEngine() == null) {
return;
}
if (provider.getEngine() instanceof IrisEngine irisEngine) {
handle.setGenerationSession(irisEngine.getGenerationSessionId(), irisEngine.getGenerationSessions().activeLeases());
}
}
private void cleanupWorld(World world, String worldName) {
if (world != null) {
PlatformChunkGenerator provider = IrisToolbelt.access(world);
if (provider != null) {
provider.close();
}
WorldLifecycleService.get().unload(world, false);
}
File container = Bukkit.getWorldContainer();
deleteFolder(new File(container, worldName), worldName);
deleteFolder(new File(container, worldName + "_nether"), null);
deleteFolder(new File(container, worldName + "_the_end"), null);
}
private void deleteFolder(File folder, String worldName) {
if (folder == null) {
return;
}
IO.delete(folder);
if (!folder.exists()) {
return;
}
if (worldName == null) {
return;
}
try {
Iris.queueWorldDeletionOnStartup(Collections.singleton(worldName));
} catch (IOException e) {
Iris.reportError("Failed to queue smoke world deletion for \"" + worldName + "\".", e);
}
}
private void cleanupTransientPrefix(String prefix) {
File container = Bukkit.getWorldContainer();
File[] children = container.listFiles();
if (children == null) {
return;
}
for (File child : children) {
if (!child.isDirectory()) {
continue;
}
if (!child.getName().startsWith(prefix)) {
continue;
}
if (Bukkit.getWorld(child.getName()) != null) {
continue;
}
IO.delete(child);
}
}
private String nextWorldName(String mode) {
return "iris-smoke-" + mode + "-" + UUID.randomUUID().toString().substring(0, 8);
}
private String normalizePlayerName(String playerName) {
if (playerName == null) {
return null;
}
String trimmed = playerName.trim();
if (trimmed.isEmpty() || trimmed.equalsIgnoreCase("none")) {
return null;
}
return trimmed;
}
private String mapCreateStage(String stage) {
if (stage == null || stage.isBlank()) {
return "create_world";
}
String normalized = stage.trim().toLowerCase();
return switch (normalized) {
case "resolve_dimension", "resolving dimension" -> "resolve_dimension";
case "prepare_world_pack", "preparing world pack" -> "prepare_world_pack";
case "install_datapacks", "installing datapacks" -> "install_datapacks";
case "create_world", "creating world", "world created" -> "create_world";
default -> normalized.replace(' ', '_');
};
}
public record WorldInspection(
String worldName,
String lifecycleBackend,
String runtimeBackend,
boolean studio,
boolean engineClosed,
boolean engineFailing,
long generationSessionId,
int activeLeaseCount,
List<String> datapackFolders,
boolean maintenanceActive
) {
}
}

View File

@@ -0,0 +1,660 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.tools.IrisCreator;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.exceptions.IrisException;
import art.arcane.volmlib.util.io.IO;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
public final class StudioOpenCoordinator {
private static volatile StudioOpenCoordinator instance;
private final SmokeDiagnosticsService diagnostics;
private StudioOpenCoordinator() {
this.diagnostics = SmokeDiagnosticsService.get();
}
public static StudioOpenCoordinator get() {
StudioOpenCoordinator current = instance;
if (current != null) {
return current;
}
synchronized (StudioOpenCoordinator.class) {
if (instance != null) {
return instance;
}
instance = new StudioOpenCoordinator();
return instance;
}
}
public CompletableFuture<StudioOpenResult> open(StudioOpenRequest request) {
CompletableFuture<StudioOpenResult> future = new CompletableFuture<>();
J.aBukkit(() -> executeOpen(request, future));
return future;
}
public CompletableFuture<StudioCloseResult> closeProject(IrisProject project) {
CompletableFuture<StudioCloseResult> future = new CompletableFuture<>();
J.aBukkit(() -> future.complete(executeClose(project)));
return future;
}
private StudioCloseResult executeClose(IrisProject project) {
if (project == null) {
return new StudioCloseResult(null, true, true, false, null, null);
}
PlatformChunkGenerator provider = project.getActiveProvider();
if (provider == null) {
return new StudioCloseResult(null, true, true, false, null, null);
}
World world = provider.getTarget().getWorld().realWorld();
String worldName = world == null ? provider.getTarget().getWorld().name() : world.getName();
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
SmokeDiagnosticsService.SmokeRunMode.STUDIO_CLOSE,
worldName,
true,
true,
null,
false
);
StudioCloseResult result;
try {
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
if (world != null) {
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
captureGenerationSession(provider, handle);
}
result = closeWorld(provider, worldName, world, true, handle, project);
handle.setCloseState(result.unloadCompletedLive(), result.folderDeletionCompletedLive(), result.startupCleanupQueued());
if (result.failureCause() != null) {
handle.completeFailure("finalize_close", result.failureCause(), result.folderDeletionCompletedLive() || result.startupCleanupQueued());
} else {
handle.completeSuccess("finalize_close", result.folderDeletionCompletedLive() || result.startupCleanupQueued());
}
} catch (Throwable e) {
project.setActiveProvider(null);
handle.completeFailure("finalize_close", e, false);
result = new StudioCloseResult(worldName, false, false, false, e, handle.runId());
}
return result;
}
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
boolean ownsHandle = request.runHandle() == null;
SmokeDiagnosticsService.SmokeRunHandle handle = ownsHandle
? diagnostics.beginRun(
request.mode(),
request.worldName(),
true,
request.playerName() == null || request.playerName().isBlank(),
request.playerName(),
request.retainOnFailure()
)
: request.runHandle();
World world = null;
PlatformChunkGenerator provider = null;
boolean cleanupApplied = false;
try {
updateStage(handle, request, "resolve_dimension", 0.04D);
if (IrisToolbelt.getDimension(request.dimensionKey()) == null) {
throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + ".");
}
updateStage(handle, request, "prepare_world_pack", 0.10D);
cleanupStaleTransientWorlds(request.worldName());
updateStage(handle, request, "install_datapacks", 0.18D);
IrisCreator creator = IrisToolbelt.createWorld()
.seed(request.seed())
.sender(request.sender())
.studio(true)
.name(request.worldName())
.dimension(request.dimensionKey())
.studioProgressConsumer((progress, stage) -> updateStage(handle, request, mapCreatorStage(stage), progress));
world = creator.create();
provider = IrisToolbelt.access(world);
if (provider == null) {
throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\".");
}
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
handle.setDatapackReadiness(creator.getLastDatapackReadinessResult());
captureGenerationSession(provider, handle);
updateStage(handle, request, "apply_world_rules", 0.72D);
WorldRuntimeControlService.get().applyStudioWorldRules(world);
updateStage(handle, request, "prepare_generator", 0.78D);
WorldRuntimeControlService.get().prepareGenerator(world);
Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world);
if (entryAnchor == null) {
throw new IllegalStateException("Studio entry anchor could not be resolved.");
}
long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(120L);
updateStage(handle, request, "request_entry_chunk", 0.84D);
requestEntryChunk(world, entryAnchor, deadline, handle);
updateStage(handle, request, "resolve_safe_entry", 0.90D);
Location safeEntry = resolveSafeEntry(world, entryAnchor, deadline);
if (safeEntry == null) {
throw new IllegalStateException("Studio safe entry resolution timed out.");
}
if (request.playerName() != null && !request.playerName().isBlank()) {
updateStage(handle, request, "teleport_player", 0.96D);
Player player = resolvePlayer(request.playerName());
if (player == null) {
throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online.");
}
long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
Boolean teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(remaining, TimeUnit.MILLISECONDS);
if (!Boolean.TRUE.equals(teleported)) {
throw new IllegalStateException("Studio teleport did not complete successfully.");
}
}
updateStage(handle, request, "finalize_open", 1.00D);
if (request.project() != null) {
request.project().setActiveProvider(provider);
}
if (request.openWorkspace() && request.project() != null) {
request.project().openVSCode(request.sender());
}
if (request.onDone() != null) {
request.onDone().accept(world);
}
if (request.completeHandle()) {
handle.completeSuccess("finalize_open", false);
} else {
handle.stage("finalize_open");
}
future.complete(new StudioOpenResult(world, handle.runId(), safeEntry, creator.getLastDatapackReadinessResult()));
} catch (Throwable e) {
Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
if (!request.retainOnFailure()) {
try {
updateStage(handle, request, "cleanup", 1.00D);
StudioCloseResult cleanupResult = closeWorld(provider, request.worldName(), world, true, handle, request.project());
cleanupApplied = cleanupResult.folderDeletionCompletedLive() || cleanupResult.startupCleanupQueued();
} catch (Throwable cleanupError) {
Iris.reportError("Studio cleanup failed for world \"" + request.worldName() + "\".", cleanupError);
}
}
if (request.completeHandle()) {
handle.completeFailure("cleanup", e, cleanupApplied);
} else {
handle.stage("cleanup", String.valueOf(e.getMessage()));
}
future.completeExceptionally(e);
}
}
private void requestEntryChunk(World world, Location entryAnchor, long deadline, SmokeDiagnosticsService.SmokeRunHandle handle) throws Exception {
int chunkX = entryAnchor.getBlockX() >> 4;
int chunkZ = entryAnchor.getBlockZ() >> 4;
handle.setEntryChunk(chunkX, chunkZ);
long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
waitForEntryChunk(world, chunkX, chunkZ, deadline, null).get(remaining, TimeUnit.MILLISECONDS);
}
private Location resolveSafeEntry(World world, Location entryAnchor, long deadline) throws Exception {
long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
return waitForSafeEntry(world, entryAnchor, deadline, null).get(remaining, TimeUnit.MILLISECONDS);
}
private StudioCloseResult closeWorld(
PlatformChunkGenerator provider,
String worldName,
World world,
boolean deleteFolder,
SmokeDiagnosticsService.SmokeRunHandle handle,
IrisProject project
) {
Throwable failure = null;
boolean unloadCompletedLive = world == null || !isWorldFamilyLoaded(worldName);
boolean folderDeletionCompletedLive = !deleteFolder;
boolean startupCleanupQueued = false;
CompletableFuture<Void> closeFuture = provider == null ? CompletableFuture.completedFuture(null) : CompletableFuture.completedFuture(null);
updateCloseStage(handle, "prepare_close");
if (world != null) {
handle.setWorldName(world.getName());
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
captureGenerationSession(provider, handle);
}
if (world != null) {
updateCloseStage(handle, "evacuate_players");
try {
evacuatePlayers(world);
} catch (Throwable e) {
failure = e;
}
}
if (world != null) {
IrisToolbelt.beginWorldMaintenance(world, "studio-close", true);
}
try {
updateCloseStage(handle, "seal_runtime");
if (project != null) {
project.setActiveProvider(null);
}
if (provider != null) {
captureGenerationSession(provider, handle);
closeFuture = provider.closeAsync();
}
updateCloseStage(handle, "request_unload");
if (worldName != null && !worldName.isBlank()) {
requestWorldFamilyUnload(worldName);
}
updateCloseStage(handle, "await_unload");
if (worldName != null && !worldName.isBlank()) {
long unloadDeadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20L);
CompletableFuture<Void> unloadFuture = waitForWorldFamilyUnload(worldName, unloadDeadline);
try {
unloadFuture.get(Math.max(1000L, unloadDeadline - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
unloadCompletedLive = true;
} catch (TimeoutException e) {
unloadCompletedLive = !isWorldFamilyLoaded(worldName);
} catch (Throwable e) {
failure = failure == null ? unwrapFailure(e) : failure;
}
}
try {
closeFuture.get(20L, TimeUnit.SECONDS);
} catch (Throwable e) {
Throwable cause = unwrapFailure(e);
if (failure == null) {
failure = cause;
}
}
if (deleteFolder && worldName != null && !worldName.isBlank()) {
updateCloseStage(handle, "delete_world_family");
WorldFamilyDeleteResult deleteResult = deleteWorldFamily(worldName, unloadCompletedLive);
folderDeletionCompletedLive = deleteResult.liveDeleted();
startupCleanupQueued = deleteResult.startupCleanupQueued();
}
updateCloseStage(handle, "finalize_close");
} finally {
if (world != null) {
IrisToolbelt.endWorldMaintenance(world, "studio-close");
}
}
handle.setCloseState(unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued);
return new StudioCloseResult(worldName, unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued, failure, handle.runId());
}
private void evacuatePlayers(World world) throws Exception {
if (world == null) {
return;
}
CompletableFuture<Void> future = J.sfut(() -> {
IrisToolbelt.evacuate(world);
return null;
});
if (future != null) {
future.get(10L, TimeUnit.SECONDS);
}
}
private void requestWorldFamilyUnload(String worldName) {
if (worldName == null || worldName.isBlank()) {
return;
}
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
World familyWorld = Bukkit.getWorld(familyWorldName);
if (familyWorld == null) {
continue;
}
Iris.linkMultiverseCore.removeFromConfig(familyWorld);
WorldLifecycleService.get().unload(familyWorld, false);
}
}
private WorldFamilyDeleteResult deleteWorldFamily(String worldName, boolean unloadCompletedLive) {
if (worldName == null || worldName.isBlank()) {
return new WorldFamilyDeleteResult(true, false);
}
File container = Bukkit.getWorldContainer();
boolean liveDeleted = true;
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
File folder = new File(container, familyWorldName);
if (!folder.exists()) {
continue;
}
try {
deleteWorldFolderAsync(folder, 40).get(15L, TimeUnit.SECONDS);
} catch (Throwable e) {
liveDeleted = false;
Iris.reportError("Studio folder deletion retries failed for \"" + folder.getAbsolutePath() + "\".", unwrapFailure(e));
}
if (folder.exists()) {
liveDeleted = false;
}
}
if (liveDeleted) {
return new WorldFamilyDeleteResult(true, false);
}
try {
Iris.queueWorldDeletionOnStartup(Collections.singleton(worldName));
return new WorldFamilyDeleteResult(false, true);
} catch (IOException e) {
if (unloadCompletedLive) {
Iris.reportError("Failed to queue deferred deletion for world \"" + worldName + "\".", e);
}
return new WorldFamilyDeleteResult(false, false);
}
}
private void cleanupStaleTransientWorlds(String worldName) {
File container = Bukkit.getWorldContainer();
LinkedHashSet<String> staleWorldNames = TransientWorldCleanupSupport.collectTransientStudioWorldNames(container);
String requestedBaseName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName);
if (requestedBaseName != null) {
staleWorldNames.add(requestedBaseName);
}
for (String staleWorldName : staleWorldNames) {
if (Bukkit.getWorld(staleWorldName) != null) {
continue;
}
deleteWorldFamily(staleWorldName, true);
}
}
private void captureGenerationSession(PlatformChunkGenerator provider, SmokeDiagnosticsService.SmokeRunHandle handle) {
if (provider == null || provider.getEngine() == null) {
return;
}
if (provider.getEngine() instanceof IrisEngine irisEngine) {
handle.setGenerationSession(irisEngine.getGenerationSessionId(), irisEngine.getGenerationSessions().activeLeases());
}
}
private void updateStage(SmokeDiagnosticsService.SmokeRunHandle handle, StudioOpenRequest request, String stage, double progress) {
handle.stage(stage);
if (request.progressConsumer() != null) {
request.progressConsumer().accept(new StudioOpenProgress(progress, stage));
}
}
private String mapCreatorStage(String stage) {
if (stage == null || stage.isBlank()) {
return "create_world";
}
String normalized = stage.trim().toLowerCase();
return switch (normalized) {
case "resolve_dimension", "resolving dimension" -> "resolve_dimension";
case "prepare_world_pack", "preparing world pack" -> "prepare_world_pack";
case "install_datapacks", "installing datapacks", "datapacks ready" -> "install_datapacks";
case "create_world", "creating world", "world created" -> "create_world";
default -> normalized.replace(' ', '_');
};
}
private CompletableFuture<Void> waitForEntryChunk(World world, int chunkX, int chunkZ, long deadline, Throwable lastFailure) {
long now = System.currentTimeMillis();
if (now >= deadline) {
return CompletableFuture.failedFuture(timeoutFailure("Studio entry chunk request timed out.", lastFailure));
}
long attemptTimeout = Math.min(Math.max(1000L, deadline - now), 3000L);
CompletableFuture<org.bukkit.Chunk> request = withAttemptTimeout(
WorldRuntimeControlService.get().requestChunkAsync(world, chunkX, chunkZ, true),
attemptTimeout,
"Studio entry chunk request attempt timed out."
);
return request.handle((chunk, throwable) -> {
if (throwable == null && world.isChunkLoaded(chunkX, chunkZ)) {
return CompletableFuture.<Void>completedFuture(null);
}
Throwable nextFailure = throwable == null ? lastFailure : unwrapFailure(throwable);
if (System.currentTimeMillis() >= deadline) {
return CompletableFuture.<Void>failedFuture(timeoutFailure("Studio entry chunk request timed out.", nextFailure));
}
return delayFuture(1000L).thenCompose(ignored -> waitForEntryChunk(world, chunkX, chunkZ, deadline, nextFailure));
}).thenCompose(next -> next);
}
private CompletableFuture<Location> waitForSafeEntry(World world, Location entryAnchor, long deadline, Throwable lastFailure) {
long now = System.currentTimeMillis();
if (now >= deadline) {
return CompletableFuture.failedFuture(timeoutFailure("Studio safe-entry resolution timed out.", lastFailure));
}
long attemptTimeout = Math.min(Math.max(1000L, deadline - now), 3000L);
CompletableFuture<Location> resolve = withAttemptTimeout(
WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor),
attemptTimeout,
"Studio safe-entry resolution attempt timed out."
);
return resolve.handle((location, throwable) -> {
if (throwable == null && location != null) {
return CompletableFuture.completedFuture(location);
}
Throwable nextFailure = throwable == null ? lastFailure : unwrapFailure(throwable);
if (System.currentTimeMillis() >= deadline) {
return CompletableFuture.<Location>failedFuture(timeoutFailure("Studio safe-entry resolution timed out.", nextFailure));
}
return delayFuture(250L).thenCompose(ignored -> waitForSafeEntry(world, entryAnchor, deadline, nextFailure));
}).thenCompose(next -> next);
}
private CompletableFuture<Void> waitForWorldFamilyUnload(String worldName, long deadline) {
if (worldName == null || !isWorldFamilyLoaded(worldName) || System.currentTimeMillis() >= deadline) {
return CompletableFuture.completedFuture(null);
}
return delayFuture(100L).thenCompose(ignored -> waitForWorldFamilyUnload(worldName, deadline));
}
private CompletableFuture<Void> deleteWorldFolderAsync(File folder, int attemptsRemaining) {
if (folder == null || !folder.exists()) {
return CompletableFuture.completedFuture(null);
}
IO.delete(folder);
if (!folder.exists()) {
return CompletableFuture.completedFuture(null);
}
if (attemptsRemaining <= 1) {
return CompletableFuture.failedFuture(new IllegalStateException("World folder still exists after deletion retries: " + folder.getAbsolutePath()));
}
return delayFuture(250L).thenCompose(ignored -> deleteWorldFolderAsync(folder, attemptsRemaining - 1));
}
private CompletableFuture<Void> delayFuture(long delayMillis) {
long safeDelay = Math.max(0L, delayMillis);
return CompletableFuture.runAsync(() -> {
}, CompletableFuture.delayedExecutor(safeDelay, TimeUnit.MILLISECONDS));
}
private <T> CompletableFuture<T> withAttemptTimeout(CompletableFuture<T> source, long timeoutMillis, String message) {
CompletableFuture<T> future = new CompletableFuture<>();
source.whenComplete((value, throwable) -> {
if (throwable != null) {
future.completeExceptionally(unwrapFailure(throwable));
return;
}
future.complete(value);
});
delayFuture(timeoutMillis).whenComplete((ignored, throwable) -> {
if (!future.isDone()) {
future.completeExceptionally(new TimeoutException(message));
}
});
return future;
}
private IllegalStateException timeoutFailure(String message, Throwable lastFailure) {
if (lastFailure == null) {
return new IllegalStateException(message);
}
return new IllegalStateException(message, lastFailure);
}
private Throwable unwrapFailure(Throwable throwable) {
Throwable cursor = throwable;
while (cursor instanceof CompletionException || cursor instanceof ExecutionException) {
if (cursor.getCause() == null) {
break;
}
cursor = cursor.getCause();
}
return cursor;
}
private Player resolvePlayer(String playerName) {
Player exact = Bukkit.getPlayerExact(playerName);
if (exact != null) {
return exact;
}
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.getName().equalsIgnoreCase(playerName)) {
return player;
}
}
return null;
}
private void updateCloseStage(SmokeDiagnosticsService.SmokeRunHandle handle, String stage) {
handle.stage(stage);
}
private boolean isWorldFamilyLoaded(String worldName) {
if (worldName == null || worldName.isBlank()) {
return false;
}
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
if (Bukkit.getWorld(familyWorldName) != null) {
return true;
}
}
return false;
}
public record StudioOpenRequest(
String dimensionKey,
IrisProject project,
VolmitSender sender,
long seed,
String worldName,
String playerName,
boolean openWorkspace,
boolean retainOnFailure,
SmokeDiagnosticsService.SmokeRunMode mode,
SmokeDiagnosticsService.SmokeRunHandle runHandle,
boolean completeHandle,
Consumer<StudioOpenProgress> progressConsumer,
Consumer<World> onDone
) {
public static StudioOpenRequest studioProject(IrisProject project, VolmitSender sender, long seed, Consumer<StudioOpenProgress> progressConsumer, Consumer<World> onDone) {
String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null;
return new StudioOpenRequest(
project.getName(),
project,
sender,
seed,
"iris-" + UUID.randomUUID(),
playerName,
true,
false,
SmokeDiagnosticsService.SmokeRunMode.STUDIO_OPEN,
null,
true,
progressConsumer,
onDone
);
}
}
public record StudioOpenProgress(double progress, String stage) {
}
public record StudioOpenResult(World world, String runId, Location entryLocation, DatapackReadinessResult datapackReadiness) {
}
public record StudioCloseResult(
String worldName,
boolean unloadCompletedLive,
boolean folderDeletionCompletedLive,
boolean startupCleanupQueued,
Throwable failureCause,
String runId
) {
public boolean successful() {
return failureCause == null;
}
}
private record WorldFamilyDeleteResult(boolean liveDeleted, boolean startupCleanupQueued) {
}
}

View File

@@ -0,0 +1,91 @@
package art.arcane.iris.core.runtime;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
public final class TransientWorldCleanupSupport {
private static final Pattern TRANSIENT_STUDIO_WORLD_PATTERN = Pattern.compile("^iris-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE);
private TransientWorldCleanupSupport() {
}
public static boolean isTransientStudioWorldName(String worldName) {
return transientStudioBaseWorldName(worldName) != null;
}
public static String transientStudioBaseWorldName(String worldName) {
if (worldName == null || worldName.isBlank()) {
return null;
}
String candidate = worldName.trim();
if (candidate.endsWith("_nether")) {
candidate = candidate.substring(0, candidate.length() - "_nether".length());
} else if (candidate.endsWith("_the_end")) {
candidate = candidate.substring(0, candidate.length() - "_the_end".length());
}
if (!TRANSIENT_STUDIO_WORLD_PATTERN.matcher(candidate).matches()) {
return null;
}
return candidate;
}
public static List<String> worldFamilyNames(String worldName) {
ArrayList<String> names = new ArrayList<>();
String normalized = normalizeWorldName(worldName);
if (normalized == null) {
return names;
}
names.add(normalized);
names.add(normalized + "_nether");
names.add(normalized + "_the_end");
return names;
}
public static LinkedHashSet<String> collectTransientStudioWorldNames(File worldContainer) {
LinkedHashSet<String> names = new LinkedHashSet<>();
if (worldContainer == null) {
return names;
}
File[] children = worldContainer.listFiles();
if (children == null) {
return names;
}
for (File child : children) {
if (child == null || !child.isDirectory()) {
continue;
}
String baseName = transientStudioBaseWorldName(child.getName());
if (baseName == null) {
continue;
}
names.add(baseName);
}
return names;
}
private static String normalizeWorldName(String worldName) {
if (worldName == null) {
return null;
}
String normalized = worldName.trim();
if (normalized.isEmpty()) {
return null;
}
return normalized.toLowerCase(Locale.ROOT).equals(normalized) ? normalized : normalized;
}
}

View File

@@ -0,0 +1,21 @@
package art.arcane.iris.core.runtime;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;
interface WorldRuntimeControlBackend {
String backendName();
String describeCapabilities();
OptionalLong readDayTime(World world);
boolean writeDayTime(World world, long dayTime) throws ReflectiveOperationException;
void syncTime(World world);
CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate);
}

View File

@@ -0,0 +1,483 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
import art.arcane.iris.core.lifecycle.ServerFamily;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.service.BoardSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.GameRule;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.world.TimeSkipEvent;
import org.bukkit.plugin.PluginManager;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public final class WorldRuntimeControlService {
private static volatile WorldRuntimeControlService instance;
private final CapabilitySnapshot capabilities;
private final WorldRuntimeControlBackend backend;
private final String capabilityDescription;
private WorldRuntimeControlService(CapabilitySnapshot capabilities) {
this.capabilities = capabilities;
this.backend = selectBackend(capabilities);
this.capabilityDescription = "family=" + capabilities.serverFamily().id()
+ ", backend=" + backend.backendName()
+ ", " + backend.describeCapabilities();
}
public static WorldRuntimeControlService get() {
WorldRuntimeControlService current = instance;
if (current != null) {
return current;
}
synchronized (WorldRuntimeControlService.class) {
if (instance != null) {
return instance;
}
CapabilitySnapshot capabilities = WorldLifecycleService.get().capabilities();
instance = new WorldRuntimeControlService(capabilities);
Iris.info("WorldRuntimeControl capabilities: %s", instance.capabilityDescription);
return instance;
}
}
public String backendName() {
return backend.backendName();
}
public String capabilityDescription() {
return capabilityDescription;
}
public OptionalLong readDayTime(World world) {
return backend.readDayTime(world);
}
public boolean applyStudioWorldRules(World world) {
if (world == null) {
return false;
}
Iris.linkMultiverseCore.removeFromConfig(world);
if (!IrisSettings.get().getStudio().isDisableTimeAndWeather()) {
return true;
}
setBooleanGameRule(world, false, "ADVANCE_WEATHER", "DO_WEATHER_CYCLE", "WEATHER_CYCLE", "doWeatherCycle", "weatherCycle");
setBooleanGameRule(world, false, "ADVANCE_TIME", "DO_DAYLIGHT_CYCLE", "DAYLIGHT_CYCLE", "doDaylightCycle", "daylightCycle");
applyNoonTimeLock(world);
return true;
}
public boolean applyNoonTimeLock(World world) {
if (world == null) {
return false;
}
if (!hasMutableClock(world)) {
return false;
}
OptionalLong currentTime = readDayTime(world);
if (currentTime.isEmpty()) {
return false;
}
long skipAmount = (6000L - currentTime.getAsLong()) % 24000L;
if (skipAmount < 0L) {
skipAmount += 24000L;
}
TimeSkipEvent event = new TimeSkipEvent(world, TimeSkipEvent.SkipReason.CUSTOM, skipAmount);
PluginManager pluginManager = Bukkit.getPluginManager();
if (pluginManager != null) {
pluginManager.callEvent(event);
}
if (event.isCancelled()) {
return false;
}
try {
boolean written = backend.writeDayTime(world, currentTime.getAsLong() + event.getSkipAmount());
if (!written) {
return false;
}
backend.syncTime(world);
return true;
} catch (Throwable e) {
Iris.reportError("Runtime time control failed for world \"" + world.getName() + "\".", e);
return false;
}
}
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
return backend.requestChunkAsync(world, chunkX, chunkZ, generate);
}
public void prepareGenerator(World world) {
if (world == null) {
return;
}
try {
art.arcane.iris.engine.platform.PlatformChunkGenerator provider = art.arcane.iris.core.tools.IrisToolbelt.access(world);
if (provider == null) {
return;
}
art.arcane.iris.engine.framework.Engine engine = provider.getEngine();
if (engine == null) {
return;
}
engine.getMantle().getComponents();
engine.getMantle().getRealRadius();
} catch (Throwable e) {
Iris.reportError("Failed to prepare generator state for world \"" + world.getName() + "\".", e);
}
}
public Location resolveEntryAnchor(World world) {
if (world == null) {
return null;
}
PlatformChunkGenerator provider = IrisToolbelt.access(world);
return resolveEntryAnchor(world, provider);
}
static Location resolveEntryAnchor(World world, PlatformChunkGenerator provider) {
if (world == null) {
return null;
}
if (provider != null && provider.isStudio()) {
Location initialSpawn = provider.getInitialSpawnLocation(world);
if (initialSpawn != null) {
return initialSpawn.clone();
}
}
Location spawnLocation = world.getSpawnLocation();
if (spawnLocation != null) {
return spawnLocation.clone();
}
int minY = world.getMinHeight() + 1;
int y = Math.max(minY, 96);
return new Location(world, 0.5D, y, 0.5D);
}
public CompletableFuture<Location> resolveSafeEntry(World world, Location source) {
if (world == null || source == null) {
return CompletableFuture.completedFuture(null);
}
int chunkX = source.getBlockX() >> 4;
int chunkZ = source.getBlockZ() >> 4;
return requestChunkAsync(world, chunkX, chunkZ, true).thenCompose(chunk -> {
CompletableFuture<Location> future = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
future.complete(findTopSafeLocation(world, source));
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
if (!scheduled) {
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule safe-entry surface resolve for " + world.getName() + "@" + chunkX + "," + chunkZ + "."));
}
return future;
});
}
public CompletableFuture<Boolean> teleport(Player player, Location location) {
if (player == null || location == null) {
return CompletableFuture.completedFuture(false);
}
CompletableFuture<Boolean> future = new CompletableFuture<>();
boolean scheduled = J.runEntity(player, () -> {
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(player, location);
if (teleportFuture == null) {
future.complete(false);
return;
}
teleportFuture.whenComplete((success, throwable) -> {
if (throwable != null) {
future.completeExceptionally(throwable);
return;
}
if (Boolean.TRUE.equals(success)) {
J.runEntity(player, () -> Iris.service(BoardSVC.class).updatePlayer(player));
future.complete(true);
return;
}
future.complete(false);
});
});
if (!scheduled) {
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule teleport for " + player.getName() + "."));
}
return future;
}
public boolean hasMutableClock(World world) {
try {
Object handle = invokeNoArg(world, "getHandle");
if (handle == null) {
return false;
}
Object dimensionTypeHolder = invokeNoArg(handle, "dimensionTypeRegistration");
Object dimensionType = unwrapDimensionType(dimensionTypeHolder);
if (dimensionType == null) {
return false;
}
return !dimensionTypeHasFixedTime(dimensionType);
} catch (Throwable e) {
return false;
}
}
private static WorldRuntimeControlBackend selectBackend(CapabilitySnapshot capabilities) {
ServerFamily family = capabilities.serverFamily();
if (family.isPaperLike()) {
return new PaperLikeRuntimeControlBackend(capabilities);
}
return new BukkitPublicRuntimeControlBackend(capabilities);
}
static Location findTopSafeLocation(World world, Location source) {
int x = source.getBlockX();
int z = source.getBlockZ();
float yaw = source.getYaw();
float pitch = source.getPitch();
for (int y : buildSafeLocationScanOrder(world, source)) {
if (isSafeStandingLocation(world, x, y, z)) {
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
}
}
return null;
}
static int[] buildSafeLocationScanOrder(World world, Location source) {
int x = source.getBlockX();
int z = source.getBlockZ();
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
int highestY = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(x, z) + 1));
int[] scanOrder = new int[maxY - minY + 1];
int index = 0;
for (int y = highestY; y >= minY; y--) {
scanOrder[index++] = y;
}
for (int y = highestY + 1; y <= maxY; y++) {
scanOrder[index++] = y;
}
return scanOrder;
}
private static boolean isSafeStandingLocation(World world, int x, int y, int z) {
if (y <= world.getMinHeight() || y >= world.getMaxHeight() - 1) {
return false;
}
Block below = world.getBlockAt(x, y - 1, z);
Block feet = world.getBlockAt(x, y, z);
Block head = world.getBlockAt(x, y + 1, z);
Material belowType = below.getType();
if (!belowType.isSolid()) {
return false;
}
if (Tag.LEAVES.isTagged(belowType)) {
return false;
}
if (belowType == Material.LAVA
|| belowType == Material.MAGMA_BLOCK
|| belowType == Material.FIRE
|| belowType == Material.SOUL_FIRE
|| belowType == Material.CAMPFIRE
|| belowType == Material.SOUL_CAMPFIRE) {
return false;
}
if (feet.getType().isSolid() || head.getType().isSolid()) {
return false;
}
if (feet.isLiquid() || head.isLiquid()) {
return false;
}
return true;
}
@SuppressWarnings("unchecked")
private static void setBooleanGameRule(World world, boolean value, String... names) {
GameRule<Boolean> gameRule = resolveBooleanGameRule(world, names);
if (gameRule != null) {
world.setGameRule(gameRule, value);
}
}
@SuppressWarnings("unchecked")
private static GameRule<Boolean> resolveBooleanGameRule(World world, String... names) {
if (world == null || names == null || names.length == 0) {
return null;
}
Set<String> candidates = buildRuleNameCandidates(names);
for (String name : candidates) {
if (name == null || name.isBlank()) {
continue;
}
try {
Field field = GameRule.class.getField(name);
Object value = field.get(null);
if (value instanceof GameRule<?> gameRule && Boolean.class.equals(gameRule.getType())) {
return (GameRule<Boolean>) gameRule;
}
} catch (Throwable ignored) {
}
try {
GameRule<?> byName = GameRule.getByName(name);
if (byName != null && Boolean.class.equals(byName.getType())) {
return (GameRule<Boolean>) byName;
}
} catch (Throwable ignored) {
}
}
String[] availableRules = world.getGameRules();
if (availableRules == null || availableRules.length == 0) {
return null;
}
Set<String> normalizedCandidates = new LinkedHashSet<>();
for (String candidate : candidates) {
if (candidate != null && !candidate.isBlank()) {
normalizedCandidates.add(normalizeRuleName(candidate));
}
}
for (String availableRule : availableRules) {
String normalizedAvailable = normalizeRuleName(availableRule);
if (!normalizedCandidates.contains(normalizedAvailable)) {
continue;
}
try {
GameRule<?> byName = GameRule.getByName(availableRule);
if (byName != null && Boolean.class.equals(byName.getType())) {
return (GameRule<Boolean>) byName;
}
} catch (Throwable ignored) {
}
}
return null;
}
private static Set<String> buildRuleNameCandidates(String... names) {
Set<String> candidates = new LinkedHashSet<>();
for (String name : names) {
if (name == null || name.isBlank()) {
continue;
}
candidates.add(name);
candidates.add(name.toUpperCase());
candidates.add(name.toLowerCase());
}
return candidates;
}
private static String normalizeRuleName(String name) {
if (name == null) {
return "";
}
StringBuilder builder = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) {
char current = name.charAt(i);
if (Character.isLetterOrDigit(current)) {
builder.append(Character.toLowerCase(current));
}
}
return builder.toString();
}
private static boolean dimensionTypeHasFixedTime(Object dimensionType) throws ReflectiveOperationException {
Object fixedTimeFlag;
try {
fixedTimeFlag = invokeNoArg(dimensionType, "hasFixedTime");
} catch (NoSuchMethodException ignored) {
Object fixedTime = invokeNoArg(dimensionType, "fixedTime");
if (fixedTime instanceof OptionalLong optionalLong) {
return optionalLong.isPresent();
}
if (fixedTime instanceof Optional<?> optional) {
return optional.isPresent();
}
return false;
}
return fixedTimeFlag instanceof Boolean && (Boolean) fixedTimeFlag;
}
private static Object unwrapDimensionType(Object dimensionTypeHolder) throws ReflectiveOperationException {
if (dimensionTypeHolder == null) {
return null;
}
Class<?> holderClass = dimensionTypeHolder.getClass();
if (holderClass.getName().startsWith("net.minecraft.world.level.dimension.")) {
return dimensionTypeHolder;
}
Method valueMethod = holderClass.getMethod("value");
return valueMethod.invoke(dimensionTypeHolder);
}
private static Object invokeNoArg(Object instance, String methodName) throws ReflectiveOperationException {
Method method = instance.getClass().getMethod(methodName);
return method.invoke(instance);
}
}

View File

@@ -74,7 +74,7 @@ public enum Mode {
String[] info = new String[]{
"",
padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RELEASE]",
padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1]",
padd2 + C.GRAY + " Version: " + color + version,
padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)",
padd2 + C.GRAY + " Server: " + color + serverVersion,
@@ -89,10 +89,8 @@ public enum Mode {
StringBuilder builder = new StringBuilder("\n\n");
for (int i = 0; i < splash.length; i++) {
builder.append(splash[i]);
if (i < info.length) {
builder.append(info[i]);
}
builder.append("\n");
builder.append(info[i]);
builder.append("\n");
}
Iris.info(builder.toString());

View File

@@ -22,10 +22,12 @@ import com.google.gson.JsonSyntaxException;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pack.IrisPack;
import art.arcane.iris.core.project.IrisProject;
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.object.IrisDimension;
@@ -46,7 +48,10 @@ import org.zeroturnaround.zip.commons.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
public class StudioSVC implements IrisService {
@@ -55,6 +60,7 @@ public class StudioSVC implements IrisService {
private static final AtomicCache<Integer> counter = new AtomicCache<>();
private final KMap<String, String> cacheListing = null;
private IrisProject activeProject;
private CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> activeClose;
@Override
public void onEnable() {
@@ -78,21 +84,41 @@ public class StudioSVC implements IrisService {
public void onDisable() {
Iris.debug("Studio Mode Active: Closing Projects");
boolean stopping = IrisToolbelt.isServerStopping();
LinkedHashSet<String> worldNamesToDelete = new LinkedHashSet<>(TransientWorldCleanupSupport.collectTransientStudioWorldNames(Bukkit.getWorldContainer()));
for (World i : Bukkit.getWorlds()) {
if (IrisToolbelt.isIrisWorld(i)) {
if (IrisToolbelt.isStudio(i)) {
PlatformChunkGenerator generator = IrisToolbelt.access(i);
if (!stopping) {
IrisToolbelt.evacuate(i);
}
if (generator != null) {
generator.close();
}
if (activeProject != null) {
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
if (activeProvider != null) {
String activeWorldName = activeProvider.getTarget().getWorld().name();
if (activeWorldName != null && !activeWorldName.isBlank()) {
worldNamesToDelete.add(activeWorldName);
}
}
}
for (World i : Bukkit.getWorlds()) {
if (!IrisToolbelt.isIrisWorld(i) || !IrisToolbelt.isStudio(i)) {
continue;
}
worldNamesToDelete.add(i.getName());
PlatformChunkGenerator generator = IrisToolbelt.access(i);
if (!stopping) {
destroyStudioWorld(i, generator);
continue;
}
if (generator != null) {
try {
generator.close();
} catch (Throwable e) {
Iris.reportError("Failed to close studio generator for \"" + i.getName() + "\" during shutdown.", e);
}
}
}
activeProject = null;
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
}
public IrisDimension installIntoWorld(VolmitSender sender, String type, File folder) {
@@ -348,20 +374,46 @@ public class StudioSVC implements IrisService {
open(sender, seed, dimm, (w) -> {
});
} catch (Exception e) {
Iris.reportError(e);
Iris.reportError("Failed to open studio world \"" + dimm + "\".", e);
sender.sendMessage("Failed to open studio world: " + e.getMessage());
Iris.error("Studio world creation failed: " + e.getMessage());
}
}
public void open(VolmitSender sender, long seed, String dimm, Consumer<World> onDone) throws IrisException {
if (isProjectOpen()) {
close();
}
CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> pendingClose = close();
pendingClose.whenComplete((closeResult, closeThrowable) -> {
if (closeThrowable != null) {
Iris.reportError("Failed while closing an existing studio project before opening \"" + dimm + "\".", closeThrowable);
J.s(() -> sender.sendMessage("Failed to close the existing studio project: " + closeThrowable.getMessage()));
return;
}
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimm));
activeProject = project;
project.open(sender, seed, onDone);
if (closeResult != null && closeResult.failureCause() != null) {
Throwable failure = closeResult.failureCause();
Iris.reportError("Failed while closing an existing studio project before opening \"" + dimm + "\".", failure);
J.s(() -> sender.sendMessage("Failed to close the existing studio project: " + failure.getMessage()));
return;
}
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimm));
activeProject = project;
try {
project.open(sender, seed, onDone).whenComplete((result, throwable) -> {
if (throwable == null) {
return;
}
if (activeProject == project && !project.isOpen()) {
activeProject = null;
}
});
} catch (IrisException e) {
if (activeProject == project) {
activeProject = null;
}
J.s(() -> sender.sendMessage("Failed to open studio world: " + e.getMessage()));
}
});
}
public void openVSCode(VolmitSender sender, String dim) {
@@ -376,11 +428,89 @@ public class StudioSVC implements IrisService {
return Iris.instance.getDataFileList(WORKSPACE_NAME, sub);
}
public void close() {
if (isProjectOpen()) {
Iris.debug("Closing Active Project");
activeProject.close();
activeProject = null;
public CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> close() {
if (activeClose != null && !activeClose.isDone()) {
return activeClose;
}
if (activeProject == null) {
return CompletableFuture.completedFuture(new art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null, null));
}
Iris.debug("Closing Active Project");
IrisProject project = activeProject;
activeProject = null;
activeClose = project.close();
activeClose.whenComplete((result, throwable) -> activeClose = null);
return activeClose;
}
private void destroyStudioWorld(World world, PlatformChunkGenerator generator) {
try {
IrisToolbelt.evacuate(world);
} catch (Throwable e) {
Iris.reportError("Failed to evacuate studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
}
if (generator != null) {
try {
generator.close();
} catch (Throwable e) {
Iris.reportError("Failed to close studio generator for \"" + world.getName() + "\" during shutdown cleanup.", e);
}
}
try {
WorldLifecycleService.get().unload(world, false);
} catch (Throwable e) {
Iris.reportError("Failed to unload studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
}
deleteTransientStudioFolders(world.getName());
}
private void deleteTransientStudioFolders(String worldName) {
if (worldName == null || worldName.isBlank()) {
return;
}
File container = Bukkit.getWorldContainer();
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
File folder = new File(container, familyWorldName);
if (!folder.exists()) {
continue;
}
IO.delete(folder);
}
}
private void queueStudioWorldDeletionOnStartup(LinkedHashSet<String> worldNamesToDelete) {
if (worldNamesToDelete.isEmpty()) {
return;
}
LinkedHashSet<String> normalizedNames = new LinkedHashSet<>();
for (String worldName : worldNamesToDelete) {
String baseWorldName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName);
if (baseWorldName != null) {
normalizedNames.add(baseWorldName);
continue;
}
if (worldName != null && !worldName.isBlank()) {
normalizedNames.add(worldName);
}
}
if (normalizedNames.isEmpty()) {
return;
}
try {
Iris.queueWorldDeletionOnStartup(List.copyOf(normalizedNames));
} catch (IOException e) {
Iris.reportError("Failed to queue studio world deletion on startup.", e);
}
}

View File

@@ -24,14 +24,16 @@ import art.arcane.iris.core.IrisRuntimeSchedulerMode;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.ServerConfigurator;
import art.arcane.iris.core.link.FoliaWorldsLink;
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
import art.arcane.iris.core.lifecycle.WorldLifecycleRequest;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.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.core.runtime.DatapackReadinessResult;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
@@ -42,7 +44,6 @@ import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.scheduling.O;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
import io.papermc.lib.PaperLib;
import lombok.Data;
@@ -52,21 +53,22 @@ import org.bukkit.block.Block;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.world.TimeSkipEvent;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.Duration;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.IntSupplier;
@@ -79,9 +81,6 @@ import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
@Data
@Accessors(fluent = true, chain = true)
public class IrisCreator {
private static final int STUDIO_PREWARM_RADIUS_CHUNKS = 1;
private static final Duration STUDIO_PREWARM_TIMEOUT = Duration.ofSeconds(45L);
/**
* Specify an area to pregenerate during creation
*/
@@ -114,6 +113,11 @@ public class IrisCreator {
*/
private boolean benchmark = false;
private BiConsumer<Double, String> studioProgressConsumer;
private DatapackReadinessResult lastDatapackReadinessResult;
public DatapackReadinessResult getLastDatapackReadinessResult() {
return lastDatapackReadinessResult;
}
public static boolean removeFromBukkitYml(String name) throws IOException {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
@@ -144,7 +148,7 @@ public class IrisCreator {
throw new IrisException("You cannot invoke create() on the main thread.");
}
reportStudioProgress(0.02D, "Preparing studio open");
reportStudioProgress(0.02D, "resolve_dimension");
if (studio()) {
World existing = Bukkit.getWorld(name());
@@ -155,7 +159,7 @@ public class IrisCreator {
}
}
reportStudioProgress(0.08D, "Resolving dimension");
reportStudioProgress(0.08D, "resolve_dimension");
IrisDimension d = IrisToolbelt.getDimension(dimension());
if (d == null) {
@@ -165,7 +169,7 @@ public class IrisCreator {
if (sender == null)
sender = Iris.getSender();
reportStudioProgress(0.16D, "Preparing world pack");
reportStudioProgress(0.16D, "prepare_world_pack");
if (!studio() || benchmark) {
Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name()));
}
@@ -174,12 +178,10 @@ public class IrisCreator {
Iris.info("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
+ ", regionizedRuntime=" + FoliaScheduler.isRegionizedRuntime(Bukkit.getServer()));
}
prebakeNoisemapsBeforeWorldCreate(d);
reportStudioProgress(0.28D, "Installing datapacks");
reportStudioProgress(0.28D, "install_datapacks");
AtomicDouble pp = new AtomicDouble(0);
O<Boolean> done = new O<>();
done.set(false);
AtomicBoolean done = new AtomicBoolean(false);
WorldCreator wc = new IrisWorldCreator()
.dimension(dimension)
.name(name)
@@ -199,104 +201,63 @@ public class IrisCreator {
extraWorldDatapackFoldersByPack = new KMap<>();
extraWorldDatapackFoldersByPack.put(d.getLoadKey(), studioDatapackFolders);
}
if (ServerConfigurator.installDataPacks(verifyDataPacks, includeExternalDataPacks, extraWorldDatapackFoldersByPack)) {
throw new IrisException("Datapacks were missing!");
lastDatapackReadinessResult = DatapackReadinessResult.installForStudioWorld(
d.getLoadKey(),
d.getDimensionTypeKey(),
new File(Bukkit.getWorldContainer(), name()),
verifyDataPacks,
includeExternalDataPacks,
extraWorldDatapackFoldersByPack
);
if (!"ok".equals(lastDatapackReadinessResult.getExternalDatapackInstallResult())) {
throw new IrisException("Datapack external install failed: " + lastDatapackReadinessResult.getExternalDatapackInstallResult());
}
reportStudioProgress(0.40D, "Datapacks ready");
if (lastDatapackReadinessResult.isRestartRequired()) {
throw new IrisException("Datapack install requested a server restart for "
+ d.getLoadKey()
+ ". folders="
+ lastDatapackReadinessResult.getResolvedDatapackFolders());
}
if (!lastDatapackReadinessResult.isVerificationPassed()) {
throw new IrisException("Datapack readiness verification failed for "
+ d.getLoadKey()
+ ". missingPaths="
+ lastDatapackReadinessResult.getMissingPaths()
+ ", folders="
+ lastDatapackReadinessResult.getResolvedDatapackFolders());
}
reportStudioProgress(0.40D, "install_datapacks");
PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator();
if (access == null) throw new IrisException("Access is null. Something bad happened.");
J.a(() -> {
IntSupplier g = () -> {
if (access.getEngine() == null) {
return 0;
}
return access.getEngine().getGenerated();
};
if(!benchmark) {
int req = access.getSpawnChunks().join();
for (int c = 0; c < req && !done.get(); c = g.getAsInt()) {
double v = (double) c / req;
if (studioProgressConsumer != null) {
reportStudioProgress(0.40D + (0.42D * v), "Generating spawn");
J.sleep(16);
} else if (sender.isPlayer()) {
sender.sendProgress(v, "Generating");
J.sleep(16);
} else {
sender.sendMessage(C.WHITE + "Generating " + Form.pc(v) + ((C.GRAY + " (" + (req - c) + " Left)")));
J.sleep(1000);
}
}
}
});
AtomicInteger createProgressTask = startCreateProgressReporter(access, done);
World world;
reportStudioProgress(0.46D, "Creating world");
reportStudioProgress(0.46D, "create_world");
try {
world = J.sfut(() -> INMS.get().createWorldAsync(wc))
WorldLifecycleCaller callerKind = benchmark ? WorldLifecycleCaller.BENCHMARK : studio() ? WorldLifecycleCaller.STUDIO : WorldLifecycleCaller.CREATE;
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(wc, studio(), benchmark, callerKind);
world = J.sfut(() -> INMS.get().createWorldAsync(wc, request))
.thenCompose(Function.identity())
.get();
} catch (Throwable e) {
done.set(true);
cancelRepeatingTask(createProgressTask);
if (J.isFolia() && containsCreateWorldUnsupportedOperation(e)) {
if (FoliaWorldsLink.get().isActive()) {
throw new IrisException("Runtime world creation is blocked and async Folia runtime world-loader creation also failed.", e);
}
throw new IrisException("Runtime world creation is blocked and no async Folia runtime world-loader path is active.", e);
throw new IrisException("Runtime world creation is blocked and the selected world lifecycle backend could not create the world.", e);
}
throw new IrisException("Failed to create world!", e);
throw new IrisException("Failed to create world with backend family " + WorldLifecycleService.get().capabilities().serverFamily().id() + "!", e);
}
done.set(true);
reportStudioProgress(0.86D, "World created");
cancelRepeatingTask(createProgressTask);
reportStudioProgress(0.86D, "create_world");
if (sender.isPlayer() && !benchmark) {
Player senderPlayer = sender.player();
if (senderPlayer == null) {
Iris.warn("Studio opened, but sender player reference is unavailable for teleport.");
} else {
Location studioEntryLocation = resolveStudioEntryLocation(world);
if (studioEntryLocation == null) {
sender.sendMessage(C.YELLOW + "Studio opened, but entry location could not be resolved safely.");
} else {
prewarmStudioEntryChunks(world, studioEntryLocation, STUDIO_PREWARM_RADIUS_CHUNKS, STUDIO_PREWARM_TIMEOUT);
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(senderPlayer, studioEntryLocation);
if (teleportFuture != null) {
teleportFuture.thenAccept(success -> {
if (Boolean.TRUE.equals(success)) {
J.runEntity(senderPlayer, () -> Iris.service(BoardSVC.class).updatePlayer(senderPlayer));
}
});
teleportFuture.exceptionally(throwable -> {
Iris.warn("Failed to schedule studio teleport task for " + senderPlayer.getName() + ".");
Iris.reportError(throwable);
return false;
});
}
}
}
}
if (studio || benchmark) {
Runnable applyStudioWorldSettings = () -> {
Iris.linkMultiverseCore.removeFromConfig(world);
if (IrisSettings.get().getStudio().isDisableTimeAndWeather()) {
setBooleanGameRule(world, false, "ADVANCE_WEATHER", "DO_WEATHER_CYCLE", "WEATHER_CYCLE", "doWeatherCycle", "weatherCycle");
setBooleanGameRule(world, false, "ADVANCE_TIME", "DO_DAYLIGHT_CYCLE", "DAYLIGHT_CYCLE", "doDaylightCycle", "daylightCycle");
world.setTime(6000);
}
};
J.s(applyStudioWorldSettings);
} else {
if (!studio && !benchmark) {
addToBukkitYml();
J.s(() -> Iris.linkMultiverseCore.updateWorld(world, dimension));
}
reportStudioProgress(0.93D, "Applying world settings");
if (pregen != null) {
CompletableFuture<Boolean> ff = new CompletableFuture<>();
@@ -305,28 +266,18 @@ public class IrisCreator {
.onProgress(pp::set)
.whenDone(() -> ff.complete(true));
AtomicBoolean dx = new AtomicBoolean(false);
AtomicInteger pregenProgressTask = startPregenProgressReporter(pp, dx);
try {
AtomicBoolean dx = new AtomicBoolean(false);
J.a(() -> {
while (!dx.get()) {
if (sender.isPlayer()) {
sender.sendProgress(pp.get(), "Pregenerating");
J.sleep(16);
} else {
sender.sendMessage(C.WHITE + "Pregenerating " + Form.pc(pp.get()));
J.sleep(1000);
}
}
});
ff.get();
dx.set(true);
cancelRepeatingTask(pregenProgressTask);
} catch (Throwable e) {
dx.set(true);
cancelRepeatingTask(pregenProgressTask);
e.printStackTrace();
}
}
reportStudioProgress(0.98D, "Finalizing");
return world;
}
@@ -340,360 +291,89 @@ public class IrisCreator {
try {
consumer.accept(clamped, stage);
} catch (Throwable e) {
Iris.reportError(e);
Iris.reportError("Studio progress consumer failed for world \"" + name() + "\".", e);
}
}
private void prebakeNoisemapsBeforeWorldCreate(IrisDimension dimension) {
IrisSettings.IrisSettingsPregen pregenSettings = IrisSettings.get().getPregen();
if (!pregenSettings.isStartupNoisemapPrebake()) {
private AtomicInteger startCreateProgressReporter(PlatformChunkGenerator access, AtomicBoolean done) {
AtomicInteger taskId = new AtomicInteger(-1);
if (benchmark) {
return taskId;
}
IntSupplier generatedSupplier = () -> {
if (access.getEngine() == null) {
return 0;
}
return access.getEngine().getGenerated();
};
access.getSpawnChunks().whenComplete((required, throwable) -> {
if (throwable != null) {
Iris.reportError("Failed to resolve studio spawn chunk target for world \"" + name() + "\".", throwable);
return;
}
if (done.get() || required == null || required <= 0) {
return;
}
int interval = studioProgressConsumer != null || sender.isPlayer() ? 1 : 20;
taskId.set(J.ar(() -> {
if (done.get()) {
cancelRepeatingTask(taskId);
return;
}
int generated = generatedSupplier.getAsInt();
if (generated >= required) {
cancelRepeatingTask(taskId);
return;
}
double progress = (double) generated / required;
if (studioProgressConsumer != null) {
reportStudioProgress(0.40D + (0.42D * progress), "create_world");
return;
}
if (sender.isPlayer()) {
sender.sendProgress(progress, "Generating");
return;
}
sender.sendMessage(C.WHITE + "Generating " + Form.pc(progress) + ((C.GRAY + " (" + (required - generated) + " Left)")));
}, interval));
});
return taskId;
}
private AtomicInteger startPregenProgressReporter(AtomicDouble progress, AtomicBoolean done) {
AtomicInteger taskId = new AtomicInteger(-1);
int interval = sender.isPlayer() ? 1 : 20;
taskId.set(J.ar(() -> {
if (done.get()) {
cancelRepeatingTask(taskId);
return;
}
if (sender.isPlayer()) {
sender.sendProgress(progress.get(), "Pregenerating");
return;
}
sender.sendMessage(C.WHITE + "Pregenerating " + Form.pc(progress.get()));
}, interval));
return taskId;
}
private void cancelRepeatingTask(AtomicInteger taskId) {
if (taskId == null) {
return;
}
if (studio() && !benchmark) {
boolean startupPrebakeReady = IrisNoisemapPrebakePipeline.awaitInstalledPacksPrebakeForStudio();
if (startupPrebakeReady) {
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();
if (spawnLocation != null) {
return spawnLocation.clone();
}
int x = 0;
int z = 0;
int y = Math.max(world.getMinHeight() + 1, 96);
return new Location(world, x + 0.5D, y, z + 0.5D);
});
if (locationFuture == null) {
Iris.warn("Failed to schedule studio entry-location resolve task on the global scheduler for world \"" + world.getName() + "\".");
return null;
}
try {
Location rawLocation = locationFuture.get(15, TimeUnit.SECONDS);
return resolveTopSafeStudioLocation(world, rawLocation);
} catch (Throwable e) {
Iris.warn("Failed to resolve studio entry location for world \"" + world.getName() + "\".");
Iris.reportError(e);
return null;
}
}
private Location resolveTopSafeStudioLocation(World world, Location rawLocation) {
if (world == null || rawLocation == null) {
return rawLocation;
}
int chunkX = rawLocation.getBlockX() >> 4;
int chunkZ = rawLocation.getBlockZ() >> 4;
try {
CompletableFuture<Chunk> chunkFuture = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, false);
if (chunkFuture != null) {
chunkFuture.get(10, TimeUnit.SECONDS);
}
} catch (Throwable ignored) {
return rawLocation;
}
if (!world.isChunkLoaded(chunkX, chunkZ)) {
return rawLocation;
}
CompletableFuture<Location> regionFuture = new CompletableFuture<>();
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
regionFuture.complete(findTopSafeStudioLocation(world, rawLocation));
} catch (Throwable e) {
regionFuture.completeExceptionally(e);
}
});
if (!scheduled) {
return rawLocation;
}
try {
Location resolved = regionFuture.get(15, TimeUnit.SECONDS);
return resolved == null ? rawLocation : resolved;
} catch (Throwable e) {
Iris.warn("Failed to resolve safe studio entry surface for world \"" + world.getName() + "\".");
Iris.reportError(e);
return rawLocation;
}
}
private Location findTopSafeStudioLocation(World world, Location source) {
int x = source.getBlockX();
int z = source.getBlockZ();
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
int sourceY = source.getBlockY();
int startY = Math.max(minY, Math.min(maxY, sourceY));
float yaw = source.getYaw();
float pitch = source.getPitch();
int upperBound = Math.min(maxY, startY + 32);
for (int y = startY; y <= upperBound; y++) {
if (isSafeStandingLocation(world, x, y, z)) {
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
}
}
int lowerBound = Math.max(minY, startY - 64);
for (int y = startY - 1; y >= lowerBound; y--) {
if (isSafeStandingLocation(world, x, y, z)) {
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
}
}
int fallbackY = Math.max(minY, Math.min(maxY, source.getBlockY()));
return new Location(world, x + 0.5D, fallbackY, z + 0.5D, yaw, pitch);
}
private boolean isSafeStandingLocation(World world, int x, int y, int z) {
if (y <= world.getMinHeight() || y >= world.getMaxHeight() - 1) {
return false;
}
Block below = world.getBlockAt(x, y - 1, z);
Block feet = world.getBlockAt(x, y, z);
Block head = world.getBlockAt(x, y + 1, z);
Material belowType = below.getType();
if (!belowType.isSolid()) {
return false;
}
if (Tag.LEAVES.isTagged(belowType)) {
return false;
}
if (belowType == Material.LAVA
|| belowType == Material.MAGMA_BLOCK
|| belowType == Material.FIRE
|| belowType == Material.SOUL_FIRE
|| belowType == Material.CAMPFIRE
|| belowType == Material.SOUL_CAMPFIRE) {
return false;
}
if (feet.getType().isSolid() || head.getType().isSolid()) {
return false;
}
if (feet.isLiquid() || head.isLiquid()) {
return false;
}
return true;
}
private void prewarmStudioEntryChunks(World world, Location entry, int radiusChunks, Duration timeout) throws IrisException {
if (world == null || entry == null) {
throw new IrisException("Studio prewarm failed: world or entry location is null.");
}
int centerChunkX = entry.getBlockX() >> 4;
int centerChunkZ = entry.getBlockZ() >> 4;
List<StudioChunkCoordinate> chunkTargets = resolveStudioPrewarmTargets(centerChunkX, centerChunkZ, radiusChunks);
if (chunkTargets.isEmpty()) {
throw new IrisException("Studio prewarm failed: no target chunks were resolved.");
}
int loadedBefore = 0;
Map<StudioChunkCoordinate, CompletableFuture<Chunk>> futures = new LinkedHashMap<>();
for (StudioChunkCoordinate coordinate : chunkTargets) {
if (world.isChunkLoaded(coordinate.getX(), coordinate.getZ())) {
loadedBefore++;
}
CompletableFuture<Chunk> chunkFuture = PaperLib.getChunkAtAsync(world, coordinate.getX(), coordinate.getZ(), true);
if (chunkFuture == null) {
throw new IrisException("Studio prewarm failed: async chunk future was null for " + coordinate + ".");
}
futures.put(coordinate, chunkFuture);
}
int total = chunkTargets.size();
int completed = 0;
Set<StudioChunkCoordinate> remaining = new LinkedHashSet<>(chunkTargets);
long startNanos = System.nanoTime();
long timeoutNanos = Math.max(1L, timeout.toNanos());
reportStudioProgress(0.88D, "Prewarming entry chunks (0/" + total + ")");
while (!remaining.isEmpty()) {
long elapsedNanos = System.nanoTime() - startNanos;
if (elapsedNanos >= timeoutNanos) {
StudioPrewarmDiagnostics diagnostics = buildStudioPrewarmDiagnostics(world, chunkTargets, remaining, loadedBefore, elapsedNanos);
throw new IrisException("Studio prewarm timed out: " + diagnostics.toMessage());
}
boolean progressed = false;
List<StudioChunkCoordinate> completedCoordinates = new ArrayList<>();
for (StudioChunkCoordinate coordinate : remaining) {
CompletableFuture<Chunk> chunkFuture = futures.get(coordinate);
if (chunkFuture == null || !chunkFuture.isDone()) {
continue;
}
try {
Chunk loadedChunk = chunkFuture.get();
if (loadedChunk == null) {
throw new IrisException("Studio prewarm failed: chunk " + coordinate + " resolved to null.");
}
} catch (IrisException e) {
throw e;
} catch (Throwable e) {
throw new IrisException("Studio prewarm failed while loading chunk " + coordinate + ".", e);
}
completedCoordinates.add(coordinate);
progressed = true;
}
if (!completedCoordinates.isEmpty()) {
for (StudioChunkCoordinate completedCoordinate : completedCoordinates) {
remaining.remove(completedCoordinate);
}
completed += completedCoordinates.size();
double ratio = (double) completed / (double) total;
reportStudioProgress(0.88D + (0.04D * ratio), "Prewarming entry chunks (" + completed + "/" + total + ")");
}
if (!progressed) {
J.sleep(20);
}
}
long elapsedNanos = System.nanoTime() - startNanos;
StudioPrewarmDiagnostics diagnostics = buildStudioPrewarmDiagnostics(world, chunkTargets, new LinkedHashSet<>(), loadedBefore, elapsedNanos);
Iris.info("Studio prewarm complete: " + diagnostics.toMessage());
}
private StudioPrewarmDiagnostics buildStudioPrewarmDiagnostics(
World world,
List<StudioChunkCoordinate> chunkTargets,
Set<StudioChunkCoordinate> timedOutChunks,
int loadedBefore,
long elapsedNanos
) {
int loadedAfter = 0;
for (StudioChunkCoordinate coordinate : chunkTargets) {
if (world.isChunkLoaded(coordinate.getX(), coordinate.getZ())) {
loadedAfter++;
}
}
int generatedDuring = Math.max(0, loadedAfter - loadedBefore);
List<String> timedOut = new ArrayList<>();
for (StudioChunkCoordinate timedOutChunk : timedOutChunks) {
timedOut.add(timedOutChunk.toString());
}
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(Math.max(0L, elapsedNanos));
return new StudioPrewarmDiagnostics(elapsedMs, loadedBefore, loadedAfter, generatedDuring, timedOut);
}
private List<StudioChunkCoordinate> resolveStudioPrewarmTargets(int centerChunkX, int centerChunkZ, int radiusChunks) {
int safeRadius = Math.max(0, radiusChunks);
List<StudioChunkCoordinate> targets = new ArrayList<>();
targets.add(new StudioChunkCoordinate(centerChunkX, centerChunkZ));
for (int x = -safeRadius; x <= safeRadius; x++) {
for (int z = -safeRadius; z <= safeRadius; z++) {
if (x == 0 && z == 0) {
continue;
}
targets.add(new StudioChunkCoordinate(centerChunkX + x, centerChunkZ + z));
}
}
return targets;
}
private static final class StudioChunkCoordinate {
private final int x;
private final int z;
private StudioChunkCoordinate(int x, int z) {
this.x = x;
this.z = z;
}
private int getX() {
return x;
}
private int getZ() {
return z;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof StudioChunkCoordinate coordinate)) {
return false;
}
return x == coordinate.x && z == coordinate.z;
}
@Override
public int hashCode() {
return 31 * x + z;
}
@Override
public String toString() {
return x + "," + z;
}
}
private static final class StudioPrewarmDiagnostics {
private final long elapsedMs;
private final int loadedBefore;
private final int loadedAfter;
private final int generatedDuring;
private final List<String> timedOutChunks;
private StudioPrewarmDiagnostics(long elapsedMs, int loadedBefore, int loadedAfter, int generatedDuring, List<String> timedOutChunks) {
this.elapsedMs = elapsedMs;
this.loadedBefore = loadedBefore;
this.loadedAfter = loadedAfter;
this.generatedDuring = generatedDuring;
this.timedOutChunks = new ArrayList<>(timedOutChunks);
}
private String toMessage() {
return "elapsedMs=" + elapsedMs
+ ", loadedBefore=" + loadedBefore
+ ", loadedAfter=" + loadedAfter
+ ", generatedDuring=" + generatedDuring
+ ", timedOut=" + timedOutChunks;
int id = taskId.getAndSet(-1);
if (id >= 0) {
J.car(id);
}
}
@@ -713,138 +393,6 @@ public class IrisCreator {
return false;
}
@SuppressWarnings("unchecked")
private static void setBooleanGameRule(World world, boolean value, String... names) {
GameRule<Boolean> gameRule = resolveBooleanGameRule(world, names);
if (gameRule != null) {
world.setGameRule(gameRule, value);
}
}
@SuppressWarnings("unchecked")
private static GameRule<Boolean> resolveBooleanGameRule(World world, String... names) {
if (world == null || names == null || names.length == 0) {
return null;
}
Set<String> candidates = buildRuleNameCandidates(names);
for (String name : candidates) {
if (name == null || name.isBlank()) {
continue;
}
try {
Field field = GameRule.class.getField(name);
Object value = field.get(null);
if (value instanceof GameRule<?> gameRule && Boolean.class.equals(gameRule.getType())) {
return (GameRule<Boolean>) gameRule;
}
} catch (Throwable ignored) {
}
try {
GameRule<?> byName = GameRule.getByName(name);
if (byName != null && Boolean.class.equals(byName.getType())) {
return (GameRule<Boolean>) byName;
}
} catch (Throwable ignored) {
}
}
String[] availableRules = world.getGameRules();
if (availableRules == null || availableRules.length == 0) {
return null;
}
Set<String> normalizedCandidates = new LinkedHashSet<>();
for (String candidate : candidates) {
if (candidate != null && !candidate.isBlank()) {
normalizedCandidates.add(normalizeRuleName(candidate));
}
}
for (String availableRule : availableRules) {
String normalizedAvailable = normalizeRuleName(availableRule);
if (!normalizedCandidates.contains(normalizedAvailable)) {
continue;
}
try {
GameRule<?> byName = GameRule.getByName(availableRule);
if (byName != null && Boolean.class.equals(byName.getType())) {
return (GameRule<Boolean>) byName;
}
} catch (Throwable ignored) {
}
}
return null;
}
private static Set<String> buildRuleNameCandidates(String... names) {
Set<String> candidates = new LinkedHashSet<>();
for (String name : names) {
if (name == null || name.isBlank()) {
continue;
}
candidates.add(name);
candidates.add(name.toLowerCase(Locale.ROOT));
String lowerCamel = toLowerCamel(name);
if (!lowerCamel.isEmpty()) {
candidates.add(lowerCamel);
}
}
return candidates;
}
private static String toLowerCamel(String name) {
if (name == null) {
return "";
}
String raw = name.trim();
if (raw.isEmpty()) {
return "";
}
String[] parts = raw.split("_+");
if (parts.length == 0) {
return raw;
}
StringBuilder builder = new StringBuilder();
builder.append(parts[0].toLowerCase(Locale.ROOT));
for (int i = 1; i < parts.length; i++) {
String part = parts[i].toLowerCase(Locale.ROOT);
if (part.isEmpty()) {
continue;
}
builder.append(Character.toUpperCase(part.charAt(0)));
if (part.length() > 1) {
builder.append(part.substring(1));
}
}
return builder.toString();
}
private static String normalizeRuleName(String name) {
if (name == null || name.isBlank()) {
return "";
}
StringBuilder builder = new StringBuilder(name.length());
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (Character.isLetterOrDigit(c)) {
builder.append(Character.toLowerCase(c));
}
}
return builder.toString();
}
private void addToBukkitYml() {
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
String gen = "Iris:" + dimension;

View File

@@ -2,7 +2,7 @@ package art.arcane.iris.core.tools;
import art.arcane.iris.Iris;
import art.arcane.iris.core.link.FoliaWorldsLink;
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
@@ -100,10 +100,10 @@ public class IrisPackBenchmarking {
}
J.s(() -> {
var world = Bukkit.getWorld("benchmark");
org.bukkit.World world = Bukkit.getWorld("benchmark");
if (world == null) return;
IrisToolbelt.evacuate(world);
FoliaWorldsLink.get().unloadWorld(world, true);
WorldLifecycleService.get().unload(world, true);
});
stopwatch.end();

View File

@@ -90,9 +90,10 @@ 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 final GenerationSessionManager generationSessions;
private final AtomicBoolean closing;
private CompletableFuture<Long> hash32;
private EngineMode mode;
private EngineEffects effects;
@@ -113,6 +114,8 @@ public class IrisEngine implements Engine {
getEngineData();
verifySeed();
this.seedManager = new SeedManager(target.getWorld().getRawWorldSeed());
this.generationSessions = new GenerationSessionManager();
this.closing = new AtomicBoolean(false);
bud = new AtomicInteger(0);
buds = new AtomicInteger(0);
metrics = new EngineMetrics(32);
@@ -127,7 +130,6 @@ public class IrisEngine implements Engine {
mantle = new IrisEngineMantle(this);
context = new IrisContext(this);
cleaning = new AtomicBoolean(false);
noisemapPrebakeRunning = new AtomicBoolean(false);
modeFallbackLogged = new AtomicBoolean(false);
if (studio) {
getData().dump();
@@ -164,6 +166,13 @@ public class IrisEngine implements Engine {
}
private void prehotload() {
closing.set(true);
try {
generationSessions.sealAndAwait("hotload", 15000L);
} catch (GenerationSessionException e) {
throw new IllegalStateException(e);
}
EngineWorldManager currentWorldManager = worldManager;
worldManager = null;
if (currentWorldManager != null) {
@@ -193,6 +202,8 @@ public class IrisEngine implements Engine {
private void setupEngine() {
try {
generationSessions.activateNextSession();
closing.set(false);
Iris.debug("Setup Engine " + getCacheID());
cacheId = RNG.r.nextInt();
complex = ensureComplex();
@@ -215,7 +226,6 @@ 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();
@@ -297,30 +307,6 @@ public class IrisEngine implements Engine {
}
}
private void scheduleStartupNoisemapPrebake() {
if (!IrisSettings.get().getPregen().isStartupNoisemapPrebake()) {
return;
}
if (studio) {
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);
@@ -532,8 +518,14 @@ public class IrisEngine implements Engine {
@Override
public void close() {
PregeneratorJob.shutdownInstance();
closing.set(true);
closed = true;
J.car(art);
try {
generationSessions.sealAndAwait("close", 15000L, true);
} catch (GenerationSessionException e) {
throw new IllegalStateException(e);
}
EngineWorldManager currentWorldManager = getWorldManager();
if (currentWorldManager != null) {
currentWorldManager.close();
@@ -574,6 +566,10 @@ public class IrisEngine implements Engine {
return closed;
}
public boolean isClosing() {
return closing.get();
}
@Override
public void recycle() {
if (!cleanLatch.flip()) {
@@ -602,13 +598,14 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public void generate(int x, int z, Hunk<BlockData> vblocks, Hunk<Biome> vbiomes, boolean multicore) throws WrongEngineBroException {
if (closed) {
throw new WrongEngineBroException();
if (closing.get() || closed) {
throw new GenerationSessionException("Generation session is closed for world \"" + getWorld().name() + "\".", true);
}
context.touch();
getEngineData().getStatistics().generatedChunk();
try {
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate")) {
context.touch();
context.setGenerationSessionId(lease.sessionId());
getEngineData().getStatistics().generatedChunk();
PrecisionStopwatch p = PrecisionStopwatch.start();
Hunk<BlockData> blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t));
@@ -634,12 +631,19 @@ public class IrisEngine implements Engine {
if (generated.get() == 661) {
J.a(() -> getData().savePrefetch(this));
}
} catch (GenerationSessionException e) {
throw e;
} catch (Throwable e) {
Iris.reportError(e);
fail("Failed to generate " + x + ", " + z, e);
}
}
@Override
public GenerationSessionManager getGenerationSessions() {
return generationSessions;
}
@Override
public void saveEngineData() {
//TODO: Method this file

View File

@@ -111,6 +111,10 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
void close();
default boolean isClosing() {
return isClosed();
}
IrisContext getContext();
double getMaxBiomeObjectDensity();
@@ -121,6 +125,24 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
boolean isClosed();
default GenerationSessionManager getGenerationSessions() {
return null;
}
default GenerationSessionLease acquireGenerationLease(String operation) throws GenerationSessionException {
GenerationSessionManager generationSessions = getGenerationSessions();
if (generationSessions == null) {
return GenerationSessionLease.noop();
}
return generationSessions.acquire(operation);
}
default long getGenerationSessionId() {
GenerationSessionManager generationSessions = getGenerationSessions();
return generationSessions == null ? 0L : generationSessions.currentSessionId();
}
EngineWorldManager getWorldManager();
default UUID getBiomeID(int x, int z) {

View File

@@ -71,6 +71,7 @@ public interface EngineMode extends Staged {
@BlockCoordinates
default void generate(int x, int z, Hunk<BlockData> blocks, Hunk<Biome> biomes, boolean multicore) {
IrisContext context = IrisContext.getOr(getEngine());
boolean cacheContext = true;
if (J.isFolia()) {
org.bukkit.World world = getEngine().getWorld().realWorld();
@@ -79,8 +80,8 @@ public interface EngineMode extends Staged {
}
}
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
ChunkContext ctx = new ChunkContext(x, z, getComplex(), cacheContext, prefillPlan, getEngine().getMetrics());
IrisContext.getOr(getEngine()).setChunkContext(ctx);
ChunkContext ctx = new ChunkContext(x, z, getComplex(), context.getGenerationSessionId(), cacheContext, prefillPlan, getEngine().getMetrics());
context.setChunkContext(ctx);
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
for (EngineStage i : stages) {

View File

@@ -0,0 +1,18 @@
package art.arcane.iris.engine.framework;
public class GenerationSessionException extends WrongEngineBroException {
private final boolean expectedTeardown;
public GenerationSessionException(String message) {
this(message, false);
}
public GenerationSessionException(String message, boolean expectedTeardown) {
super(message);
this.expectedTeardown = expectedTeardown;
}
public boolean isExpectedTeardown() {
return expectedTeardown;
}
}

View File

@@ -0,0 +1,35 @@
package art.arcane.iris.engine.framework;
public final class GenerationSessionLease implements AutoCloseable {
private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L);
private final GenerationSessionManager manager;
private final GenerationSessionManager.GenerationSessionState state;
private final long sessionId;
private boolean released;
GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state, long sessionId) {
this.manager = manager;
this.state = state;
this.sessionId = sessionId;
this.released = false;
}
public static GenerationSessionLease noop() {
return NOOP;
}
public long sessionId() {
return sessionId;
}
@Override
public void close() {
if (released || state == null) {
return;
}
released = true;
manager.releaseLease(state);
}
}

View File

@@ -0,0 +1,116 @@
package art.arcane.iris.engine.framework;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
public final class GenerationSessionManager {
private final AtomicLong sessionSequence;
private final AtomicReference<GenerationSessionState> current;
private final Object drainMonitor;
public GenerationSessionManager() {
this.sessionSequence = new AtomicLong(0L);
this.current = new AtomicReference<>(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
this.drainMonitor = new Object();
}
public GenerationSessionLease acquire(String operation) throws GenerationSessionException {
while (true) {
GenerationSessionState state = current.get();
if (state == null || !state.accepting().get()) {
throw rejected(operation, state == null ? null : state);
}
state.activeLeases().incrementAndGet();
if (state != current.get()) {
state.activeLeases().decrementAndGet();
continue;
}
if (!state.accepting().get()) {
releaseLease(state);
throw rejected(operation, state);
}
return new GenerationSessionLease(this, state, state.sessionId());
}
}
public long currentSessionId() {
GenerationSessionState state = current.get();
return state == null ? 0L : state.sessionId();
}
public int activeLeases() {
GenerationSessionState state = current.get();
return state == null ? 0 : state.activeLeases().get();
}
public void sealAndAwait(String reason, long timeoutMs) throws GenerationSessionException {
sealAndAwait(reason, timeoutMs, false);
}
public void sealAndAwait(String reason, long timeoutMs, boolean teardown) throws GenerationSessionException {
GenerationSessionState state = current.get();
if (state == null) {
return;
}
state.accepting().set(false);
state.teardown().set(teardown);
state.sealReason().set(reason);
long deadline = System.currentTimeMillis() + Math.max(0L, timeoutMs);
synchronized (drainMonitor) {
while (state.activeLeases().get() > 0) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0L) {
break;
}
try {
drainMonitor.wait(remaining);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new GenerationSessionException("Generation session " + state.sessionId() + " was interrupted while draining for " + reason + ".", teardown);
}
}
}
if (state.activeLeases().get() > 0) {
throw new GenerationSessionException("Generation session " + state.sessionId() + " failed to drain for " + reason + " after " + timeoutMs + "ms. Active leases=" + state.activeLeases().get() + ".", teardown);
}
}
public void activateNextSession() {
current.set(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
}
private long nextSessionId() {
return sessionSequence.incrementAndGet();
}
void releaseLease(GenerationSessionState state) {
int remaining = state.activeLeases().decrementAndGet();
if (remaining <= 0) {
synchronized (drainMonitor) {
drainMonitor.notifyAll();
}
}
}
private GenerationSessionException rejected(String operation, GenerationSessionState state) {
long sessionId = state == null ? currentSessionId() : state.sessionId();
boolean teardown = state != null && state.teardown().get();
String reason = state == null ? null : state.sealReason().get();
if (teardown && reason != null && !reason.isBlank()) {
return new GenerationSessionException("Generation session " + sessionId + " rejected new work for " + operation + " during " + reason + ".", true);
}
return new GenerationSessionException("Generation session " + sessionId + " rejected new work for " + operation + ".", teardown);
}
record GenerationSessionState(long sessionId, AtomicBoolean accepting, AtomicInteger activeLeases, AtomicBoolean teardown, AtomicReference<String> sealReason) {
}
}

View File

@@ -26,6 +26,7 @@ import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.common.format.C;
import art.arcane.volmlib.util.format.Form;
@@ -90,7 +91,12 @@ public interface Locator<T> {
static Locator<IrisBiome> caveOrMantleBiome(String loadKey) {
return (e, c) -> {
AtomicBoolean found = new AtomicBoolean(false);
e.generateMatter(c.getX(), c.getZ(), true, new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), false));
try (GenerationSessionLease lease = e.acquireGenerationLease("locator_generate_matter")) {
IrisContext.getOr(e).setGenerationSessionId(lease.sessionId());
e.generateMatter(c.getX(), c.getZ(), true, new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), lease.sessionId(), false, ChunkContext.PrefillPlan.NONE, null));
} catch (GenerationSessionException sessionException) {
throw new IllegalStateException(sessionException);
}
e.getMantle().getMantle().iterateChunk(c.getX(), c.getZ(), MatterCavern.class, (x, y, z, t) -> {
if (found.get()) {
return;

View File

@@ -19,4 +19,11 @@
package art.arcane.iris.engine.framework;
public class WrongEngineBroException extends Exception {
public WrongEngineBroException() {
super();
}
public WrongEngineBroException(String message) {
super(message);
}
}

View File

@@ -18,6 +18,7 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
@@ -81,12 +82,13 @@ public class MantleCarvingComponent extends IrisMantleComponent {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(FIELD_SIZE * FIELD_SIZE);
BlendScratch blendScratch = BLEND_SCRATCH.get();
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, resolverState, caveBiomeCache);
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState, caveBiomeCache);
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
for (WeightedProfile weightedProfile : weightedProfiles) {
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights);
@@ -99,7 +101,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights);
}
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
BlendScratch blendScratch = BLEND_SCRATCH.get();
IrisCaveProfile[] profileField = blendScratch.profileField;
Map<IrisCaveProfile, double[]> tileProfileWeights = blendScratch.tileProfileWeights;
@@ -107,7 +109,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles;
double[] kernelProfileWeights = blendScratch.kernelProfileWeights;
activeProfiles.clear();
fillProfileField(profileField, chunkX, chunkZ, resolverState, caveBiomeCache);
fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, caveBiomeCache);
for (int tileX = 0; tileX < TILE_COUNT; tileX++) {
for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) {
@@ -313,7 +315,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return (tileX * TILE_COUNT) + tileZ;
}
private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
int startX = (chunkX << 4) - BLEND_RADIUS;
int startZ = (chunkZ << 4) - BLEND_RADIUS;
@@ -321,7 +323,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
int worldZ = startZ + fieldZ;
profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, resolverState, caveBiomeCache);
profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, complex, resolverState, caveBiomeCache);
}
}
}
@@ -336,14 +338,14 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return -1;
}
private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
IrisCaveProfile resolved = null;
IrisCaveProfile dimensionProfile = getDimension().getCaveProfile();
if (isProfileEnabled(dimensionProfile)) {
resolved = dimensionProfile;
}
IrisRegion region = getComplex().getRegionStream().get(worldX, worldZ);
IrisRegion region = complex.getRegionStream().get(worldX, worldZ);
if (region != null) {
IrisCaveProfile regionProfile = region.getCaveProfile();
if (isProfileEnabled(regionProfile)) {
@@ -351,7 +353,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
}
}
IrisBiome surfaceBiome = getComplex().getTrueBiomeStream().get(worldX, worldZ);
IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(worldX, worldZ);
if (surfaceBiome != null) {
IrisCaveProfile surfaceProfile = surfaceBiome.getCaveProfile();
if (isProfileEnabled(surfaceProfile)) {

View File

@@ -18,6 +18,7 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
@@ -39,11 +40,12 @@ public class MantleFluidBodyComponent extends IrisMantleComponent {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
RNG rng = new RNG(Cache.key(x, z) + seed() + 405666);
int xxx = 8 + (x << 4);
int zzz = 8 + (z << 4);
IrisRegion region = getComplex().getRegionStream().get(xxx, zzz);
IrisBiome biome = getComplex().getTrueBiomeStream().get(xxx, zzz);
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
IrisBiome biome = complex.getTrueBiomeStream().get(xxx, zzz);
generate(writer, rng, x, z, region, biome);
}

View File

@@ -20,8 +20,8 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.Iris;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.mantle.ComponentFlag;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.IrisMantleComponent;
@@ -40,8 +40,6 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.noise.NoiseType;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.util.BlockVector;
import java.io.IOException;
@@ -64,12 +62,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
IrisComplex complex = context.getComplex();
boolean traceRegen = isRegenTraceThread();
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
int xxx = 8 + (x << 4);
int zzz = 8 + (z << 4);
IrisRegion region = getComplex().getRegionStream().get(xxx, zzz);
IrisBiome surfaceBiome = getComplex().getTrueBiomeStream().get(xxx, zzz);
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(xxx, zzz);
int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true);
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
if (traceRegen) {
@@ -82,7 +81,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
+ " regionSurfacePlacers=" + region.getSurfaceObjects().size()
+ " regionCavePlacers=" + region.getCarvingObjects().size());
}
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, traceRegen);
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen);
if (traceRegen) {
Iris.info("Regen object layer done: chunk=" + x + "," + z
+ " biomeSurfacePlacersChecked=" + summary.biomeSurfacePlacersChecked()
@@ -142,7 +141,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
@ChunkCoordinates
private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, boolean traceRegen) {
private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, IrisComplex complex, boolean traceRegen) {
int biomeSurfaceChecked = 0;
int biomeSurfaceTriggered = 0;
int biomeCaveChecked = 0;
@@ -175,7 +174,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (chance) {
biomeSurfaceTriggered++;
try {
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, biomeSurfaceExclusionDepth, traceRegen, x, z, "biome-surface");
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, biomeSurfaceExclusionDepth, complex, traceRegen, x, z, "biome-surface");
attempts += result.attempts();
placed += result.placed();
rejected += result.rejected();
@@ -209,7 +208,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (chance) {
biomeCaveTriggered++;
try {
ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, biomeCaveProfile, traceRegen, x, z, "biome-cave");
ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, biomeCaveProfile, complex, traceRegen, x, z, "biome-cave");
attempts += result.attempts();
placed += result.placed();
rejected += result.rejected();
@@ -240,7 +239,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (chance) {
regionSurfaceTriggered++;
try {
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, regionSurfaceExclusionDepth, traceRegen, x, z, "region-surface");
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, regionSurfaceExclusionDepth, complex, traceRegen, x, z, "region-surface");
attempts += result.attempts();
placed += result.placed();
rejected += result.rejected();
@@ -274,7 +273,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (chance) {
regionCaveTriggered++;
try {
ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, regionCaveProfile, traceRegen, x, z, "region-cave");
ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, regionCaveProfile, complex, traceRegen, x, z, "region-cave");
attempts += result.attempts();
placed += result.placed();
rejected += result.rejected();
@@ -316,6 +315,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int z,
IrisObjectPlacement objectPlacement,
int surfaceObjectExclusionDepth,
IrisComplex complex,
boolean traceRegen,
int chunkX,
int chunkZ,
@@ -330,7 +330,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (int i = 0; i < density; i++) {
attempts++;
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng));
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng));
if (v == null) {
nullObjects++;
if (traceRegen) {
@@ -398,6 +398,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int chunkZ,
IrisObjectPlacement objectPlacement,
IrisCaveProfile caveProfile,
IrisComplex complex,
boolean traceRegen,
int metricChunkX,
int metricChunkZ,
@@ -419,7 +420,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
for (int i = 0; i < density; i++) {
attempts++;
IrisObject object = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng));
IrisObject object = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng));
if (object == null) {
nullObjects++;
if (traceRegen) {
@@ -903,15 +904,15 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
protected int computeRadius() {
var dimension = getDimension();
IrisDimension dimension = getDimension();
AtomicInteger xg = new AtomicInteger();
AtomicInteger zg = new AtomicInteger();
KSet<String> objects = new KSet<>();
KMap<IrisObjectScale, KList<String>> scalars = new KMap<>();
for (var region : dimension.getAllRegions(this::getData)) {
for (var j : region.getObjects()) {
for (IrisRegion region : dimension.getAllRegions(this::getData)) {
for (IrisObjectPlacement j : region.getObjects()) {
if (j.getScale().canScaleBeyond()) {
scalars.put(j.getScale(), j.getPlace());
} else {
@@ -919,8 +920,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
}
for (var biome : dimension.getAllBiomes(this::getData)) {
for (var j : biome.getObjects()) {
for (IrisBiome biome : dimension.getAllBiomes(this::getData)) {
for (IrisObjectPlacement j : biome.getObjects()) {
if (j.getScale().canScaleBeyond()) {
scalars.put(j.getScale(), j.getPlace());
} else {
@@ -929,93 +930,59 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
BurstExecutor e = getEngineMantle().getTarget().getBurster().burst(objects.size());
boolean maintenanceFolia = false;
if (J.isFolia()) {
var world = getEngineMantle().getEngine().getWorld().realWorld();
maintenanceFolia = world != null && IrisToolbelt.isWorldMaintenanceActive(world);
}
if (maintenanceFolia) {
Iris.info("MantleObjectComponent radius scan using single-threaded mode during maintenance regen.");
e.setMulticore(false);
}
KMap<String, BlockVector> sizeCache = new KMap<>();
for (String i : objects) {
e.queue(() -> {
try {
BlockVector bv = sizeCache.computeIfAbsent(i, (k) -> {
try {
return IrisObject.sampleSize(getData().getObjectLoader().findFile(i));
} catch (IOException ex) {
Iris.reportError(ex);
ex.printStackTrace();
}
return null;
});
if (bv == null) {
throw new RuntimeException();
}
if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) {
Iris.warn("Object " + i + " has a large size (" + bv + ") and may increase memory usage!");
}
synchronized (xg) {
xg.getAndSet(Math.max(bv.getBlockX(), xg.get()));
}
synchronized (zg) {
zg.getAndSet(Math.max(bv.getBlockZ(), zg.get()));
}
} catch (Throwable ed) {
Iris.reportError(ed);
}
});
updateRadiusBounds(sizeCache, xg, zg, i, 1D);
}
for (Map.Entry<IrisObjectScale, KList<String>> entry : scalars.entrySet()) {
double ms = entry.getKey().getMaximumScale();
for (String j : entry.getValue()) {
e.queue(() -> {
try {
BlockVector bv = sizeCache.computeIfAbsent(j, (k) -> {
try {
return IrisObject.sampleSize(getData().getObjectLoader().findFile(j));
} catch (IOException ioException) {
Iris.reportError(ioException);
ioException.printStackTrace();
}
return null;
});
if (bv == null) {
throw new RuntimeException();
}
if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) {
Iris.warn("Object " + j + " has a large size (" + bv + ") and may increase memory usage! (Object scaled up to " + Form.pc(ms, 2) + ")");
}
synchronized (xg) {
xg.getAndSet((int) Math.max(Math.ceil(bv.getBlockX() * ms), xg.get()));
}
synchronized (zg) {
zg.getAndSet((int) Math.max(Math.ceil(bv.getBlockZ() * ms), zg.get()));
}
} catch (Throwable ee) {
Iris.reportError(ee);
}
});
updateRadiusBounds(sizeCache, xg, zg, j, ms);
}
}
e.complete();
return Math.max(xg.get(), zg.get());
}
private void updateRadiusBounds(
KMap<String, BlockVector> sizeCache,
AtomicInteger xg,
AtomicInteger zg,
String objectKey,
double scale
) {
try {
BlockVector bv = loadObjectSize(sizeCache, objectKey);
if (bv == null) {
throw new RuntimeException();
}
if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) {
if (scale > 1D) {
Iris.warn("Object " + objectKey + " has a large size (" + bv + ") and may increase memory usage! (Object scaled up to " + Form.pc(scale, 2) + ")");
} else {
Iris.warn("Object " + objectKey + " has a large size (" + bv + ") and may increase memory usage!");
}
}
xg.getAndSet(Math.max((int) Math.ceil(bv.getBlockX() * scale), xg.get()));
zg.getAndSet(Math.max((int) Math.ceil(bv.getBlockZ() * scale), zg.get()));
} catch (Throwable e) {
Iris.reportError(e);
}
}
private BlockVector loadObjectSize(KMap<String, BlockVector> sizeCache, String objectKey) {
return sizeCache.computeIfAbsent(objectKey, k -> {
try {
return IrisObject.sampleSize(getData().getObjectLoader().findFile(objectKey));
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}
return null;
});
}
}

View File

@@ -0,0 +1,51 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.util.common.data.B;
import org.bukkit.block.data.BlockData;
import java.util.function.Function;
final class BlockDataMergeSupport {
private BlockDataMergeSupport() {
}
static BlockData merge(BlockData base, BlockData update) {
return merge(base, update, B::get);
}
static BlockData merge(BlockData base, BlockData update, Function<String, BlockData> resolver) {
try {
return base.merge(update);
} catch (IllegalArgumentException e) {
BlockData normalizedBase = resolve(base, resolver);
BlockData normalizedUpdate = resolve(update, resolver);
if (normalizedBase != null && normalizedUpdate != null) {
try {
return normalizedBase.merge(normalizedUpdate);
} catch (IllegalArgumentException ignored) {
return normalizedUpdate;
}
}
if (normalizedUpdate != null) {
return normalizedUpdate;
}
return update;
}
}
private static BlockData resolve(BlockData data, Function<String, BlockData> resolver) {
if (data == null || resolver == null) {
return null;
}
String serialized = data.getAsString(false);
if (serialized == null || serialized.isBlank()) {
return null;
}
return resolver.apply(serialized);
}
}

View File

@@ -564,33 +564,52 @@ public class IrisDimension extends IrisRegistrant {
}
}
public Dimension getBaseDimension() {
return switch (getEnvironment()) {
case NETHER -> Dimension.NETHER;
case THE_END -> Dimension.END;
default -> Dimension.OVERWORLD;
};
}
public String getDimensionTypeKey() {
return getDimensionType().key();
}
public IrisDimensionType getDimensionType() {
return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
}
public void installDimensionType(IDataFixer fixer, KList<File> folders) {
IrisDimensionType type = getDimensionType();
String json = type.toJson(fixer);
Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + type.key() + '"');
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/iris/dimension_type/" + type.key() + ".json");
output.getParentFile().mkdirs();
try {
IO.writeAll(output, json);
} catch (IOException e) {
public Dimension getBaseDimension() {
return switch (getEnvironment()) {
case NETHER -> Dimension.NETHER;
case THE_END -> Dimension.END;
default -> Dimension.OVERWORLD;
};
}
public String getDimensionTypeKey() {
return sanitizeDimensionTypeKeyValue(getLoadKey());
}
public static String sanitizeDimensionTypeKeyValue(String value) {
if (value == null || value.isBlank()) {
return "dimension";
}
String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/");
sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_");
sanitized = sanitized.replaceAll("/+", "/");
sanitized = sanitized.replaceAll("^/+", "");
sanitized = sanitized.replaceAll("/+$", "");
if (sanitized.contains("..")) {
sanitized = sanitized.replace("..", "_");
}
sanitized = sanitized.replace("/", "_");
return sanitized.isBlank() ? "dimension" : sanitized;
}
public IrisDimensionType getDimensionType() {
return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
}
public void installDimensionType(IDataFixer fixer, KList<File> folders) {
IrisDimensionType type = getDimensionType();
String json = type.toJson(fixer);
String dimensionTypeKey = getDimensionTypeKey();
Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + dimensionTypeKey + '"');
for (File datapacks : folders) {
File output = new File(datapacks, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json");
output.getParentFile().mkdirs();
try {
IO.writeAll(output, json);
} catch (IOException e) {
Iris.reportError(e);
e.printStackTrace();
}

View File

@@ -969,7 +969,7 @@ public class IrisObject extends IrisRegistrant {
BlockData newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata).clone();
if (newData.getMaterial() == data.getMaterial() && !(newData instanceof IrisCustomData || data instanceof IrisCustomData))
data = data.merge(newData);
data = BlockDataMergeSupport.merge(data, newData);
else
data = newData;
@@ -1093,7 +1093,7 @@ public class IrisObject extends IrisRegistrant {
BlockData newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata).clone();
if (newData.getMaterial() == d.getMaterial()) {
d = d.merge(newData);
d = BlockDataMergeSupport.merge(d, newData);
} else {
d = newData;
}

View File

@@ -30,6 +30,7 @@ import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.object.StudioMode;
@@ -93,10 +94,12 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private final AtomicInteger a = new AtomicInteger(0);
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
private final AtomicReference<CompletableFuture<Void>> closeFuture = new AtomicReference<>();
private volatile Engine engine;
private volatile Looper hotloader;
private volatile StudioMode lastMode;
private volatile DummyBiomeProvider dummyBiomeProvider;
private volatile boolean closing;
@Setter
private volatile StudioGenerator studioGenerator;
@@ -118,6 +121,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
new KList<>(".iris"),
new KList<>()
);
this.closing = false;
Bukkit.getServer().getPluginManager().registerEvents(this, Iris.instance);
}
@@ -142,6 +146,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
try {
INMS.get().inject(world.getSeed(), engine, world);
Iris.info("Injected Iris Biome Source into " + world.getName());
J.s(() -> updateSpawnLocation(world), 1);
} catch (Throwable e) {
Iris.reportError(e);
Iris.error("Failed to inject biome source into " + world.getName());
@@ -156,15 +161,65 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Nullable
@Override
public Location getFixedSpawnLocation(@NotNull World world, @NotNull Random random) {
Location location = new Location(world, 0, 64, 0);
PaperLib.getChunkAtAsync(location)
.thenAccept(c -> {
World w = c.getWorld();
if (!w.getSpawnLocation().equals(location))
return;
w.setSpawnLocation(location.add(0, w.getHighestBlockYAt(location) - 64, 0));
});
return location;
return getInitialSpawnLocation(world);
}
@Override
public Location getInitialSpawnLocation(World world) {
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
int y = Math.max(minY, Math.min(maxY, 96));
return new Location(world, 0.5D, y, 0.5D);
}
private void updateSpawnLocation(World world) {
Location initialSpawn = getInitialSpawnLocation(world);
int chunkX = initialSpawn.getBlockX() >> 4;
int chunkZ = initialSpawn.getBlockZ() >> 4;
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
if (chunkFuture == null) {
return;
}
chunkFuture.thenAccept(chunk ->
J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> applySpawnLocation(chunk.getWorld(), initialSpawn)));
}
private void applySpawnLocation(World world, Location initialSpawn) {
Location currentSpawn = world.getSpawnLocation();
if (currentSpawn == null) {
return;
}
if (!studio && (currentSpawn.getBlockX() != initialSpawn.getBlockX() || currentSpawn.getBlockZ() != initialSpawn.getBlockZ())) {
return;
}
int minY = world.getMinHeight() + 1;
int maxY = world.getMaxHeight() - 2;
int y = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn)));
world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch()));
}
@SuppressWarnings("unchecked")
private CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
try {
Object result = World.class
.getMethod("getChunkAtAsync", int.class, int.class, boolean.class)
.invoke(world, chunkX, chunkZ, generate);
if (result instanceof CompletableFuture<?>) {
return (CompletableFuture<Chunk>) result;
}
if (PaperLib.isPaper()) {
return CompletableFuture.failedFuture(new IllegalStateException("Paper World#getChunkAtAsync returned a non-future result."));
}
} catch (Throwable e) {
if (PaperLib.isPaper()) {
return CompletableFuture.failedFuture(new IllegalStateException("Paper World#getChunkAtAsync is unavailable.", e));
}
}
return PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate);
}
private void setupEngine() {
@@ -530,18 +585,48 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public void close() {
withExclusiveControl(() -> {
if (isStudio()) {
hotloader.interrupt();
closeAsync();
}
@Override
public CompletableFuture<Void> closeAsync() {
CompletableFuture<Void> existing = closeFuture.get();
if (existing != null && !existing.isDone()) {
return existing;
}
closing = true;
CompletableFuture<Void> future = withExclusiveControlFuture(() -> {
Looper activeHotloader = hotloader;
hotloader = null;
if (isStudio() && activeHotloader != null) {
activeHotloader.interrupt();
try {
activeHotloader.join(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Iris.reportError(e);
}
}
final Engine engine = getEngine();
if (engine != null && !engine.isClosed())
engine.close();
Engine currentEngine = engine;
if (currentEngine != null && !currentEngine.isClosed()) {
currentEngine.close();
}
folder.clear();
populators.clear();
});
if (!closeFuture.compareAndSet(existing, future)) {
CompletableFuture<Void> winningFuture = closeFuture.get();
return winningFuture == null ? future : winningFuture;
}
future.whenComplete((ignored, throwable) -> {
if (throwable != null) {
closeFuture.compareAndSet(future, null);
}
});
return future;
}
@Override
@@ -551,7 +636,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public void hotload() {
if (!isStudio()) {
if (!isStudio() || closing) {
return;
}
@@ -570,6 +655,22 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
});
}
public CompletableFuture<Void> withExclusiveControlFuture(Runnable r) {
CompletableFuture<Void> future = new CompletableFuture<>();
J.a(() -> {
try {
loadLock.acquire(LOAD_LOCKS);
r.run();
future.complete(null);
} catch (Throwable e) {
future.completeExceptionally(e);
} finally {
loadLock.release(LOAD_LOCKS);
}
});
return future;
}
@Override
public void touch(World world) {
getEngine(world);
@@ -577,6 +678,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) {
if (closing) {
return;
}
try {
Engine engine = getEngine(world);
computeStudioGenerator();
@@ -592,6 +697,21 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
Iris.debug("Generated " + x + " " + z);
} catch (GenerationSessionException e) {
if (closing || isExpectedTeardown(engine, e)) {
return;
}
Iris.error("======================================");
e.printStackTrace();
Iris.reportErrorChunk(x, z, e, "CHUNK");
Iris.error("======================================");
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
d.setBlock(i, 0, j, Material.RED_GLAZED_TERRACOTTA.createBlockData());
}
}
} catch (Throwable e) {
Iris.error("======================================");
e.printStackTrace();
@@ -606,6 +726,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
private boolean isExpectedTeardown(Engine currentEngine, Throwable throwable) {
if (throwable instanceof GenerationSessionException generationSessionException && generationSessionException.isExpectedTeardown()) {
return true;
}
if (currentEngine != null && currentEngine.isClosing()) {
return true;
}
World realWorld = this.world.realWorld();
return realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld);
}
@Override
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
Engine currentEngine = engine;

View File

@@ -23,6 +23,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.Hotloadable;
import art.arcane.iris.util.common.data.DataProvider;
import org.bukkit.Location;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -53,9 +54,21 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
void close();
default CompletableFuture<Void> closeAsync() {
close();
return CompletableFuture.completedFuture(null);
}
boolean isStudio();
default boolean isClosing() {
return false;
}
void touch(World world);
CompletableFuture<Integer> getSpawnChunks();
@Nullable
Location getInitialSpawnLocation(World world);
}

View File

@@ -431,6 +431,10 @@ public class VolmitSender implements CommandSender {
return m.removeDuplicates().convert((iff) -> iff.replaceAll("\\Q \\E", " ")).toString("\n");
}
static String escapeMiniMessageQuotedText(String text) {
return text.replace("\\", "\\\\").replace("'", "\\'");
}
public void sendHeader(String name, int overrideLength) {
int len = overrideLength;
int h = name.length() + 2;
@@ -469,7 +473,8 @@ public class VolmitSender implements CommandSender {
if (v.getNodes().isNotEmpty()) {
sendHeader(v.getPath() + (page > 0 ? (" {" + (page + 1) + "}") : ""));
if (isPlayer() && v.getParent() != null) {
sendMessageRaw("<hover:show_text:'" + "<#2b7a3f>Click to go back to <#32bfad>" + Form.capitalize(v.getParent().getName()) + " Help" + "'><click:run_command:" + v.getParent().getPath() + "><font:minecraft:uniform><#6fe98f>〈 Back</click></hover>");
String backHover = escapeMiniMessageQuotedText("<#2b7a3f>Click to go back to <#32bfad>" + Form.capitalize(v.getParent().getName()) + " Help");
sendMessageRaw("<hover:show_text:'" + backHover + "'><click:run_command:" + v.getParent().getPath() + "><font:minecraft:uniform><#6fe98f>〈 Back</click></hover>");
}
AtomicBoolean next = new AtomicBoolean(false);
@@ -481,13 +486,15 @@ public class VolmitSender implements CommandSender {
int l = 75 - (page > 0 ? 10 : 0) - (next.get() ? 10 : 0);
if (page > 0) {
s += "<hover:show_text:'<green>Click to go back to page " + page + "'><click:run_command:" + v.getPath() + " help=" + page + "><gradient:#34eb6b:#1f8f4d>〈 Page " + page + "</click></hover><reset> ";
String previousPageHover = escapeMiniMessageQuotedText("<green>Click to go back to page " + page);
s += "<hover:show_text:'" + previousPageHover + "'><click:run_command:" + v.getPath() + " help=" + page + "><gradient:#34eb6b:#1f8f4d>〈 Page " + page + "</click></hover><reset> ";
}
s += "<reset><font:minecraft:uniform><strikethrough><gradient:#32bfad:#34eb6b>" + Form.repeat(" ", l) + "<reset>";
if (next.get()) {
s += " <hover:show_text:'<green>Click to go to back to page " + (page + 2) + "'><click:run_command:" + v.getPath() + " help=" + (page + 2) + "><gradient:#1f8f4d:#34eb6b>Page " + (page + 2) + " ❭</click></hover>";
String nextPageHover = escapeMiniMessageQuotedText("<green>Click to go to back to page " + (page + 2));
s += " <hover:show_text:'" + nextPageHover + "'><click:run_command:" + v.getPath() + " help=" + (page + 2) + "><gradient:#1f8f4d:#34eb6b>Page " + (page + 2) + " ❭</click></hover>";
}
sendMessageRaw(s);
@@ -550,13 +557,11 @@ public class VolmitSender implements CommandSender {
nUsage = "<#3fbe6f>✔ <#9de5b6><font:minecraft:uniform>This parameter is optional.";
}
String type = "<#4fbf7f>✢ <#8ad9af><font:minecraft:uniform>This parameter is of type " + p.getType().getSimpleName() + ".";
String parameterHover = escapeMiniMessageQuotedText(nHoverTitle + newline + nDescription + newline + nUsage + newline + type);
nodes
.append("<hover:show_text:'")
.append(nHoverTitle).append(newline)
.append(nDescription).append(newline)
.append(nUsage).append(newline)
.append(type)
.append(parameterHover)
.append("'>")
.append(fullTitle)
.append("</hover>");
@@ -565,12 +570,16 @@ public class VolmitSender implements CommandSender {
nodes = new StringBuilder("<gradient:#b7eecb:#9de5b6> - Category of Commands");
}
return "<hover:show_text:'" +
String entryHover = escapeMiniMessageQuotedText(
hoverTitle + newline +
description + newline +
usage +
suggestion +
suggestions +
description + newline +
usage +
suggestion +
suggestions
);
return "<hover:show_text:'" +
entryHover +
"'>" +
"<click:" +
onClick +

View File

@@ -617,18 +617,38 @@ public class J {
}
private static boolean runAsyncImmediate(Runnable runnable) {
if (!isFolia()) {
if (!isPluginEnabled()) {
return false;
}
if (!isFolia()) {
try {
Bukkit.getScheduler().runTaskAsynchronously(Iris.instance, runnable);
return true;
} catch (Throwable e) {
Iris.reportError(e);
return false;
}
}
return FoliaScheduler.runAsync(Iris.instance, runnable);
}
private static boolean runAsyncDelayed(Runnable runnable, int delayTicks) {
if (!isFolia()) {
if (!isPluginEnabled()) {
return false;
}
if (!isFolia()) {
try {
Bukkit.getScheduler().runTaskLaterAsynchronously(Iris.instance, runnable, Math.max(0, delayTicks));
return true;
} catch (Throwable e) {
Iris.reportError(e);
return false;
}
}
return FoliaScheduler.runAsync(Iris.instance, runnable, Math.max(0, delayTicks));
}

View File

@@ -15,6 +15,8 @@ import java.util.concurrent.CompletableFuture;
public class ChunkContext {
private final int x;
private final int z;
private final IrisComplex complex;
private final long generationSessionId;
private final ChunkedDataCache<Double> height;
private final ChunkedDataCache<IrisBiome> biome;
private final ChunkedDataCache<IrisBiome> cave;
@@ -23,20 +25,26 @@ public class ChunkContext {
private final ChunkedDataCache<IrisRegion> region;
public ChunkContext(int x, int z, IrisComplex complex) {
this(x, z, complex, true, PrefillPlan.NO_CAVE, null);
this(x, z, complex, 0L, true, PrefillPlan.NO_CAVE, null);
}
public ChunkContext(int x, int z, IrisComplex complex, boolean cache) {
this(x, z, complex, cache, PrefillPlan.NO_CAVE, null);
this(x, z, complex, 0L, cache, PrefillPlan.NO_CAVE, null);
}
public ChunkContext(int x, int z, IrisComplex complex, boolean cache, EngineMetrics metrics) {
this(x, z, complex, cache, PrefillPlan.NO_CAVE, metrics);
this(x, z, complex, 0L, cache, PrefillPlan.NO_CAVE, metrics);
}
public ChunkContext(int x, int z, IrisComplex complex, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) {
this(x, z, complex, 0L, cache, prefillPlan, metrics);
}
public ChunkContext(int x, int z, IrisComplex complex, long generationSessionId, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) {
this.x = x;
this.z = z;
this.complex = complex;
this.generationSessionId = generationSessionId;
this.height = new ChunkedDataCache<>(complex.getHeightStream(), x, z, cache);
this.biome = new ChunkedDataCache<>(complex.getTrueBiomeStream(), x, z, cache);
this.cave = new ChunkedDataCache<>(complex.getCaveBiomeStream(), x, z, cache);
@@ -68,7 +76,7 @@ public class ChunkContext {
fillTasks.add(new PrefillFillTask(cave));
}
if (fillTasks.size() <= 1 || Iris.instance == null) {
if (!shouldPrefillAsync(fillTasks.size())) {
for (PrefillFillTask fillTask : fillTasks) {
fillTask.run();
}
@@ -88,6 +96,15 @@ public class ChunkContext {
}
}
static boolean shouldPrefillAsync(int fillTaskCount) {
if (fillTaskCount <= 1 || Iris.instance == null) {
return false;
}
String threadName = Thread.currentThread().getName();
return threadName != null && threadName.startsWith("Iris ");
}
public int getX() {
return x;
}
@@ -96,6 +113,10 @@ public class ChunkContext {
return z;
}
public IrisComplex getComplex() {
return complex;
}
public ChunkedDataCache<Double> getHeight() {
return height;
}

View File

@@ -32,6 +32,7 @@ public class IrisContext {
private static final ChronoLatch cl = new ChronoLatch(60000);
private final Engine engine;
private ChunkContext chunkContext;
private long generationSessionId;
public IrisContext(Engine engine) {
this.engine = engine;
@@ -100,6 +101,7 @@ public class IrisContext {
return new KMap<String, Object>()
.qput("studio", engine.isStudio())
.qput("closed", engine.isClosed())
.qput("generationSessionId", generationSessionId)
.qput("pack", new KMap<>()
.qput("key", dimension == null ? "" : dimension.getLoadKey())
.qput("version", dimension == null ? "" : dimension.getVersion())

View File

@@ -0,0 +1,74 @@
package art.arcane.iris;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisDiagnosticsTest {
@Test
public void reportErrorWithContextPrintsFullStacktrace() {
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
try {
Iris.reportError("Runtime world creation failed.", new IllegalStateException("outer", new IllegalArgumentException("inner")));
} finally {
System.setErr(originalErr);
}
String text = output.toString(StandardCharsets.UTF_8);
assertTrue(text.contains("Runtime world creation failed."));
assertTrue(text.contains("IllegalStateException"));
assertTrue(text.contains("IllegalArgumentException"));
assertTrue(text.contains("inner"));
}
@Test
public void collectSplashPacksSkipsInternalAndInvalidFolders() throws Exception {
Path root = Files.createTempDirectory("iris-splash");
try {
Path validPack = root.resolve("overworld");
Files.createDirectories(validPack.resolve("dimensions"));
Files.writeString(validPack.resolve("dimensions").resolve("overworld.json"), "{\"version\":\"4000\"}");
Files.createDirectories(root.resolve("datapack-imports"));
Path brokenPack = root.resolve("broken");
Files.createDirectories(brokenPack.resolve("dimensions"));
Files.writeString(brokenPack.resolve("dimensions").resolve("broken.json"), "{");
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
List<Iris.SplashPackMetadata> packs;
try {
packs = Iris.collectSplashPacks(root.toFile());
} finally {
System.setErr(originalErr);
}
assertEquals(1, packs.size());
assertEquals("overworld", packs.get(0).name());
assertEquals("4000", packs.get(0).version());
String text = output.toString(StandardCharsets.UTF_8);
assertTrue(text.contains("Failed to read splash metadata for dimension pack \"broken\"."));
assertTrue(text.contains("Json"));
} finally {
Files.walk(root)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
}

View File

@@ -37,6 +37,16 @@ public class IrisRuntimeSchedulerModeRoutingTest {
assertEquals(IrisRuntimeSchedulerMode.FOLIA, resolved);
}
@Test
public void autoResolvesToPaperLikeOnCanvasBranding() {
installServer("Canvas", "git-Canvas-101 (MC: 1.21.11)");
IrisSettings.IrisSettingsPregen pregen = new IrisSettings.IrisSettingsPregen();
pregen.runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO;
IrisRuntimeSchedulerMode resolved = IrisRuntimeSchedulerMode.resolve(pregen);
assertEquals(IrisRuntimeSchedulerMode.PAPER_LIKE, resolved);
}
@Test
public void explicitModeBypassesAutoDetection() {
installServer("Purpur", "git-Purpur-2562 (MC: 1.21.11)");

View File

@@ -0,0 +1,44 @@
package art.arcane.iris.core;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import org.junit.Test;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ServerConfiguratorDatapackFolderTest {
@Test
public void resolvesDimensionWorldFolderBackToRootDatapacks() {
File folder = new File("/tmp/server/world/dimensions/minecraft/overworld");
File datapacks = ServerConfigurator.resolveDatapacksFolder(folder);
assertEquals(new File("/tmp/server/world/datapacks").getAbsolutePath(), datapacks.getAbsolutePath());
}
@Test
public void keepsStandaloneWorldFolderDatapacksUnchanged() {
File folder = new File("/tmp/server/custom_world");
File datapacks = ServerConfigurator.resolveDatapacksFolder(folder);
assertEquals(new File("/tmp/server/custom_world/datapacks").getAbsolutePath(), datapacks.getAbsolutePath());
}
@Test
public void installFoldersIncludeExtraStudioWorldDatapackTargets() {
File baseFolder = new File("/tmp/server/world/datapacks");
File extraFolder = new File("/tmp/server/iris-studio/datapacks");
KList<File> baseFolders = new KList<>();
baseFolders.add(baseFolder);
KList<File> extraFolders = new KList<>();
extraFolders.add(extraFolder);
KMap<String, KList<File>> extrasByPack = new KMap<>();
extrasByPack.put("overworld", extraFolders);
KList<File> folders = ServerConfigurator.collectInstallDatapackFolders(baseFolders, extrasByPack);
assertEquals(2, folders.size());
assertTrue(folders.contains(baseFolder));
assertTrue(folders.contains(extraFolder));
}
}

View File

@@ -0,0 +1,47 @@
package art.arcane.iris.core;
import art.arcane.iris.core.commands.CommandStudio;
import art.arcane.iris.core.tools.IrisCreator;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.assertFalse;
public class StudioRuntimeCleanupTest {
@Test
public void pregenSettingsNoLongerExposeStartupNoisemapPrebake() {
boolean found = Arrays.stream(IrisSettings.IrisSettingsPregen.class.getDeclaredFields())
.anyMatch(field -> field.getName().equals("startupNoisemapPrebake"));
assertFalse(found);
}
@Test
public void studioCommandNoLongerExposesProfilecache() {
boolean found = Arrays.stream(CommandStudio.class.getDeclaredMethods())
.anyMatch(method -> method.getName().equals("profilecache"));
assertFalse(found);
}
@Test
public void studioCreatorNoLongerContainsPrewarmOrPrebakeHelpers() {
boolean found = Arrays.stream(IrisCreator.class.getDeclaredMethods())
.map(method -> method.getName().toLowerCase())
.anyMatch(name -> name.contains("prewarm") || name.contains("prebake"));
assertFalse(found);
}
@Test
public void noisemapPrebakePipelineClassIsRemoved() {
try {
Class.forName("art.arcane.iris.engine.IrisNoisemapPrebakePipeline");
} catch (ClassNotFoundException ignored) {
return;
}
throw new AssertionError("IrisNoisemapPrebakePipeline should not exist.");
}
}

View File

@@ -0,0 +1,164 @@
package art.arcane.iris.core.lifecycle;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
public class CapabilityResolutionTest {
@Test
public void resolvesExactCreateLevelMethod() throws Exception {
Method method = CapabilityResolution.resolveCreateLevelMethod(CurrentCreateLevelOwner.class);
assertEquals("createLevel", method.getName());
assertEquals(3, method.getParameterCount());
}
@Test
public void resolvesDeclaredLegacyCreateLevelMethod() throws Exception {
Method method = CapabilityResolution.resolveCreateLevelMethod(DeclaredLegacyCreateLevelOwner.class);
assertEquals("createLevel", method.getName());
assertEquals(4, method.getParameterCount());
}
@Test
public void resolvesTwoArgLevelStorageAccessMethod() throws Exception {
Method method = CapabilityResolution.resolveLevelStorageAccessMethod(TwoArgLevelStorageSource.class);
assertEquals("validateAndCreateAccess", method.getName());
assertEquals(2, method.getParameterCount());
}
@Test
public void resolvesOneArgLevelStorageAccessMethod() throws Exception {
Method method = CapabilityResolution.resolveLevelStorageAccessMethod(OneArgLevelStorageSource.class);
assertEquals("validateAndCreateAccess", method.getName());
assertEquals(1, method.getParameterCount());
}
@Test
public void resolvesPublicWorldDataHelper() throws Exception {
Method method = CapabilityResolution.resolvePaperWorldDataMethod(PublicWorldLoader.class);
assertEquals("loadWorldData", method.getName());
assertEquals(3, method.getParameterCount());
}
@Test
public void resolvesDeclaredWorldDataHelper() throws Exception {
Method method = CapabilityResolution.resolvePaperWorldDataMethod(DeclaredWorldLoader.class);
assertEquals("loadWorldData", method.getName());
assertEquals(3, method.getParameterCount());
}
@Test
public void resolvesDeclaredServerRegistryAccessMethod() throws Exception {
Method method = CapabilityResolution.resolveServerRegistryAccessMethod(DeclaredRegistryAccessOwner.class);
assertEquals("registryAccess", method.getName());
assertEquals(0, method.getParameterCount());
}
@Test
public void resolvesCurrentWorldLoadingInfoConstructor() throws Exception {
Constructor<?> constructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(CurrentWorldLoadingInfo.class);
assertEquals(4, constructor.getParameterCount());
}
@Test
public void resolvesLegacyWorldLoadingInfoConstructor() throws Exception {
Constructor<?> constructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(LegacyWorldLoadingInfo.class);
assertEquals(5, constructor.getParameterCount());
}
public static final class LevelStem {
}
public static final class WorldLoadingInfo {
}
public static final class WorldLoadingInfoAndData {
}
public static final class WorldDataAndGenSettings {
}
public static final class PrimaryLevelData {
}
public static final class LevelStorageAccess {
}
public static final class ResourceKey {
}
public static final class LoadedWorldData {
}
public static final class MinecraftServer {
}
public static final class RegistryAccess {
}
public enum Environment {
NORMAL
}
public static final class CurrentCreateLevelOwner {
public void createLevel(LevelStem levelStem, WorldLoadingInfoAndData worldLoadingInfoAndData, WorldDataAndGenSettings worldDataAndGenSettings) {
}
}
public static final class DeclaredLegacyCreateLevelOwner {
private void createLevel(LevelStem levelStem, WorldLoadingInfo worldLoadingInfo, LevelStorageAccess levelStorageAccess, PrimaryLevelData primaryLevelData) {
}
}
public static final class TwoArgLevelStorageSource {
public LevelStorageAccess validateAndCreateAccess(String worldName, ResourceKey resourceKey) {
return null;
}
}
public static final class OneArgLevelStorageSource {
public LevelStorageAccess validateAndCreateAccess(String worldName) {
return null;
}
}
public static final class PublicWorldLoader {
public static LoadedWorldData loadWorldData(MinecraftServer minecraftServer, ResourceKey dimensionKey, String worldName) {
return null;
}
}
public static final class DeclaredWorldLoader {
private static LoadedWorldData loadWorldData(MinecraftServer minecraftServer, ResourceKey dimensionKey, String worldName) {
return null;
}
}
public static final class DeclaredRegistryAccessOwner {
private RegistryAccess registryAccess() {
return null;
}
}
public static final class CurrentWorldLoadingInfo {
public CurrentWorldLoadingInfo(Environment environment, ResourceKey stemKey, ResourceKey dimensionKey, boolean enabled) {
}
}
public static final class LegacyWorldLoadingInfo {
private LegacyWorldLoadingInfo(int index, String worldName, String environment, ResourceKey stemKey, boolean enabled) {
}
}
}

View File

@@ -0,0 +1,38 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.World;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletionException;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class WorldLifecycleDiagnosticsTest {
@Test
public void studioCreateSelectionFailurePrintsFullStacktrace() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, false));
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
ByteArrayOutputStream output = new ByteArrayOutputStream();
PrintStream originalErr = System.err;
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
try {
try {
service.create(request).join();
fail("Expected lifecycle create to fail when paper_like_runtime is unavailable.");
} catch (CompletionException | IllegalStateException ignored) {
}
} finally {
System.setErr(originalErr);
}
String text = output.toString(StandardCharsets.UTF_8);
assertTrue(text.contains("WorldLifecycle create backend selection failed"));
assertTrue(text.contains("paper_like_runtime"));
assertTrue(text.contains("IllegalStateException"));
}
}

View File

@@ -0,0 +1,142 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.platform.ChunkReplacementListener;
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
public class WorldLifecycleRuntimeLevelStemTest {
@Test
public void runtimeStemUsesFullServerRegistryAccessForPlatformGenerators() throws Exception {
Object datapackDimensions = new MissingDimensionTypeRegistry();
Object serverRegistryAccess = new Object();
CapabilitySnapshot capabilities = CapabilitySnapshot.forTestingRuntimeRegistries(ServerFamily.PURPUR, false, datapackDimensions, serverRegistryAccess);
WorldLifecycleRequest request = new WorldLifecycleRequest(
"studio",
World.Environment.NORMAL,
new TestingPlatformChunkGenerator(),
null,
null,
true,
false,
1337L,
true,
false,
WorldLifecycleCaller.STUDIO
);
AtomicReference<Object> seenRegistryAccess = new AtomicReference<>();
INMSBinding binding = createBinding((registryAccess, generator) -> {
seenRegistryAccess.set(registryAccess);
return "runtime-stem";
});
Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request, binding);
assertEquals("runtime-stem", levelStem);
assertSame(serverRegistryAccess, seenRegistryAccess.get());
assertNotSame(datapackDimensions, seenRegistryAccess.get());
}
private static INMSBinding createBinding(RuntimeStemFactory factory) {
InvocationHandler handler = (proxy, method, args) -> {
if ("createRuntimeLevelStem".equals(method.getName())) {
return factory.create(args[0], (ChunkGenerator) args[1]);
}
Class<?> returnType = method.getReturnType();
if (boolean.class.equals(returnType)) {
return false;
}
if (int.class.equals(returnType)) {
return 0;
}
if (long.class.equals(returnType)) {
return 0L;
}
if (float.class.equals(returnType)) {
return 0F;
}
if (double.class.equals(returnType)) {
return 0D;
}
return null;
};
return (INMSBinding) Proxy.newProxyInstance(
INMSBinding.class.getClassLoader(),
new Class[]{INMSBinding.class},
handler
);
}
@FunctionalInterface
private interface RuntimeStemFactory {
Object create(Object registryAccess, ChunkGenerator generator);
}
private static final class MissingDimensionTypeRegistry {
}
private static final class TestingPlatformChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator {
@Override
public Engine getEngine() {
return null;
}
@Override
public IrisData getData() {
return null;
}
@Override
public EngineTarget getTarget() {
return null;
}
@Override
public void injectChunkReplacement(World world, int x, int z, Executor syncExecutor, ChunkReplacementOptions options, ChunkReplacementListener listener) {
}
@Override
public void close() {
}
@Override
public boolean isStudio() {
return true;
}
@Override
public void touch(World world) {
}
@Override
public CompletableFuture<Integer> getSpawnChunks() {
return CompletableFuture.completedFuture(0);
}
@Override
public Location getInitialSpawnLocation(World world) {
return null;
}
@Override
public void hotload() {
}
}
}

View File

@@ -0,0 +1,64 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.World;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class WorldLifecycleSelectionTest {
@Test
public void studioSelectsPaperLikeBackendOnPaper() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, true));
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
}
@Test
public void studioSelectsPaperLikeBackendOnPurpur() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true));
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
}
@Test
public void studioSelectsPaperLikeBackendOnCanvas() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.CANVAS, true, false, true));
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
}
@Test
public void studioSelectsPaperLikeBackendOnFolia() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, true));
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
}
@Test
public void studioSelectsBukkitBackendOnSpigot() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.SPIGOT, false, false, false));
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
assertEquals("bukkit_public", service.selectCreateBackend(request).backendName());
}
@Test
public void persistentCreatePrefersBukkitBackendOnPaperLikeServers() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true));
WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE);
assertEquals("bukkit_public", service.selectCreateBackend(request).backendName());
}
@Test
public void unloadUsesRememberedBackendFamily() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true));
service.rememberBackend("studio", "paper_like_runtime");
assertEquals("paper_like_runtime", service.selectUnloadBackend("studio").backendName());
}
}

View File

@@ -0,0 +1,46 @@
package art.arcane.iris.core.lifecycle;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
public class WorldLifecycleStagingTest {
@Test
public void stagedGeneratorIsConsumedExactlyOnce() {
ChunkGenerator generator = mock(ChunkGenerator.class);
WorldLifecycleStaging.stageGenerator("world", generator, null);
assertSame(generator, WorldLifecycleStaging.consumeGenerator("world"));
assertNull(WorldLifecycleStaging.consumeGenerator("world"));
}
@Test
public void stagedStemGeneratorIsIndependentFromGeneratorConsumption() {
ChunkGenerator generator = mock(ChunkGenerator.class);
WorldLifecycleStaging.stageGenerator("world", generator, null);
WorldLifecycleStaging.stageStemGenerator("world", generator);
assertSame(generator, WorldLifecycleStaging.consumeGenerator("world"));
assertSame(generator, WorldLifecycleStaging.consumeStemGenerator("world"));
}
@Test
public void clearAllRemovesGeneratorBiomeAndStemState() {
ChunkGenerator generator = mock(ChunkGenerator.class);
BiomeProvider biomeProvider = mock(BiomeProvider.class);
WorldLifecycleStaging.stageGenerator("world", generator, biomeProvider);
WorldLifecycleStaging.stageStemGenerator("world", generator);
WorldLifecycleStaging.clearAll("world");
assertNull(WorldLifecycleStaging.consumeGenerator("world"));
assertNull(WorldLifecycleStaging.consumeBiomeProvider("world"));
assertNull(WorldLifecycleStaging.consumeStemGenerator("world"));
}
}

View File

@@ -0,0 +1,31 @@
package art.arcane.iris.core.nms;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class INMSBindingProbeCodesTest {
@Test
public void skipsSyntheticBukkitBindingWhenNmsIsEnabled() {
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("BUKKIT", false, List.of("v1_21_R7"));
assertEquals(List.of("v1_21_R7"), probeCodes);
}
@Test
public void leavesBukkitFallbackEmptyWhenNmsIsDisabled() {
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("BUKKIT", true, List.of("v1_21_R7"));
assertTrue(probeCodes.isEmpty());
}
@Test
public void keepsConcreteBindingCodesAsPrimaryProbe() {
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("v1_21_R7", false, List.of("v1_21_R7"));
assertEquals(List.of("v1_21_R7"), probeCodes);
}
}

View File

@@ -0,0 +1,63 @@
package art.arcane.iris.core.nms;
import org.bukkit.Server;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class MinecraftVersionTest {
private interface PaperLikeServer extends Server {
String getMinecraftVersion();
}
@Test
public void detectsMinecraftVersionFromPurpurDecoratedVersion() {
Server server = mock(Server.class);
doReturn("git-Purpur-2570 (MC: 1.21.11)").when(server).getVersion();
doReturn("26.1.2.build.2570-experimental").when(server).getBukkitVersion();
MinecraftVersion version = MinecraftVersion.detect(server);
assertEquals("1.21.11", version.value());
assertEquals(21, version.major());
assertEquals(11, version.minor());
}
@Test
public void prefersRuntimeMinecraftVersionMethodWhenPresent() {
PaperLikeServer server = mock(PaperLikeServer.class);
doReturn("1.21.11").when(server).getMinecraftVersion();
doReturn("26.1.2-2570-e64b1b2 (MC: 26.1.2)").when(server).getVersion();
doReturn("26.1.2.build.2570-experimental").when(server).getBukkitVersion();
MinecraftVersion version = MinecraftVersion.detect(server);
assertEquals("1.21.11", version.value());
assertEquals(21, version.major());
assertEquals(11, version.minor());
}
@Test
public void rejectsPurpurApiBuildNumbersAsMinecraftVersion() {
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.1.2.build.2570-experimental");
assertNull(version);
}
@Test
public void parsesStandardBukkitSnapshotVersion() {
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("1.21.11-R0.1-SNAPSHOT");
assertEquals("1.21.11", version.value());
assertEquals(21, version.major());
assertEquals(11, version.minor());
}
@Test
public void comparesMajorBeforeMinor() {
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("1.20.12-R0.1-SNAPSHOT");
assertFalse(version.isAtLeast(21, 11));
assertTrue(version.isNewerThan(20, 11));
}
}

View File

@@ -0,0 +1,28 @@
package art.arcane.iris.core.nms.datapack.v1217;
import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class DataFixerV1217DimensionTypeTest {
private final DataFixerV1217 fixer = new DataFixerV1217();
@Test
public void createsOverworldDimensionWithDragonFightDisabled() {
JSONObject json = fixer.createDimension(Dimension.OVERWORLD, -256, 768, 512, null);
assertTrue(json.has("has_ender_dragon_fight"));
assertEquals(false, json.getBoolean("has_ender_dragon_fight"));
}
@Test
public void createsEndDimensionWithDragonFightEnabled() {
JSONObject json = fixer.createDimension(Dimension.END, 0, 256, 256, null);
assertTrue(json.has("has_ender_dragon_fight"));
assertEquals(true, json.getBoolean("has_ender_dragon_fight"));
}
}

View File

@@ -0,0 +1,45 @@
package art.arcane.iris.core.runtime;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class DatapackReadinessResultTest {
@Test
public void verificationUsesDimensionTypeKeyPath() throws Exception {
Path root = Files.createTempDirectory("iris-datapack-readiness");
Path datapackRoot = root.resolve("iris");
Files.createDirectories(datapackRoot.resolve("data/iris/dimension_type"));
Files.writeString(datapackRoot.resolve("pack.mcmeta"), "{}");
Files.writeString(datapackRoot.resolve("data/iris/dimension_type/runtime-key.json"), "{}");
ArrayList<String> verifiedPaths = new ArrayList<>();
ArrayList<String> missingPaths = new ArrayList<>();
DatapackReadinessResult.collectVerificationPaths(root.toFile(), "runtime-key", verifiedPaths, missingPaths);
assertTrue(missingPaths.isEmpty());
assertEquals(2, verifiedPaths.size());
}
@Test
public void verificationMarksMissingDimensionTypePath() throws Exception {
Path root = Files.createTempDirectory("iris-datapack-readiness-missing");
Path datapackRoot = root.resolve("iris");
Files.createDirectories(datapackRoot);
Files.writeString(datapackRoot.resolve("pack.mcmeta"), "{}");
ArrayList<String> verifiedPaths = new ArrayList<>();
ArrayList<String> missingPaths = new ArrayList<>();
DatapackReadinessResult.collectVerificationPaths(root.toFile(), "runtime-key", verifiedPaths, missingPaths);
assertEquals(1, verifiedPaths.size());
assertEquals(1, missingPaths.size());
assertTrue(missingPaths.get(0).endsWith(File.separator + "iris" + File.separator + "data" + File.separator + "iris" + File.separator + "dimension_type" + File.separator + "runtime-key.json"));
}
}

View File

@@ -0,0 +1,28 @@
package art.arcane.iris.core.runtime;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SmokeDiagnosticsServiceCloseStateTest {
@Test
public void closeStateIsPersistedIntoRunSnapshot() {
SmokeDiagnosticsService service = SmokeDiagnosticsService.get();
SmokeDiagnosticsService.SmokeRunHandle handle = service.beginRun(
SmokeDiagnosticsService.SmokeRunMode.STUDIO_CLOSE,
"iris-test-world",
true,
true,
null,
false
);
handle.setCloseState(true, false, true);
SmokeDiagnosticsService.SmokeRunReport report = handle.snapshot();
assertTrue(report.isCloseUnloadCompletedLive());
assertFalse(report.isCloseFolderDeletionCompletedLive());
assertTrue(report.isCloseStartupCleanupQueued());
}
}

View File

@@ -0,0 +1,63 @@
package art.arcane.iris.core.runtime;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.LinkedHashSet;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class TransientWorldCleanupSupportTest {
@Test
public void identifiesTransientStudioBaseNamesAndSidecars() {
String baseName = "iris-123e4567-e89b-12d3-a456-426614174000";
assertTrue(TransientWorldCleanupSupport.isTransientStudioWorldName(baseName));
assertEquals(baseName, TransientWorldCleanupSupport.transientStudioBaseWorldName(baseName));
assertEquals(baseName, TransientWorldCleanupSupport.transientStudioBaseWorldName(baseName + "_nether"));
assertEquals(baseName, TransientWorldCleanupSupport.transientStudioBaseWorldName(baseName + "_the_end"));
assertFalse(TransientWorldCleanupSupport.isTransientStudioWorldName("iris-smoke-studio-deadbeef"));
assertNull(TransientWorldCleanupSupport.transientStudioBaseWorldName("overworld"));
}
@Test
public void expandsWorldFamilyNamesForDeletion() {
String baseName = "iris-123e4567-e89b-12d3-a456-426614174000";
List<String> names = TransientWorldCleanupSupport.worldFamilyNames(baseName);
assertEquals(List.of(baseName, baseName + "_nether", baseName + "_the_end"), names);
}
@Test
public void collectsOnlyTransientStudioWorldFamiliesFromContainer() throws IOException {
File container = Files.createTempDirectory("transient-world-cleanup-test").toFile();
String baseName = "iris-123e4567-e89b-12d3-a456-426614174000";
File baseFolder = new File(container, baseName);
File netherFolder = new File(container, baseName + "_nether");
File smokeFolder = new File(container, "iris-smoke-studio-deadbeef");
File regularFolder = new File(container, "overworld");
baseFolder.mkdirs();
netherFolder.mkdirs();
smokeFolder.mkdirs();
regularFolder.mkdirs();
try {
LinkedHashSet<String> names = TransientWorldCleanupSupport.collectTransientStudioWorldNames(container);
assertEquals(new LinkedHashSet<>(List.of(baseName)), names);
} finally {
Files.deleteIfExists(baseFolder.toPath());
Files.deleteIfExists(netherFolder.toPath());
Files.deleteIfExists(smokeFolder.toPath());
Files.deleteIfExists(regularFolder.toPath());
Files.deleteIfExists(container.toPath());
}
}
}

View File

@@ -0,0 +1,59 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class WorldRuntimeControlServiceSafeEntryTest {
@Test
public void resolvesStudioEntryAnchorFromGeneratorInsteadOfMutableWorldSpawn() {
World world = mock(World.class);
PlatformChunkGenerator provider = mock(PlatformChunkGenerator.class);
Location initialSpawn = new Location(world, 0.5D, 96D, 0.5D);
Location mutableWorldSpawn = new Location(world, 128.5D, 80D, -64.5D);
doReturn(true).when(provider).isStudio();
doReturn(initialSpawn).when(provider).getInitialSpawnLocation(world);
doReturn(mutableWorldSpawn).when(world).getSpawnLocation();
Location resolved = WorldRuntimeControlService.resolveEntryAnchor(world, provider);
assertEquals(initialSpawn, resolved);
}
@Test
public void fallsBackToWorldSpawnWhenGeneratorIsNotStudio() {
World world = mock(World.class);
PlatformChunkGenerator provider = mock(PlatformChunkGenerator.class);
Location mutableWorldSpawn = new Location(world, 128.5D, 80D, -64.5D);
doReturn(false).when(provider).isStudio();
doReturn(mutableWorldSpawn).when(world).getSpawnLocation();
Location resolved = WorldRuntimeControlService.resolveEntryAnchor(world, provider);
assertEquals(mutableWorldSpawn, resolved);
}
@Test
public void scansFromRuntimeSurfaceBeforeFallingThroughRemainingWorldHeight() {
World world = mock(World.class);
doReturn(0).when(world).getMinHeight();
doReturn(256).when(world).getMaxHeight();
doReturn(179).when(world).getHighestBlockYAt(0, 0);
int[] scanOrder = WorldRuntimeControlService.buildSafeLocationScanOrder(world, new Location(world, 0.5D, 96D, 0.5D));
assertEquals(180, scanOrder[0]);
assertEquals(179, scanOrder[1]);
assertEquals(1, scanOrder[179]);
assertEquals(181, scanOrder[180]);
assertEquals(254, scanOrder[scanOrder.length - 1]);
}
}

View File

@@ -0,0 +1,239 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
import art.arcane.iris.core.lifecycle.ServerFamily;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.plugin.PluginManager;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class WorldRuntimeControlServiceTimeLockTest {
@Before
public void ensureBukkitServer() {
if (Bukkit.getServer() != null) {
return;
}
Server server = mock(Server.class);
PluginManager pluginManager = mock(PluginManager.class);
doReturn(pluginManager).when(server).getPluginManager();
doReturn(Logger.getLogger("WorldRuntimeControlServiceTimeLockTest")).when(server).getLogger();
Bukkit.setServer(server);
}
@Test
public void skipsTimeLockWhenWorldDoesNotExposeMutableClock() throws Exception {
AtomicBoolean setTimeCalled = new AtomicBoolean(false);
AtomicLong dayTime = new AtomicLong(0L);
World world = createWorldProxy("fixed", true, setTimeCalled, dayTime, false);
boolean applied = createService().applyNoonTimeLock(world);
assertFalse(applied);
assertFalse(setTimeCalled.get());
}
@Test
public void skipsTimeLockWhenRuntimeSetterRejectsClockMutation() throws Exception {
AtomicBoolean setTimeCalled = new AtomicBoolean(false);
AtomicLong dayTime = new AtomicLong(0L);
World world = createWorldProxy("no-clock", false, setTimeCalled, dayTime, true);
boolean applied = createService().applyNoonTimeLock(world);
assertFalse(applied);
assertTrue(setTimeCalled.get());
}
@Test
public void appliesTimeLockWhenWorldHasMutableClock() throws Exception {
AtomicBoolean setTimeCalled = new AtomicBoolean(false);
AtomicLong dayTime = new AtomicLong(0L);
World world = createWorldProxy("mutable", false, setTimeCalled, dayTime, false);
boolean applied = createService().applyNoonTimeLock(world);
assertTrue(applied);
assertTrue(setTimeCalled.get());
assertTrue(dayTime.get() == 6000L);
}
private WorldRuntimeControlService createService() throws Exception {
Constructor<CapabilitySnapshot> snapshotConstructor = CapabilitySnapshot.class.getDeclaredConstructor(
ServerFamily.class,
boolean.class,
Object.class,
Class.class,
Class.class,
String.class,
Object.class,
Object.class,
java.lang.reflect.Method.class,
CapabilitySnapshot.PaperLikeFlavor.class,
Class.class,
java.lang.reflect.Method.class,
java.lang.reflect.Constructor.class,
java.lang.reflect.Constructor.class,
java.lang.reflect.Method.class,
java.lang.reflect.Method.class,
java.lang.reflect.Field.class,
java.lang.reflect.Method.class,
java.lang.reflect.Field.class,
java.lang.reflect.Field.class,
java.lang.reflect.Method.class,
java.lang.reflect.Method.class,
java.lang.reflect.Method.class,
java.lang.reflect.Method.class,
String.class
);
snapshotConstructor.setAccessible(true);
CapabilitySnapshot snapshot = snapshotConstructor.newInstance(
ServerFamily.PAPER,
false,
null,
null,
null,
"test",
Bukkit.getServer(),
null,
null,
CapabilitySnapshot.PaperLikeFlavor.UNSUPPORTED,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
"test"
);
Constructor<WorldRuntimeControlService> serviceConstructor = WorldRuntimeControlService.class.getDeclaredConstructor(CapabilitySnapshot.class);
serviceConstructor.setAccessible(true);
return serviceConstructor.newInstance(snapshot);
}
private World createWorldProxy(String name, boolean fixedTime, AtomicBoolean setTimeCalled, AtomicLong dayTime, boolean throwOnSetTime) {
Object dimensionType = Proxy.newProxyInstance(
World.class.getClassLoader(),
new Class[]{DimensionTypeProbe.class},
(proxy, method, args) -> {
if ("hasFixedTime".equals(method.getName())) {
return fixedTime;
}
if ("fixedTime".equals(method.getName())) {
return fixedTime ? OptionalLong.of(6000L) : OptionalLong.empty();
}
return null;
}
);
Object holder = Proxy.newProxyInstance(
World.class.getClassLoader(),
new Class[]{HolderProbe.class},
(proxy, method, args) -> {
if ("value".equals(method.getName())) {
return dimensionType;
}
return null;
}
);
Object handle = Proxy.newProxyInstance(
World.class.getClassLoader(),
new Class[]{HandleProbe.class},
(proxy, method, args) -> {
if ("dimensionTypeRegistration".equals(method.getName())) {
return holder;
}
if ("getDayTime".equals(method.getName())) {
return dayTime.get();
}
if ("setDayTime".equals(method.getName())) {
setTimeCalled.set(true);
if (throwOnSetTime) {
throw new IllegalArgumentException("Cannot set time in world without world clock");
}
dayTime.set(((Long) args[0]).longValue());
return null;
}
return null;
}
);
return (World) Proxy.newProxyInstance(
World.class.getClassLoader(),
new Class[]{World.class, WorldHandleProbe.class},
(proxy, method, args) -> {
if ("getName".equals(method.getName())) {
return name;
}
if ("getHandle".equals(method.getName())) {
return handle;
}
if ("getFullTime".equals(method.getName())) {
return dayTime.get();
}
Class<?> returnType = method.getReturnType();
if (boolean.class.equals(returnType)) {
return false;
}
if (int.class.equals(returnType)) {
return 0;
}
if (long.class.equals(returnType)) {
return 0L;
}
if (float.class.equals(returnType)) {
return 0F;
}
if (double.class.equals(returnType)) {
return 0D;
}
return null;
}
);
}
private interface WorldHandleProbe {
Object getHandle();
}
private interface HandleProbe {
Object dimensionTypeRegistration();
long getDayTime();
void setDayTime(long time);
}
private interface HolderProbe {
Object value();
}
private interface DimensionTypeProbe {
boolean hasFixedTime();
OptionalLong fixedTime();
}
}

View File

@@ -0,0 +1,47 @@
package art.arcane.iris.engine.framework;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertTrue;
public class GenerationSessionManagerTest {
@Test
public void teardownSealMarksRejectedWorkAsExpected() throws Exception {
GenerationSessionManager manager = new GenerationSessionManager();
manager.sealAndAwait("close", 1000L, true);
try {
manager.acquire("chunk_generate");
} catch (GenerationSessionException e) {
assertTrue(e.isExpectedTeardown());
assertTrue(e.getMessage().contains("during close"));
return;
}
throw new AssertionError("Expected teardown rejection.");
}
@Test
public void sealAndAwaitCompletesWhenOutstandingLeaseReleases() throws Exception {
GenerationSessionManager manager = new GenerationSessionManager();
GenerationSessionLease lease = manager.acquire("chunk_generate");
CountDownLatch latch = new CountDownLatch(1);
Thread releaser = new Thread(() -> {
try {
latch.await(200L, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
lease.close();
});
releaser.start();
latch.countDown();
manager.sealAndAwait("close", 1000L, true);
}
}

View File

@@ -0,0 +1,23 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class IrisDimensionTypeKeyTest {
@Test
public void dimensionTypeKeyUsesSanitizedSemanticPackKey() {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("Overworld");
assertEquals("overworld", dimension.getDimensionTypeKey());
}
@Test
public void dimensionTypeKeySanitizesUnsafePackCharacters() {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("Worlds/My Pack");
assertEquals("worlds_my_pack", dimension.getDimensionTypeKey());
}
}

View File

@@ -0,0 +1,81 @@
package art.arcane.iris.engine.object;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
public class IrisObjectBlockDataMergeTest {
@Test
public void reparsesBlockDataBeforeRetryingMerge() {
BlockData base = mock(BlockData.class);
BlockData update = mock(BlockData.class);
BlockData parsedBase = mock(BlockData.class);
BlockData parsedUpdate = mock(BlockData.class);
BlockData merged = mock(BlockData.class);
Function<String, BlockData> resolver = createResolver(
"minecraft:oak_log[axis=x]", parsedBase,
"minecraft:oak_log[axis=y]", parsedUpdate
);
doThrow(new IllegalArgumentException("Block data not created via string parsing")).when(base).merge(update);
doReturn("minecraft:oak_log[axis=x]").when(base).getAsString(false);
doReturn("minecraft:oak_log[axis=y]").when(update).getAsString(false);
doReturn(merged).when(parsedBase).merge(parsedUpdate);
BlockData result = BlockDataMergeSupport.merge(base, update, resolver);
assertSame(merged, result);
}
@Test
public void fallsBackToNormalizedUpdateWhenRetryMergeStillFails() {
BlockData base = mock(BlockData.class);
BlockData update = mock(BlockData.class);
BlockData parsedBase = mock(BlockData.class);
BlockData parsedUpdate = mock(BlockData.class);
Function<String, BlockData> resolver = createResolver(
"minecraft:stone", parsedBase,
"minecraft:stone[waterlogged=true]", parsedUpdate
);
doThrow(new IllegalArgumentException("Block data not created via string parsing")).when(base).merge(update);
doReturn("minecraft:stone").when(base).getAsString(false);
doReturn("minecraft:stone[waterlogged=true]").when(update).getAsString(false);
doThrow(new IllegalArgumentException("normalized merge failed")).when(parsedBase).merge(parsedUpdate);
BlockData result = BlockDataMergeSupport.merge(base, update, resolver);
assertSame(parsedUpdate, result);
}
@Test
public void keepsDirectMergeWhenBukkitAcceptsIt() {
BlockData base = mock(BlockData.class);
BlockData update = mock(BlockData.class);
BlockData merged = mock(BlockData.class);
doReturn(Material.STONE).when(base).getMaterial();
doReturn(Material.STONE).when(update).getMaterial();
doReturn(merged).when(base).merge(update);
BlockData result = BlockDataMergeSupport.merge(base, update, key -> null);
assertSame(merged, result);
}
private Function<String, BlockData> createResolver(String firstKey, BlockData firstValue, String secondKey, BlockData secondValue) {
Map<String, BlockData> resolved = new HashMap<>();
resolved.put(firstKey, firstValue);
resolved.put(secondKey, secondValue);
return resolved::get;
}
}

View File

@@ -0,0 +1,36 @@
package art.arcane.iris.util.common.parallel;
import art.arcane.volmlib.util.parallel.BurstExecutorSupport;
import org.junit.Test;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertTrue;
public class BurstExecutorSupportReentrantTest {
@Test
public void runsNestedBurstInlineOnSameForkJoinPoolWorker() throws Exception {
ForkJoinPool pool = new ForkJoinPool(1);
AtomicBoolean nestedExecuted = new AtomicBoolean(false);
try {
Future<?> future = pool.submit(() -> {
BurstExecutorSupport burst = new BurstExecutorSupport(pool, 1);
burst.queue(() -> {
BurstExecutorSupport nested = new BurstExecutorSupport(pool, 1);
nested.queue(() -> nestedExecuted.set(true));
nested.complete();
});
burst.complete();
});
future.get(5, TimeUnit.SECONDS);
assertTrue(nestedExecuted.get());
} finally {
pool.shutdownNow();
}
}
}

View File

@@ -0,0 +1,24 @@
package art.arcane.iris.util.common.plugin;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class VolmitSenderMiniMessageEscapeTest {
@Test
public void escapesApostrophesForQuotedHoverText() {
String escaped = VolmitSender.escapeMiniMessageQuotedText("This world's dimension config");
assertEquals("This world\\'s dimension config", escaped);
MiniMessage.miniMessage().deserialize("<hover:show_text:'" + escaped + "'>ok</hover>");
}
@Test
public void escapesBackslashesBeforeQuotedHoverText() {
String escaped = VolmitSender.escapeMiniMessageQuotedText("Path \\\\ data");
assertEquals("Path \\\\\\\\ data", escaped);
MiniMessage.miniMessage().deserialize("<hover:show_text:'" + escaped + "'>ok</hover>");
}
}

View File

@@ -1,5 +1,6 @@
package art.arcane.iris.util.project.context;
import art.arcane.iris.Iris;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
@@ -7,9 +8,16 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
import org.bukkit.block.data.BlockData;
import org.junit.Test;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
@@ -76,6 +84,16 @@ public class ChunkContextPrefillPlanTest {
assertEquals(256, caveCalls.get());
}
@Test
public void paperCommonWorkerThreadsDisableAsyncPrefillWhenPluginLoaded() throws Exception {
assertPrefillAsyncDecision("Paper Common Worker #0", false);
}
@Test
public void irisWorkerThreadsKeepAsyncPrefillWhenPluginLoaded() throws Exception {
assertPrefillAsyncDecision("Iris 42", true);
}
private ChunkContext createContext(
ChunkContext.PrefillPlan prefillPlan,
AtomicInteger caveCalls,
@@ -145,4 +163,26 @@ public class ChunkContextPrefillPlanTest {
return new ChunkContext(32, 48, complex, true, prefillPlan, null);
}
private void assertPrefillAsyncDecision(String threadName, boolean expected) throws InterruptedException, ExecutionException, java.util.concurrent.TimeoutException {
Iris previous = Iris.instance;
ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable);
thread.setName(threadName);
return thread;
});
try {
Iris.instance = mock(Iris.class);
Future<Boolean> future = executor.submit(() -> ChunkContext.shouldPrefillAsync(2));
boolean actual = future.get(10, TimeUnit.SECONDS);
if (expected) {
assertTrue(actual);
} else {
assertFalse(actual);
}
} finally {
Iris.instance = previous;
executor.shutdownNow();
}
}
}

View File

@@ -0,0 +1,29 @@
Paper 1.21.11
/iris create matrix-paper overworld
/iris std o overworld
close Studio world
benchmark create/unload
Purpur 1.21.11
/iris create matrix-purpur overworld
/iris std o overworld
close Studio world
benchmark create/unload
Canvas 1.21.11
/iris create matrix-canvas overworld
/iris std o overworld
close Studio world
benchmark create/unload
Folia 1.21.11
/iris create matrix-folia overworld
/iris std o overworld
close Studio world
benchmark create/unload
Spigot 1.21.11
/iris create matrix-spigot overworld
/iris std o overworld
close Studio world
unload/remove