From 05d79b6d40170a73d8901b416a6ab406e3c22cd5 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 17 Feb 2026 06:18:55 -0500 Subject: [PATCH] Create std --- core/src/main/java/art/arcane/iris/Iris.java | 70 +- .../iris/core/commands/CommandStudio.java | 7 - .../iris/core/link/FoliaWorldsLink.java | 720 ++++++++++++++++++ .../art/arcane/iris/core/nms/INMSBinding.java | 17 + .../arcane/iris/core/project/IrisProject.java | 57 +- .../iris/core/service/GlobalCacheSVC.java | 10 +- .../arcane/iris/core/tools/IrisCreator.java | 83 +- .../arcane/iris/core/tools/IrisToolbelt.java | 71 +- .../engine/platform/BukkitChunkGenerator.java | 5 + 9 files changed, 987 insertions(+), 53 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index 44c500481..b417999fd 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -78,6 +78,7 @@ import java.io.*; import java.lang.annotation.Annotation; import java.net.URL; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -96,6 +97,8 @@ public class Iris extends VolmitPlugin implements Listener { private static Thread shutdownHook; private static File settingsFile; private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt"; + private static final Map stagedRuntimeGenerators = new ConcurrentHashMap<>(); + private static final Map stagedRuntimeBiomeProviders = new ConcurrentHashMap<>(); static { try { @@ -118,6 +121,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 service(Class c) { return (T) instance.services.get(c); @@ -264,11 +291,11 @@ public class Iris extends VolmitPlugin implements Listener { } public static void warn(String format, Object... objs) { - msg(C.YELLOW + String.format(format, objs)); + msg(C.YELLOW + safeFormat(format, objs)); } public static void error(String format, Object... objs) { - msg(C.RED + String.format(format, objs)); + msg(C.RED + safeFormat(format, objs)); } public static void debug(String string) { @@ -314,7 +341,23 @@ public class Iris extends VolmitPlugin implements Listener { } public static void info(String format, Object... args) { - msg(C.WHITE + String.format(format, args)); + msg(C.WHITE + safeFormat(format, args)); + } + + private static String safeFormat(String format, Object... args) { + if (format == null) { + return "null"; + } + + if (args == null || args.length == 0) { + return format; + } + + try { + return String.format(format, args); + } catch (IllegalFormatException ignored) { + return format; + } } @SuppressWarnings("deprecation") @@ -725,10 +768,15 @@ public class Iris extends VolmitPlugin implements Listener { Player r = new KList<>(getServer().getOnlinePlayers()).getRandom(); Iris.service(StudioSVC.class).open(r != null ? new VolmitSender(r) : getSender(), 1337, IrisSettings.get().getGenerator().getDefaultWorldType(), (w) -> { J.s(() -> { - var spawn = w.getSpawnLocation(); + final Location spawn = w.getSpawnLocation(); for (Player i : getServer().getOnlinePlayers()) { - i.setGameMode(GameMode.SPECTATOR); - i.teleport(spawn); + final Runnable playerTask = () -> { + i.setGameMode(GameMode.SPECTATOR); + i.teleport(spawn); + }; + if (!J.runEntity(i, playerTask)) { + playerTask.run(); + } } }); }); @@ -887,12 +935,22 @@ public class Iris extends VolmitPlugin implements Listener { @Nullable @Override public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) { + BiomeProvider stagedBiomeProvider = consumeRuntimeBiomeProvider(worldName); + if (stagedBiomeProvider != null) { + Iris.debug("Using staged runtime biome provider for " + worldName); + return stagedBiomeProvider; + } Iris.debug("Biome Provider Called for " + worldName + " using ID: " + id); return super.getDefaultBiomeProvider(worldName, id); } @Override public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { + ChunkGenerator stagedGenerator = consumeRuntimeWorldGenerator(worldName); + if (stagedGenerator != null) { + Iris.debug("Using staged runtime generator for " + worldName); + return stagedGenerator; + } Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id); if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType(); Iris.debug("Generator ID: " + id + " requested by bukkit/plugin"); diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java index 5729f5422..603208757 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -114,13 +114,6 @@ public class CommandStudio implements DirectorExecutor { IrisDimension dimension, @Param(defaultValue = "1337", description = "The seed to generate the studio with", aliases = "s") long seed) { - if (J.isFolia()) { - sender().sendMessage(C.RED + "Studio world opening is disabled on Folia."); - sender().sendMessage(C.YELLOW + "Folia does not currently support runtime world creation via Bukkit.createWorld()."); - sender().sendMessage(C.YELLOW + "Use Paper/Purpur for Studio mode, or preconfigure worlds and restart."); - return; - } - sender().sendMessage(C.GREEN + "Opening studio for the \"" + dimension.getName() + "\" pack (seed: " + seed + ")"); Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey()); } diff --git a/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java b/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java new file mode 100644 index 000000000..e6e8312e3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java @@ -0,0 +1,720 @@ +package art.arcane.iris.core.link; + +import art.arcane.iris.Iris; +import art.arcane.iris.util.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; + +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() { + return isWorldsProviderActive() || isPaperWorldLoaderActive(); + } + + public CompletableFuture createWorld(WorldCreator creator) { + if (isWorldsProviderActive()) { + CompletableFuture 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; + } + + 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 isWorldsProviderActive() { + return provider != null && levelStemClass != null && generatorTypeClass != null; + } + + private boolean isPaperWorldLoaderActive() { + return minecraftServer != null && minecraftServerCreateLevelMethod != null; + } + + private CompletableFuture 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 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 overworldLevelStem = getOverworldLevelStem(); + Object[] createLevelArgs = new Object[]{overworldLevelStem, 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 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 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; + } + + Server server = Bukkit.getServer(); + boolean globalThread = false; + if (server != null) { + try { + Method isGlobalTickThreadMethod = server.getClass().getMethod("isGlobalTickThread"); + globalThread = (boolean) isGlobalTickThreadMethod.invoke(server); + } catch (Throwable ignored) { + } + } + + if (globalThread) { + detachTask.run(); + return; + } + + CompletableFuture 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 Throwable unwrap(Throwable throwable) { + if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) { + return invocationTargetException.getCause(); + } + return throwable; + } + + private static Object[] resolveProviderFromServices() { + Object provider = null; + Class levelStem = null; + Class generatorType = null; + + try { + Collection> 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 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() + "#"); + } + + 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 ""; + } + + 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 ""; + } + + 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; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java index d34c31a20..21602fb8e 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -18,6 +18,7 @@ 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.nms.container.BiomeColor; import art.arcane.iris.core.nms.container.BlockProperty; @@ -43,6 +44,7 @@ import org.bukkit.inventory.ItemStack; import java.awt.Color; import java.util.List; +import java.util.concurrent.CompletableFuture; public interface INMSBinding { boolean hasTile(Material material); @@ -100,6 +102,21 @@ public interface INMSBinding { return c.createWorld(); } + default CompletableFuture createWorldAsync(WorldCreator c) { + try { + FoliaWorldsLink link = FoliaWorldsLink.get(); + if (link.isActive()) { + CompletableFuture future = link.createWorld(c); + if (future != null) { + return future; + } + } + return CompletableFuture.completedFuture(createWorld(c)); + } catch (Throwable e) { + return CompletableFuture.failedFuture(e); + } + } + int countCustomBiomes(); void forceBiomeInto(int x, int y, int z, Object somethingVeryDirty, ChunkGenerator.BiomeGrid chunk); diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java index 8f03dab74..a45108b09 100644 --- a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -21,6 +21,7 @@ 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.loader.IrisData; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.loader.ResourceLoader; @@ -58,6 +59,7 @@ import java.io.File; import java.io.IOException; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @SuppressWarnings("ALL") @@ -225,7 +227,7 @@ public class IrisProject { sender.sendMessage("Can't find dimension: " + getName()); return; } else if (sender.isPlayer()) { - J.s(() -> sender.player().setGameMode(GameMode.SPECTATOR)); + J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.SPECTATOR)); } try { @@ -233,7 +235,7 @@ public class IrisProject { .seed(seed) .sender(sender) .studio(true) - .name("iris/" + UUID.randomUUID()) + .name("iris-" + UUID.randomUUID()) .dimension(d.getLoadKey()) .create().getGenerator(); onDone.accept(activeProvider.getTarget().getWorld().realWorld()); @@ -241,19 +243,56 @@ public class IrisProject { e.printStackTrace(); } - openVSCode(sender); + if (activeProvider != null) { + openVSCode(sender); + } }); } public void close() { + if (activeProvider == null) { + return; + } + Iris.debug("Closing Active Provider"); - IrisToolbelt.evacuate(activeProvider.getTarget().getWorld().realWorld()); - activeProvider.close(); - File folder = activeProvider.getTarget().getWorld().worldFolder(); - Iris.linkMultiverseCore.removeFromConfig(activeProvider.getTarget().getWorld().name()); - Bukkit.unloadWorld(activeProvider.getTarget().getWorld().name(), false); + 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 closeFuture = J.sfut(closeTask); + if (closeFuture != null) { + try { + closeFuture.join(); + } catch (Throwable e) { + Iris.reportError(e); + e.printStackTrace(); + } + } else { + closeTask.run(); + } + } + J.attemptAsync(() -> IO.delete(folder)); - Iris.debug("Closed Active Provider " + activeProvider.getTarget().getWorld().name()); + Iris.debug("Closed Active Provider " + worldName); activeProvider = null; } diff --git a/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java b/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java index 6d80322e7..e3b1436fa 100644 --- a/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/GlobalCacheSVC.java @@ -52,9 +52,13 @@ public class GlobalCacheSVC implements IrisService { @Override public void onDisable() { disabled = true; - try { - trimmer.join(); - } catch (InterruptedException ignored) {} + Looper activeTrimmer = trimmer; + if (activeTrimmer != null) { + try { + activeTrimmer.join(); + } catch (InterruptedException ignored) { + } + } globalCache.qclear((world, cache) -> cache.write()); } diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index a1cae7fc2..093ff1753 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -22,27 +22,34 @@ import com.google.common.util.concurrent.AtomicDouble; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.link.FoliaWorldsLink; import art.arcane.iris.core.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.object.IrisDimension; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.exceptions.IrisException; import art.arcane.iris.util.format.C; import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.IO; import art.arcane.iris.util.plugin.VolmitSender; import art.arcane.iris.util.scheduling.J; import art.arcane.volmlib.util.scheduling.O; +import io.papermc.lib.PaperLib; import lombok.Data; import lombok.experimental.Accessors; import org.bukkit.*; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; import java.io.File; import java.io.IOException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.function.IntSupplier; import static art.arcane.iris.util.misc.ServerProperties.BUKKIT_YML; @@ -114,8 +121,13 @@ public class IrisCreator { throw new IrisException("You cannot invoke create() on the main thread."); } - if (J.isFolia()) { - throw new IrisException("Folia does not support runtime world creation via Bukkit.createWorld(). Configure worlds before startup and restart the server."); + if (studio()) { + World existing = Bukkit.getWorld(name()); + if (existing == null) { + IO.delete(new File(Bukkit.getWorldContainer(), name())); + IO.delete(new File(Bukkit.getWorldContainer(), name() + "_nether")); + IO.delete(new File(Bukkit.getWorldContainer(), name() + "_the_end")); + } } IrisDimension d = IrisToolbelt.getDimension(dimension()); @@ -172,11 +184,16 @@ public class IrisCreator { World world; try { - world = J.sfut(() -> INMS.get().createWorld(wc)).get(); + world = J.sfut(() -> INMS.get().createWorldAsync(wc)) + .thenCompose(Function.identity()) + .get(); } catch (Throwable e) { done.set(true); - if (containsCreateWorldUnsupportedOperation(e)) { - throw new IrisException("Runtime world creation is not supported on this server variant. Configure worlds before startup and restart the server.", e); + 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("Failed to create world!", e); } @@ -184,11 +201,33 @@ public class IrisCreator { done.set(true); if (sender.isPlayer() && !benchmark) { - J.s(() -> sender.player().teleport(new Location(world, 0, world.getHighestBlockYAt(0, 0) + 1, 0))); + 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 { + CompletableFuture 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) { - J.s(() -> { + Runnable applyStudioWorldSettings = () -> { Iris.linkMultiverseCore.removeFromConfig(world); if (IrisSettings.get().getStudio().isDisableTimeAndWeather()) { @@ -196,7 +235,9 @@ public class IrisCreator { world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false); world.setTime(6000); } - }); + }; + + J.s(applyStudioWorldSettings); } else { addToBukkitYml(); J.s(() -> Iris.linkMultiverseCore.updateWorld(world, dimension)); @@ -233,6 +274,32 @@ public class IrisCreator { return world; } + private Location resolveStudioEntryLocation(World world) { + CompletableFuture locationFuture = J.sfut(() -> { + Location spawnLocation = world.getSpawnLocation(); + if (spawnLocation != null) { + return spawnLocation; + } + + 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 { + return locationFuture.get(15, TimeUnit.SECONDS); + } catch (Throwable e) { + Iris.warn("Failed to resolve studio entry location for world \"" + world.getName() + "\"."); + Iris.reportError(e); + return null; + } + } + private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) { Throwable cursor = throwable; while (cursor != null) { diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java index 893f95ff9..57a95f43d 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java @@ -24,6 +24,7 @@ import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod; import art.arcane.iris.core.pregenerator.methods.HybridPregenMethod; import art.arcane.iris.core.service.StudioSVC; @@ -32,7 +33,9 @@ import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.scheduling.J; import art.arcane.iris.util.plugin.VolmitSender; +import io.papermc.lib.PaperLib; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Player; import org.jetbrains.annotations.ApiStatus; @@ -41,6 +44,7 @@ import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -178,26 +182,29 @@ public class IrisToolbelt { * @return the IrisAccess or null if it's not an Iris World */ public static PlatformChunkGenerator access(World world) { + if (world == null) { + return null; + } + if (isIrisWorld(world)) { return ((PlatformChunkGenerator) world.getGenerator()); - } /*else { - Iris.warn(""" - "---------- No World? --------------- - ⠀⣞⢽⢪⢣⢣⢣⢫⡺⡵⣝⡮⣗⢷⢽⢽⢽⣮⡷⡽⣜⣜⢮⢺⣜⢷⢽⢝⡽⣝ - ⠸⡸⠜⠕⠕⠁⢁⢇⢏⢽⢺⣪⡳⡝⣎⣏⢯⢞⡿⣟⣷⣳⢯⡷⣽⢽⢯⣳⣫⠇ - ⠀⠀⢀⢀⢄⢬⢪⡪⡎⣆⡈⠚⠜⠕⠇⠗⠝⢕⢯⢫⣞⣯⣿⣻⡽⣏⢗⣗⠏⠀ - ⠀⠪⡪⡪⣪⢪⢺⢸⢢⢓⢆⢤⢀⠀⠀⠀⠀⠈⢊⢞⡾⣿⡯⣏⢮⠷⠁⠀⠀ - ⠀⠀⠀⠈⠊⠆⡃⠕⢕⢇⢇⢇⢇⢇⢏⢎⢎⢆⢄⠀⢑⣽⣿⢝⠲⠉⠀⠀⠀⠀ - ⠀⠀⠀⠀⠀⡿⠂⠠⠀⡇⢇⠕⢈⣀⠀⠁⠡⠣⡣⡫⣂⣿⠯⢪⠰⠂⠀⠀⠀⠀ - ⠀⠀⠀⠀⡦⡙⡂⢀⢤⢣⠣⡈⣾⡃⠠⠄⠀⡄⢱⣌⣶⢏⢊⠂⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⢝⡲⣜⡮⡏⢎⢌⢂⠙⠢⠐⢀⢘⢵⣽⣿⡿⠁⠁⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠨⣺⡺⡕⡕⡱⡑⡆⡕⡅⡕⡜⡼⢽⡻⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⣼⣳⣫⣾⣵⣗⡵⡱⡡⢣⢑⢕⢜⢕⡝⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⣴⣿⣾⣿⣿⣿⡿⡽⡑⢌⠪⡢⡣⣣⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⡟⡾⣿⢿⢿⢵⣽⣾⣼⣘⢸⢸⣞⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ⠀⠀⠀⠀⠁⠇⠡⠩⡫⢿⣝⡻⡮⣒⢽⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - """); - }*/ + } + + StudioSVC studioService = Iris.service(StudioSVC.class); + if (studioService != null && studioService.isProjectOpen()) { + IrisProject activeProject = studioService.getActiveProject(); + if (activeProject != null) { + PlatformChunkGenerator activeProvider = activeProject.getActiveProvider(); + if (activeProvider != null) { + World activeWorld = activeProvider.getTarget().getWorld().realWorld(); + if (activeWorld != null && activeWorld.getName().equals(world.getName())) { + activeProvider.touch(world); + return activeProvider; + } + } + } + } + return null; } @@ -264,7 +271,19 @@ public class IrisToolbelt { if (!i.getName().equals(world.getName())) { for (Player j : world.getPlayers()) { new VolmitSender(j, Iris.instance.getTag()).sendMessage("You have been evacuated from this world."); - j.teleport(i.getSpawnLocation()); + Location target = i.getSpawnLocation(); + Runnable teleportTask = () -> { + CompletableFuture teleportFuture = PaperLib.teleportAsync(j, target); + if (teleportFuture != null) { + teleportFuture.exceptionally(throwable -> { + Iris.reportError(throwable); + return false; + }); + } + }; + if (!J.runEntity(j, teleportTask)) { + teleportTask.run(); + } } return true; @@ -286,7 +305,19 @@ public class IrisToolbelt { if (!i.getName().equals(world.getName())) { for (Player j : world.getPlayers()) { new VolmitSender(j, Iris.instance.getTag()).sendMessage("You have been evacuated from this world. " + m); - j.teleport(i.getSpawnLocation()); + Location target = i.getSpawnLocation(); + Runnable teleportTask = () -> { + CompletableFuture teleportFuture = PaperLib.teleportAsync(j, target); + if (teleportFuture != null) { + teleportFuture.exceptionally(throwable -> { + Iris.reportError(throwable); + return false; + }); + } + }; + if (!J.runEntity(j, teleportTask)) { + teleportTask.run(); + } } return true; } diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index 4d078ffb6..cab2859bf 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -24,6 +24,7 @@ import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.data.cache.AtomicCache; import art.arcane.iris.engine.data.chunk.TerrainChunk; @@ -475,6 +476,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) { try { Engine engine = getEngine(world); + World realWorld = engine.getWorld().realWorld(); + if (realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld)) { + return; + } computeStudioGenerator(); TerrainChunk tc = TerrainChunk.create(d, new IrisBiomeStorage()); this.world.bind(world);