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