Create std

This commit is contained in:
Brian Neumann-Fopiano
2026-02-17 06:18:55 -05:00
parent cf64fbb730
commit 05d79b6d40
9 changed files with 987 additions and 53 deletions

View File

@@ -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<String, ChunkGenerator> stagedRuntimeGenerators = new ConcurrentHashMap<>();
private static final Map<String, BiomeProvider> 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> T service(Class<T> 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");

View File

@@ -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());
}

View File

@@ -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<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;
}
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<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 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<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;
}
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<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 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<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

@@ -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<World> createWorldAsync(WorldCreator c) {
try {
FoliaWorldsLink link = FoliaWorldsLink.get();
if (link.isActive()) {
CompletableFuture<World> 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);

View File

@@ -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<Void> 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;
}

View File

@@ -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());
}

View File

@@ -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<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) {
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<Location> 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) {

View File

@@ -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<Boolean> 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<Boolean> 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;
}

View File

@@ -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);