mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-06-18 14:50:57 +00:00
WIP Building for latest.
Still Quantizing, WOrking on that next also removed prebake
This commit is contained in:
+3
-12
@@ -77,20 +77,10 @@ nmsBindings.each { key, value ->
|
|||||||
def nmsConfig = new Config()
|
def nmsConfig = new Config()
|
||||||
nmsConfig.jvm = 25
|
nmsConfig.jvm = 25
|
||||||
nmsConfig.version = value
|
nmsConfig.version = value
|
||||||
nmsConfig.type = Enum.valueOf(nmsTypeClass, 'DIRECT')
|
nmsConfig.type = Enum.valueOf(nmsTypeClass, 'USER_DEV')
|
||||||
extensions.extraProperties.set('nms', nmsConfig)
|
extensions.extraProperties.set('nms', nmsConfig)
|
||||||
plugins.apply(NMSBinding)
|
plugins.apply(NMSBinding)
|
||||||
|
|
||||||
TaskProvider<Download> updateSpecialSource = tasks.register('updateSpecialSource', Download) {
|
|
||||||
src('https://repo1.maven.org/maven2/net/md-5/SpecialSource/1.11.6/SpecialSource-1.11.6-shaded.jar')
|
|
||||||
dest(layout.buildDirectory.file('tools/SpecialSource-1.11.4.jar'))
|
|
||||||
overwrite(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('remap').configure {
|
|
||||||
dependsOn(updateSpecialSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(':core'))
|
compileOnly(project(':core'))
|
||||||
compileOnly(volmLibCoordinate) {
|
compileOnly(volmLibCoordinate) {
|
||||||
@@ -107,7 +97,7 @@ def included = configurations.create('included')
|
|||||||
def jarJar = configurations.create('jarJar')
|
def jarJar = configurations.create('jarJar')
|
||||||
dependencies {
|
dependencies {
|
||||||
nmsBindings.keySet().each { key ->
|
nmsBindings.keySet().each { key ->
|
||||||
add('included', project(path: ":nms:${key}", configuration: 'reobf'))
|
add('included', project(path: ":nms:${key}", configuration: 'runtimeElements'))
|
||||||
}
|
}
|
||||||
add('included', project(path: ':core', configuration: 'shadow'))
|
add('included', project(path: ':core', configuration: 'shadow'))
|
||||||
add('jarJar', project(':core:agent'))
|
add('jarJar', project(':core:agent'))
|
||||||
@@ -195,6 +185,7 @@ allprojects {
|
|||||||
maven { url = uri('https://mvn.lumine.io/repository/maven-public/') } // mythic
|
maven { url = uri('https://mvn.lumine.io/repository/maven-public/') } // mythic
|
||||||
maven { url = uri('https://nexus.phoenixdevt.fr/repository/maven-public/') } //MMOItems
|
maven { url = uri('https://nexus.phoenixdevt.fr/repository/maven-public/') } //MMOItems
|
||||||
maven { url = uri('https://repo.onarandombox.com/content/groups/public/') } //Multiverse Core
|
maven { url = uri('https://repo.onarandombox.com/content/groups/public/') } //Multiverse Core
|
||||||
|
maven { url = uri('https://repo.momirealms.net/releases/') } // CraftEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.gradle.api.GradleException;
|
|||||||
import org.gradle.api.Named;
|
import org.gradle.api.Named;
|
||||||
import org.gradle.api.Plugin;
|
import org.gradle.api.Plugin;
|
||||||
import org.gradle.api.Project;
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.artifacts.Configuration;
|
||||||
import org.gradle.api.attributes.Bundling;
|
import org.gradle.api.attributes.Bundling;
|
||||||
import org.gradle.api.attributes.Category;
|
import org.gradle.api.attributes.Category;
|
||||||
import org.gradle.api.attributes.LibraryElements;
|
import org.gradle.api.attributes.LibraryElements;
|
||||||
@@ -89,19 +90,26 @@ public class NMSBinding implements Plugin<Project> {
|
|||||||
extension.getVersion().set(config.version);
|
extension.getVersion().set(config.version);
|
||||||
});
|
});
|
||||||
|
|
||||||
ObjectFactory objects = target.getObjects();
|
|
||||||
target.getConfigurations().register(REOBF_CONFIG, configuration -> {
|
|
||||||
configuration.setCanBeConsumed(true);
|
|
||||||
configuration.setCanBeResolved(false);
|
|
||||||
configuration.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, named(objects, Usage.class, Usage.JAVA_RUNTIME));
|
|
||||||
configuration.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, named(objects, Category.class, Category.LIBRARY));
|
|
||||||
configuration.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, named(objects, LibraryElements.class, LibraryElements.JAR));
|
|
||||||
configuration.getAttributes().attribute(Bundling.BUNDLING_ATTRIBUTE, named(objects, Bundling.class, Bundling.EXTERNAL));
|
|
||||||
configuration.getAttributes().attribute(Obfuscation.Companion.getOBFUSCATION_ATTRIBUTE(), named(objects, Obfuscation.class, Obfuscation.OBFUSCATED));
|
|
||||||
configuration.getOutgoing().artifact(target.getTasks().named("remap"));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String outgoingArtifactTask = type == Type.USER_DEV ? "jar" : "remap";
|
||||||
|
ObjectFactory objects = target.getObjects();
|
||||||
|
Configuration reobfConfiguration = target.getConfigurations().findByName(REOBF_CONFIG);
|
||||||
|
if (reobfConfiguration == null) {
|
||||||
|
reobfConfiguration = target.getConfigurations().create(REOBF_CONFIG);
|
||||||
|
}
|
||||||
|
|
||||||
|
target.getConfigurations().named(REOBF_CONFIG).configure(configuration -> {
|
||||||
|
configuration.setCanBeConsumed(true);
|
||||||
|
configuration.setCanBeResolved(false);
|
||||||
|
configuration.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, named(objects, Usage.class, Usage.JAVA_RUNTIME));
|
||||||
|
configuration.getAttributes().attribute(Category.CATEGORY_ATTRIBUTE, named(objects, Category.class, Category.LIBRARY));
|
||||||
|
configuration.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, named(objects, LibraryElements.class, LibraryElements.JAR));
|
||||||
|
configuration.getAttributes().attribute(Bundling.BUNDLING_ATTRIBUTE, named(objects, Bundling.class, Bundling.EXTERNAL));
|
||||||
|
configuration.getAttributes().attribute(Obfuscation.Companion.getOBFUSCATION_ATTRIBUTE(), named(objects, Obfuscation.class, Obfuscation.OBFUSCATED));
|
||||||
|
configuration.getOutgoing().artifact(target.getTasks().named(outgoingArtifactTask));
|
||||||
|
});
|
||||||
|
|
||||||
int[] version = parseVersion(config.version);
|
int[] version = parseVersion(config.version);
|
||||||
int major = version[0];
|
int major = version[0];
|
||||||
int minor = version[1];
|
int minor = version[1];
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ dependencies {
|
|||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
compileOnly(libs.multiverseCore)
|
compileOnly(libs.multiverseCore)
|
||||||
|
compileOnly(libs.craftengine.core)
|
||||||
|
compileOnly(libs.craftengine.bukkit)
|
||||||
|
|
||||||
// Shaded
|
// Shaded
|
||||||
implementation('de.crazydev22.slimjar.helper:spigot:2.1.9')
|
implementation('de.crazydev22.slimjar.helper:spigot:2.1.9')
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
2117487583
|
466077434
|
||||||
@@ -24,6 +24,10 @@ import com.google.gson.JsonParser;
|
|||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.IrisWorlds;
|
import art.arcane.iris.core.IrisWorlds;
|
||||||
import art.arcane.iris.core.ServerConfigurator;
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
|
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||||
|
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
|
||||||
import art.arcane.iris.core.link.IrisPapiExpansion;
|
import art.arcane.iris.core.link.IrisPapiExpansion;
|
||||||
import art.arcane.iris.core.link.MultiverseCoreLink;
|
import art.arcane.iris.core.link.MultiverseCoreLink;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
@@ -78,7 +82,6 @@ import java.io.*;
|
|||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -98,9 +101,6 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
private static File settingsFile;
|
private static File settingsFile;
|
||||||
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
|
private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt";
|
||||||
private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance();
|
private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance();
|
||||||
private static final Map<String, ChunkGenerator> stagedRuntimeGenerators = new ConcurrentHashMap<>();
|
|
||||||
private static final Map<String, BiomeProvider> stagedRuntimeBiomeProviders = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
InstanceState.updateInstanceId();
|
InstanceState.updateInstanceId();
|
||||||
@@ -122,40 +122,30 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
return sender;
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
public static <T> T service(Class<T> c) {
|
public static <T> T service(Class<T> c) {
|
||||||
return (T) instance.services.get(c);
|
return (T) instance.services.get(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void callEvent(Event e) {
|
public static void callEvent(Event e) {
|
||||||
|
Runnable dispatcher = () -> {
|
||||||
|
try {
|
||||||
|
Bukkit.getPluginManager().callEvent(e);
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
reportError("Event dispatch failed for \"" + e.getEventName() + "\".", ex);
|
||||||
|
if (ex instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
if (ex instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
if (!e.isAsynchronous()) {
|
if (!e.isAsynchronous()) {
|
||||||
J.s(() -> Bukkit.getPluginManager().callEvent(e));
|
J.s(dispatcher);
|
||||||
} else {
|
} else {
|
||||||
Bukkit.getPluginManager().callEvent(e);
|
dispatcher.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,8 +433,22 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void reportError(Throwable e) {
|
public static void reportError(Throwable e) {
|
||||||
|
if (e == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Bindings.capture(e);
|
Bindings.capture(e);
|
||||||
if (IrisSettings.get().getGeneral().isDebug()) {
|
boolean debug = false;
|
||||||
|
if (instance != null) {
|
||||||
|
try {
|
||||||
|
IrisSettings currentSettings = IrisSettings.settings != null ? IrisSettings.settings : IrisSettings.get();
|
||||||
|
debug = currentSettings != null && currentSettings.getGeneral().isDebug();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
debug = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
||||||
|
|
||||||
if (e.getCause() != null) {
|
if (e.getCause() != null) {
|
||||||
@@ -467,6 +471,25 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void reportError(String context, Throwable e) {
|
||||||
|
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
|
||||||
|
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (instance != null) {
|
||||||
|
Iris.error(message);
|
||||||
|
} else {
|
||||||
|
System.err.println("[Iris] " + message);
|
||||||
|
}
|
||||||
|
} catch (Throwable inner) {
|
||||||
|
System.err.println("[Iris] " + message);
|
||||||
|
inner.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
|
||||||
|
reportError(error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
|
||||||
public static void dump() {
|
public static void dump() {
|
||||||
try {
|
try {
|
||||||
File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt");
|
File fi = Iris.instance.getDataFile("dump", "td-" + new java.sql.Date(M.ms()) + ".txt");
|
||||||
@@ -533,6 +556,8 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
addShutdownHook();
|
addShutdownHook();
|
||||||
processPendingStartupWorldDeletes();
|
processPendingStartupWorldDeletes();
|
||||||
IrisToolbelt.applyPregenPerformanceProfile();
|
IrisToolbelt.applyPregenPerformanceProfile();
|
||||||
|
WorldLifecycleService.get();
|
||||||
|
WorldRuntimeControlService.get();
|
||||||
|
|
||||||
if (J.isFolia()) {
|
if (J.isFolia()) {
|
||||||
checkForBukkitWorlds(s -> true);
|
checkForBukkitWorlds(s -> true);
|
||||||
@@ -614,11 +639,10 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
Iris.error("Failed to load world " + s + "!");
|
Iris.error("Failed to load world " + s + "!");
|
||||||
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
|
Iris.error("This server denied Bukkit.createWorld for \"" + s + "\" at the current startup phase.");
|
||||||
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
|
Iris.error("Ensure Iris is loaded at STARTUP and restart after staging worlds in bukkit.yml.");
|
||||||
reportError(e);
|
reportError("Failed to load staged startup world \"" + s + "\".", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Iris.error("Failed to load world " + s + "!");
|
reportError("Failed to load startup world \"" + s + "\".", e);
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!deferredStartupWorlds.isEmpty()) {
|
if (!deferredStartupWorlds.isEmpty()) {
|
||||||
@@ -626,8 +650,7 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
Iris.warn("Bukkit.createWorld is intentionally unavailable in this startup phase. Worlds remain staged in bukkit.yml.");
|
Iris.warn("Bukkit.createWorld is intentionally unavailable in this startup phase. Worlds remain staged in bukkit.yml.");
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
e.printStackTrace();
|
reportError("Failed while loading startup Iris worlds.", e);
|
||||||
reportError(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,6 +696,9 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
private void processPendingStartupWorldDeletes() {
|
private void processPendingStartupWorldDeletes() {
|
||||||
try {
|
try {
|
||||||
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
|
LinkedHashMap<String, String> queue = loadPendingWorldDeleteMap();
|
||||||
|
for (String transientStudioWorld : TransientWorldCleanupSupport.collectTransientStudioWorldNames(Bukkit.getWorldContainer())) {
|
||||||
|
queue.putIfAbsent(transientStudioWorld.toLowerCase(Locale.ROOT), transientStudioWorld);
|
||||||
|
}
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -690,20 +716,33 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
|
boolean foundAny = false;
|
||||||
if (!worldFolder.exists()) {
|
boolean deletedAll = true;
|
||||||
|
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
|
||||||
|
File worldFolder = new File(Bukkit.getWorldContainer(), familyWorldName);
|
||||||
|
if (!worldFolder.exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundAny = true;
|
||||||
|
IO.delete(worldFolder);
|
||||||
|
if (worldFolder.exists()) {
|
||||||
|
deletedAll = false;
|
||||||
|
Iris.warn("Failed to delete queued world folder \"" + familyWorldName + "\". Retrying on next startup.");
|
||||||
|
} else {
|
||||||
|
Iris.info("Deleted queued world folder \"" + familyWorldName + "\".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundAny) {
|
||||||
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
|
Iris.info("Queued world deletion skipped for \"" + worldName + "\" (folder missing).");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
IO.delete(worldFolder);
|
if (!deletedAll) {
|
||||||
if (worldFolder.exists()) {
|
|
||||||
Iris.warn("Failed to delete queued world folder \"" + worldName + "\". Retrying on next startup.");
|
|
||||||
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
|
remaining.put(worldName.toLowerCase(Locale.ROOT), worldName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Iris.info("Deleted queued world folder \"" + worldName + "\".");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writePendingWorldDeleteMap(remaining);
|
writePendingWorldDeleteMap(remaining);
|
||||||
@@ -952,7 +991,7 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
|
public BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
|
||||||
BiomeProvider stagedBiomeProvider = consumeRuntimeBiomeProvider(worldName);
|
org.bukkit.generator.BiomeProvider stagedBiomeProvider = WorldLifecycleStaging.consumeBiomeProvider(worldName);
|
||||||
if (stagedBiomeProvider != null) {
|
if (stagedBiomeProvider != null) {
|
||||||
Iris.debug("Using staged runtime biome provider for " + worldName);
|
Iris.debug("Using staged runtime biome provider for " + worldName);
|
||||||
return stagedBiomeProvider;
|
return stagedBiomeProvider;
|
||||||
@@ -963,7 +1002,7 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
|
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
|
||||||
ChunkGenerator stagedGenerator = consumeRuntimeWorldGenerator(worldName);
|
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
|
||||||
if (stagedGenerator != null) {
|
if (stagedGenerator != null) {
|
||||||
Iris.debug("Using staged runtime generator for " + worldName);
|
Iris.debug("Using staged runtime generator for " + worldName);
|
||||||
return stagedGenerator;
|
return stagedGenerator;
|
||||||
@@ -1029,27 +1068,81 @@ public class Iris extends VolmitPlugin implements Listener {
|
|||||||
|
|
||||||
private void printPacks() {
|
private void printPacks() {
|
||||||
File packFolder = Iris.service(StudioSVC.class).getWorkspaceFolder();
|
File packFolder = Iris.service(StudioSVC.class).getWorkspaceFolder();
|
||||||
File[] packs = packFolder.listFiles(File::isDirectory);
|
List<SplashPackMetadata> packs = collectSplashPacks(packFolder);
|
||||||
if (packs == null || packs.length == 0)
|
if (packs.isEmpty())
|
||||||
return;
|
return;
|
||||||
Iris.info("Custom Dimensions: " + packs.length);
|
Iris.info("Custom Dimensions: " + packs.size());
|
||||||
for (File f : packs)
|
for (SplashPackMetadata pack : packs) {
|
||||||
printPack(f);
|
printPack(pack);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printPack(File pack) {
|
static List<SplashPackMetadata> collectSplashPacks(File packFolder) {
|
||||||
String dimName = pack.getName();
|
if (packFolder == null || !packFolder.isDirectory()) {
|
||||||
String version = "???";
|
return Collections.emptyList();
|
||||||
try (FileReader r = new FileReader(new File(pack, "dimensions/" + dimName + ".json"))) {
|
}
|
||||||
JsonObject json = JsonParser.parseReader(r).getAsJsonObject();
|
|
||||||
if (json.has("version"))
|
File[] folders = packFolder.listFiles(File::isDirectory);
|
||||||
version = json.get("version").getAsString();
|
if (folders == null || folders.length == 0) {
|
||||||
} catch (IOException | JsonParseException ex) {
|
return Collections.emptyList();
|
||||||
Iris.verbose("Failed to read dimension version metadata for " + dimName + ": "
|
}
|
||||||
+ ex.getClass().getSimpleName()
|
|
||||||
+ (ex.getMessage() == null ? "" : " - " + ex.getMessage()));
|
List<SplashPackMetadata> packs = new ArrayList<>();
|
||||||
|
for (File folder : folders) {
|
||||||
|
SplashPackMetadata metadata = readSplashPack(folder);
|
||||||
|
if (metadata != null) {
|
||||||
|
packs.add(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packs.sort(Comparator.comparing(SplashPackMetadata::name));
|
||||||
|
return packs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SplashPackMetadata readSplashPack(File pack) {
|
||||||
|
if (pack == null || !pack.isDirectory()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String dimName = pack.getName();
|
||||||
|
File dimensionFile = new File(pack, "dimensions/" + dimName + ".json");
|
||||||
|
if (!dimensionFile.isFile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileReader r = new FileReader(dimensionFile)) {
|
||||||
|
JsonObject json = JsonParser.parseReader(r).getAsJsonObject();
|
||||||
|
if (!json.has("version")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SplashPackMetadata(dimName, json.get("version").getAsString());
|
||||||
|
} catch (IOException | JsonParseException ex) {
|
||||||
|
reportError("Failed to read splash metadata for dimension pack \"" + dimName + "\".", ex);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printPack(SplashPackMetadata pack) {
|
||||||
|
Iris.info(" " + pack.name() + " v" + pack.version());
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class SplashPackMetadata {
|
||||||
|
private final String name;
|
||||||
|
private final String version;
|
||||||
|
|
||||||
|
SplashPackMetadata(String name, String version) {
|
||||||
|
this.name = name;
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
String version() {
|
||||||
|
return version;
|
||||||
}
|
}
|
||||||
Iris.info(" " + dimName + " v" + version);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getIrisVersion() {
|
public int getIrisVersion() {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public enum IrisRuntimeSchedulerMode {
|
|||||||
if (containsIgnoreCase(bukkitName, "purpur")
|
if (containsIgnoreCase(bukkitName, "purpur")
|
||||||
|| containsIgnoreCase(bukkitVersion, "purpur")
|
|| containsIgnoreCase(bukkitVersion, "purpur")
|
||||||
|| containsIgnoreCase(serverClassName, "purpur")
|
|| containsIgnoreCase(serverClassName, "purpur")
|
||||||
|
|| containsIgnoreCase(bukkitName, "canvas")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "canvas")
|
||||||
|
|| containsIgnoreCase(serverClassName, "canvas")
|
||||||
|| containsIgnoreCase(bukkitName, "paper")
|
|| containsIgnoreCase(bukkitName, "paper")
|
||||||
|| containsIgnoreCase(bukkitVersion, "paper")
|
|| containsIgnoreCase(bukkitVersion, "paper")
|
||||||
|| containsIgnoreCase(serverClassName, "paper")
|
|| containsIgnoreCase(serverClassName, "paper")
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ public class IrisSettings {
|
|||||||
public int chunkLoadTimeoutSeconds = 15;
|
public int chunkLoadTimeoutSeconds = 15;
|
||||||
public int timeoutWarnIntervalMs = 500;
|
public int timeoutWarnIntervalMs = 500;
|
||||||
public int saveIntervalMs = 120_000;
|
public int saveIntervalMs = 120_000;
|
||||||
public boolean startupNoisemapPrebake = true;
|
|
||||||
public boolean enablePregenPerformanceProfile = true;
|
public boolean enablePregenPerformanceProfile = true;
|
||||||
public int pregenProfileNoiseCacheSize = 4_096;
|
public int pregenProfileNoiseCacheSize = 4_096;
|
||||||
public boolean pregenProfileEnableFastCache = true;
|
public boolean pregenProfileEnableFastCache = true;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import art.arcane.iris.core.loader.ResourceLoader;
|
|||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||||
import art.arcane.iris.engine.IrisNoisemapPrebakePipeline;
|
|
||||||
import art.arcane.iris.engine.object.*;
|
import art.arcane.iris.engine.object.*;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
@@ -77,10 +76,7 @@ public class ServerConfigurator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deferredInstallPending = false;
|
deferredInstallPending = false;
|
||||||
boolean datapacksMissing = installDataPacks(true);
|
installDataPacks(true);
|
||||||
if (!datapacksMissing) {
|
|
||||||
IrisNoisemapPrebakePipeline.scheduleInstalledPacksPrebakeAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void configureIfDeferred() {
|
public static void configureIfDeferred() {
|
||||||
@@ -129,8 +125,15 @@ public class ServerConfigurator {
|
|||||||
return new KList<File>().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks"));
|
return new KList<File>().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks"));
|
||||||
}
|
}
|
||||||
KList<File> worlds = new KList<>();
|
KList<File> worlds = new KList<>();
|
||||||
Bukkit.getServer().getWorlds().forEach(w -> worlds.add(new File(w.getWorldFolder(), "datapacks")));
|
Bukkit.getServer().getWorlds().forEach(w -> {
|
||||||
if (worlds.isEmpty()) worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks"));
|
File folder = resolveDatapacksFolder(w.getWorldFolder());
|
||||||
|
if (!worlds.contains(folder)) {
|
||||||
|
worlds.add(folder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (worlds.isEmpty()) {
|
||||||
|
worlds.add(new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME + "/datapacks"));
|
||||||
|
}
|
||||||
return worlds;
|
return worlds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,9 +183,10 @@ public class ServerConfigurator {
|
|||||||
Iris.verbose("Checking Data Packs...");
|
Iris.verbose("Checking Data Packs...");
|
||||||
}
|
}
|
||||||
DimensionHeight height = new DimensionHeight(fixer);
|
DimensionHeight height = new DimensionHeight(fixer);
|
||||||
KList<File> folders = getDatapacksFolder();
|
KList<File> baseFolders = getDatapacksFolder();
|
||||||
|
KList<File> folders = collectInstallDatapackFolders(baseFolders, extraWorldDatapackFoldersByPack);
|
||||||
if (includeExternal) {
|
if (includeExternal) {
|
||||||
installExternalDataPacks(folders, extraWorldDatapackFoldersByPack);
|
installExternalDataPacks(baseFolders, extraWorldDatapackFoldersByPack);
|
||||||
}
|
}
|
||||||
KMap<String, KSet<String>> biomes = new KMap<>();
|
KMap<String, KSet<String>> biomes = new KMap<>();
|
||||||
|
|
||||||
@@ -205,6 +209,34 @@ public class ServerConfigurator {
|
|||||||
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
|
return fullInstall && verifyDataPacksPost(IrisSettings.get().getAutoConfiguration().isAutoRestartOnCustomBiomeInstall());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static KList<File> collectInstallDatapackFolders(
|
||||||
|
KList<File> baseFolders,
|
||||||
|
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
|
||||||
|
) {
|
||||||
|
KList<File> folders = new KList<>();
|
||||||
|
if (baseFolders != null) {
|
||||||
|
for (File folder : baseFolders) {
|
||||||
|
if (folder != null && !folders.contains(folder)) {
|
||||||
|
folders.add(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (extraWorldDatapackFoldersByPack == null || extraWorldDatapackFoldersByPack.isEmpty()) {
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
for (KList<File> extraFolders : extraWorldDatapackFoldersByPack.values()) {
|
||||||
|
if (extraFolders == null || extraFolders.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (File folder : extraFolders) {
|
||||||
|
if (folder != null && !folders.contains(folder)) {
|
||||||
|
folders.add(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
|
||||||
private static void installExternalDataPacks(
|
private static void installExternalDataPacks(
|
||||||
KList<File> folders,
|
KList<File> folders,
|
||||||
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
|
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
|
||||||
@@ -915,7 +947,10 @@ public class ServerConfigurator {
|
|||||||
if (packName.isBlank()) {
|
if (packName.isBlank()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
File datapacksFolder = new File(Bukkit.getWorldContainer(), worldName + File.separator + "datapacks");
|
org.bukkit.World world = Bukkit.getWorld(worldName);
|
||||||
|
File datapacksFolder = world == null
|
||||||
|
? new File(Bukkit.getWorldContainer(), worldName + File.separator + "datapacks")
|
||||||
|
: resolveDatapacksFolder(world.getWorldFolder());
|
||||||
addWorldDatapackFolder(foldersByPack, packName, datapacksFolder);
|
addWorldDatapackFolder(foldersByPack, packName, datapacksFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,7 +964,7 @@ public class ServerConfigurator {
|
|||||||
if (packName.isBlank()) {
|
if (packName.isBlank()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
File datapacksFolder = new File(world.getWorldFolder(), "datapacks");
|
File datapacksFolder = resolveDatapacksFolder(world.getWorldFolder());
|
||||||
addWorldDatapackFolder(foldersByPack, packName, datapacksFolder);
|
addWorldDatapackFolder(foldersByPack, packName, datapacksFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -969,6 +1004,31 @@ public class ServerConfigurator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static File resolveDatapacksFolder(File worldFolder) {
|
||||||
|
File rootFolder = resolveWorldRootFolder(worldFolder);
|
||||||
|
return new File(rootFolder, "datapacks");
|
||||||
|
}
|
||||||
|
|
||||||
|
static File resolveWorldRootFolder(File worldFolder) {
|
||||||
|
if (worldFolder == null) {
|
||||||
|
return new File(Bukkit.getWorldContainer(), ServerProperties.LEVEL_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
File current = worldFolder.getAbsoluteFile();
|
||||||
|
while (current != null) {
|
||||||
|
if ("dimensions".equals(current.getName())) {
|
||||||
|
File parent = current.getParentFile();
|
||||||
|
if (parent != null) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = current.getParentFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
return worldFolder.getAbsoluteFile();
|
||||||
|
}
|
||||||
|
|
||||||
private static String sanitizePackName(String value) {
|
private static String sanitizePackName(String value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
|||||||
private static final Set<String> ACTIVE_DELETE_CHUNK_WORLDS = ConcurrentHashMap.newKeySet();
|
private static final Set<String> ACTIVE_DELETE_CHUNK_WORLDS = ConcurrentHashMap.newKeySet();
|
||||||
private CommandTurboPregen turboPregen;
|
private CommandTurboPregen turboPregen;
|
||||||
private CommandLazyPregen lazyPregen;
|
private CommandLazyPregen lazyPregen;
|
||||||
|
private CommandSmoke smoke;
|
||||||
|
|
||||||
@Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true)
|
@Director(description = "Get Loaded TectonicPlates Count", origin = DirectorOrigin.BOTH, sync = true)
|
||||||
public void EngineStatus() {
|
public void EngineStatus() {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import art.arcane.iris.Iris;
|
|||||||
import art.arcane.iris.core.ExternalDataPackPipeline;
|
import art.arcane.iris.core.ExternalDataPackPipeline;
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.IrisWorlds;
|
import art.arcane.iris.core.IrisWorlds;
|
||||||
import art.arcane.iris.core.link.FoliaWorldsLink;
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
@@ -196,8 +196,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
|
sender().sendMessage(C.RED + "Exception raised during creation. See the console for more details.");
|
||||||
Iris.error("Exception raised during world creation: " + e.getMessage());
|
Iris.reportError("Exception raised during world creation for \"" + name + "\".", e);
|
||||||
Iris.reportError(e);
|
|
||||||
worldCreation = false;
|
worldCreation = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -742,7 +741,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FoliaWorldsLink.get().unloadWorld(world, false)) {
|
if (!WorldLifecycleService.get().unload(world, false)) {
|
||||||
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
|
sender().sendMessage(C.RED + "Failed to unload world: " + world.getName());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -755,7 +754,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
|
sender().sendMessage(C.RED + "Failed to save bukkit.yml because of " + e.getMessage());
|
||||||
e.printStackTrace();
|
Iris.reportError("Failed to remove world \"" + world.getName() + "\" from bukkit.yml.", e);
|
||||||
}
|
}
|
||||||
IrisToolbelt.evacuate(world, "Deleting world");
|
IrisToolbelt.evacuate(world, "Deleting world");
|
||||||
deletingWorld = true;
|
deletingWorld = true;
|
||||||
@@ -2144,7 +2143,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
|
sender().sendMessage(C.GREEN + "Unloading world: " + world.getName());
|
||||||
try {
|
try {
|
||||||
IrisToolbelt.evacuate(world);
|
IrisToolbelt.evacuate(world);
|
||||||
boolean unloaded = FoliaWorldsLink.get().unloadWorld(world, false);
|
boolean unloaded = WorldLifecycleService.get().unload(world, false);
|
||||||
if (unloaded) {
|
if (unloaded) {
|
||||||
sender().sendMessage(C.GREEN + "World unloaded successfully.");
|
sender().sendMessage(C.GREEN + "World unloaded successfully.");
|
||||||
} else {
|
} else {
|
||||||
@@ -2152,7 +2151,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
|
sender().sendMessage(C.RED + "Failed to unload the world: " + e.getMessage());
|
||||||
e.printStackTrace();
|
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package art.arcane.iris.core.commands;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.runtime.DatapackReadinessResult;
|
||||||
|
import art.arcane.iris.core.runtime.SmokeDiagnosticsService;
|
||||||
|
import art.arcane.iris.core.runtime.SmokeTestService;
|
||||||
|
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||||
|
import art.arcane.iris.util.common.format.C;
|
||||||
|
import art.arcane.volmlib.util.director.annotations.Director;
|
||||||
|
import art.arcane.volmlib.util.director.annotations.Param;
|
||||||
|
import art.arcane.volmlib.util.format.Form;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Director(name = "smoke", description = "Run Iris developer smoke diagnostics")
|
||||||
|
public class CommandSmoke implements DirectorExecutor {
|
||||||
|
@Director(description = "Run the full smoke suite", sync = true)
|
||||||
|
public void full(
|
||||||
|
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
|
||||||
|
String dimension,
|
||||||
|
@Param(description = "The seed to use", defaultValue = "1337")
|
||||||
|
long seed,
|
||||||
|
@Param(description = "Optional player validation target or none", defaultValue = "none")
|
||||||
|
String player,
|
||||||
|
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
|
||||||
|
boolean retainOnFailure
|
||||||
|
) {
|
||||||
|
String runId = SmokeTestService.get().startFullSmoke(sender(), dimension, seed, player, retainOnFailure);
|
||||||
|
announceRun(runId, "full");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Director(description = "Run the studio smoke flow", sync = true)
|
||||||
|
public void studio(
|
||||||
|
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
|
||||||
|
String dimension,
|
||||||
|
@Param(description = "The seed to use", defaultValue = "1337")
|
||||||
|
long seed,
|
||||||
|
@Param(description = "Optional player validation target or none", defaultValue = "none")
|
||||||
|
String player,
|
||||||
|
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
|
||||||
|
boolean retainOnFailure
|
||||||
|
) {
|
||||||
|
String runId = SmokeTestService.get().startStudioSmoke(sender(), dimension, seed, player, retainOnFailure);
|
||||||
|
announceRun(runId, "studio");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Director(description = "Run the create/unload smoke flow", sync = true)
|
||||||
|
public void create(
|
||||||
|
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
|
||||||
|
String dimension,
|
||||||
|
@Param(description = "The seed to use", defaultValue = "1337")
|
||||||
|
long seed,
|
||||||
|
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
|
||||||
|
boolean retainOnFailure
|
||||||
|
) {
|
||||||
|
String runId = SmokeTestService.get().startCreateSmoke(sender(), dimension, seed, retainOnFailure);
|
||||||
|
announceRun(runId, "create");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Director(description = "Run the benchmark create/unload smoke flow", sync = true)
|
||||||
|
public void benchmark(
|
||||||
|
@Param(name = "dimension", aliases = {"pack"}, description = "The dimension/pack key to validate")
|
||||||
|
String dimension,
|
||||||
|
@Param(description = "The seed to use", defaultValue = "1337")
|
||||||
|
long seed,
|
||||||
|
@Param(name = "retain-on-failure", aliases = {"retain"}, description = "Retain the temp world after failure", defaultValue = "false")
|
||||||
|
boolean retainOnFailure
|
||||||
|
) {
|
||||||
|
String runId = SmokeTestService.get().startBenchmarkSmoke(sender(), dimension, seed, retainOnFailure);
|
||||||
|
announceRun(runId, "benchmark");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Director(description = "Show live or persisted smoke status", sync = true)
|
||||||
|
public void status(
|
||||||
|
@Param(description = "Use latest or a specific run id", defaultValue = "latest")
|
||||||
|
String run
|
||||||
|
) {
|
||||||
|
SmokeDiagnosticsService.SmokeRunReport report = resolveReport(run);
|
||||||
|
if (report == null) {
|
||||||
|
sender().sendMessage(C.RED + "No smoke report found for \"" + run + "\".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendReport(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Director(description = "Inspect a currently loaded smoke/studio world", sync = true)
|
||||||
|
public void inspect(
|
||||||
|
@Param(description = "The loaded world name to inspect")
|
||||||
|
String world
|
||||||
|
) {
|
||||||
|
SmokeTestService.WorldInspection inspection = SmokeTestService.get().inspectWorld(world);
|
||||||
|
if (inspection == null) {
|
||||||
|
sender().sendMessage(C.RED + "World \"" + world + "\" is not currently loaded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender().sendMessage(C.GREEN + "Smoke inspection for " + C.GOLD + inspection.worldName());
|
||||||
|
sender().sendMessage(C.GRAY + "Lifecycle backend: " + C.WHITE + inspection.lifecycleBackend());
|
||||||
|
sender().sendMessage(C.GRAY + "Runtime backend: " + C.WHITE + inspection.runtimeBackend());
|
||||||
|
sender().sendMessage(C.GRAY + "Studio: " + C.WHITE + inspection.studio() + C.GRAY + " | Maintenance active: " + C.WHITE + inspection.maintenanceActive());
|
||||||
|
sender().sendMessage(C.GRAY + "Engine closed: " + C.WHITE + inspection.engineClosed() + C.GRAY + " | Engine failing: " + C.WHITE + inspection.engineFailing());
|
||||||
|
sender().sendMessage(C.GRAY + "Generation session: " + C.WHITE + inspection.generationSessionId() + C.GRAY + " | Active leases: " + C.WHITE + inspection.activeLeaseCount());
|
||||||
|
sender().sendMessage(C.GRAY + "Datapack folders: " + C.WHITE + joinList(inspection.datapackFolders()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void announceRun(String runId, String mode) {
|
||||||
|
sender().sendMessage(C.GREEN + "Started " + C.GOLD + mode + C.GREEN + " smoke run " + C.GOLD + runId + C.GREEN + ".");
|
||||||
|
sender().sendMessage(C.GREEN + "Use " + C.GOLD + "/iris developer smoke status run=" + runId + C.GREEN + " to monitor progress.");
|
||||||
|
sender().sendMessage(C.GREEN + "Latest report: " + C.GOLD + latestReportPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SmokeDiagnosticsService.SmokeRunReport resolveReport(String run) {
|
||||||
|
if (run == null || run.isBlank() || run.equalsIgnoreCase("latest")) {
|
||||||
|
return SmokeTestService.get().latest();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SmokeTestService.get().get(run);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendReport(SmokeDiagnosticsService.SmokeRunReport report) {
|
||||||
|
String elapsed = Form.duration(Math.max(0L, report.getElapsedMs()), 0);
|
||||||
|
sender().sendMessage(C.GREEN + "Smoke run " + C.GOLD + report.getRunId() + C.GREEN + " (" + C.GOLD + report.getMode() + C.GREEN + ")");
|
||||||
|
sender().sendMessage(C.GRAY + "World: " + C.WHITE + fallback(report.getWorldName()) + C.GRAY + " | Outcome: " + C.WHITE + fallback(report.getOutcome()));
|
||||||
|
sender().sendMessage(C.GRAY + "Stage: " + C.WHITE + fallback(report.getStage()) + C.GRAY + " | Elapsed: " + C.WHITE + elapsed);
|
||||||
|
if (report.getStageDetail() != null && !report.getStageDetail().isBlank()) {
|
||||||
|
sender().sendMessage(C.GRAY + "Stage detail: " + C.WHITE + report.getStageDetail());
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GRAY + "Lifecycle backend: " + C.WHITE + fallback(report.getLifecycleBackend()));
|
||||||
|
sender().sendMessage(C.GRAY + "Runtime backend: " + C.WHITE + fallback(report.getRuntimeBackend()));
|
||||||
|
sender().sendMessage(C.GRAY + "Generation session: " + C.WHITE + report.getGenerationSessionId() + C.GRAY + " | Active leases: " + C.WHITE + report.getGenerationActiveLeases());
|
||||||
|
if (report.getEntryChunkX() != null && report.getEntryChunkZ() != null) {
|
||||||
|
sender().sendMessage(C.GRAY + "Entry chunk: " + C.WHITE + report.getEntryChunkX() + "," + report.getEntryChunkZ());
|
||||||
|
}
|
||||||
|
sender().sendMessage(C.GRAY + "Headless: " + C.WHITE + report.isHeadless() + C.GRAY + " | Player: " + C.WHITE + fallback(report.getPlayerName()));
|
||||||
|
sender().sendMessage(C.GRAY + "Retain on failure: " + C.WHITE + report.isRetainOnFailure() + C.GRAY + " | Cleanup applied: " + C.WHITE + report.isCleanupApplied());
|
||||||
|
sendDatapackReadiness(report.getDatapackReadiness());
|
||||||
|
if (!report.getNotes().isEmpty()) {
|
||||||
|
sender().sendMessage(C.GRAY + "Notes: " + C.WHITE + joinList(report.getNotes()));
|
||||||
|
}
|
||||||
|
if (report.getFailureType() != null && !report.getFailureType().isBlank()) {
|
||||||
|
sender().sendMessage(C.RED + "Failure: " + report.getFailureType() + C.GRAY + " - " + C.WHITE + fallback(report.getFailureMessage()));
|
||||||
|
if (!report.getFailureChain().isEmpty()) {
|
||||||
|
sender().sendMessage(C.RED + "Failure chain: " + C.WHITE + joinList(report.getFailureChain()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendDatapackReadiness(DatapackReadinessResult readiness) {
|
||||||
|
if (readiness == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender().sendMessage(C.GRAY + "Datapack pack key: " + C.WHITE + fallback(readiness.getRequestedPackKey()));
|
||||||
|
sender().sendMessage(C.GRAY + "Datapack folders: " + C.WHITE + joinList(readiness.getResolvedDatapackFolders()));
|
||||||
|
sender().sendMessage(C.GRAY + "External datapack result: " + C.WHITE + fallback(readiness.getExternalDatapackInstallResult()));
|
||||||
|
sender().sendMessage(C.GRAY + "Verification passed: " + C.WHITE + readiness.isVerificationPassed() + C.GRAY + " | Restart required: " + C.WHITE + readiness.isRestartRequired());
|
||||||
|
if (!readiness.getMissingPaths().isEmpty()) {
|
||||||
|
sender().sendMessage(C.RED + "Missing datapack paths: " + C.WHITE + joinList(readiness.getMissingPaths()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String latestReportPath() {
|
||||||
|
if (Iris.instance == null) {
|
||||||
|
return "plugins/Iris/diagnostics/smoke/latest.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Iris.instance.getDataFile("diagnostics", "smoke", "latest.json").getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinList(List<String> values) {
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.join(", ", values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fallback(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,9 +26,7 @@ import art.arcane.iris.core.loader.IrisData;
|
|||||||
import art.arcane.iris.core.project.IrisProject;
|
import art.arcane.iris.core.project.IrisProject;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.IrisNoisemapPrebakePipeline;
|
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.SeedManager;
|
|
||||||
import art.arcane.iris.engine.object.*;
|
import art.arcane.iris.engine.object.*;
|
||||||
import art.arcane.iris.engine.platform.ChunkReplacementListener;
|
import art.arcane.iris.engine.platform.ChunkReplacementListener;
|
||||||
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
|
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
|
||||||
@@ -141,8 +139,25 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Iris.service(StudioSVC.class).close();
|
sender().sendMessage(C.YELLOW + "Closing studio...");
|
||||||
sender().sendMessage(C.GREEN + "Project Closed.");
|
Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
sender().sendMessage(C.RED + "Studio close failed: " + throwable.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null && result.failureCause() != null) {
|
||||||
|
sender().sendMessage(C.RED + "Studio close failed: " + result.failureCause().getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null && result.startupCleanupQueued()) {
|
||||||
|
sender().sendMessage(C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender().sendMessage(C.GREEN + "Studio closed.");
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Create a new studio project", aliases = "+", sync = true)
|
@Director(description = "Create a new studio project", aliases = "+", sync = true)
|
||||||
@@ -455,17 +470,13 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
IrisData data = IrisData.get(pack);
|
IrisData data = IrisData.get(pack);
|
||||||
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
|
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
|
||||||
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
|
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
|
||||||
long profileSeed = IrisNoisemapPrebakePipeline.dynamicStartupSeed();
|
|
||||||
|
|
||||||
if (activeEngine != null) {
|
if (activeEngine != null) {
|
||||||
profileSeed = activeEngine.getSeedManager().getSeed();
|
|
||||||
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
|
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
|
||||||
} else {
|
} else {
|
||||||
IrisToolbelt.applyPregenPerformanceProfile();
|
IrisToolbelt.applyPregenPerformanceProfile();
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisNoisemapPrebakePipeline.prebake(data, new SeedManager(profileSeed), "studio-profile", dimension.getLoadKey());
|
|
||||||
|
|
||||||
KList<String> fileText = new KList<>();
|
KList<String> fileText = new KList<>();
|
||||||
|
|
||||||
KMap<NoiseStyle, Double> styleTimings = new KMap<>();
|
KMap<NoiseStyle, Double> styleTimings = new KMap<>();
|
||||||
@@ -644,30 +655,6 @@ public class CommandStudio implements DirectorExecutor {
|
|||||||
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
|
sender().sendMessage(C.GREEN + "Done! " + report.getPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Profiles a dimension with a cache warm-up pass", origin = DirectorOrigin.PLAYER)
|
|
||||||
public void profilecache(
|
|
||||||
@Param(description = "The dimension to profile", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class)
|
|
||||||
IrisDimension dimension
|
|
||||||
) {
|
|
||||||
File pack = dimension.getLoadFile().getParentFile().getParentFile();
|
|
||||||
IrisData data = IrisData.get(pack);
|
|
||||||
PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension);
|
|
||||||
Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine();
|
|
||||||
long profileSeed = IrisNoisemapPrebakePipeline.dynamicStartupSeed();
|
|
||||||
|
|
||||||
if (activeEngine != null) {
|
|
||||||
profileSeed = activeEngine.getSeedManager().getSeed();
|
|
||||||
IrisToolbelt.applyPregenPerformanceProfile(activeEngine);
|
|
||||||
} else {
|
|
||||||
IrisToolbelt.applyPregenPerformanceProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
sender().sendMessage(C.YELLOW + "Warming noisemap cache for profile...");
|
|
||||||
IrisNoisemapPrebakePipeline.prebakeForced(data, new SeedManager(profileSeed), "studio-profilecache", dimension.getLoadKey());
|
|
||||||
sender().sendMessage(C.YELLOW + "Running measured profile pass...");
|
|
||||||
profile(dimension);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Director(description = "List pack noise generators as pack/generator", aliases = {"pack-noise", "packnoises"})
|
@Director(description = "List pack noise generators as pack/generator", aliases = {"pack-noise", "packnoises"})
|
||||||
public void packnoise() {
|
public void packnoise() {
|
||||||
LinkedHashSet<File> packFolders = new LinkedHashSet<>();
|
LinkedHashSet<File> packFolders = new LinkedHashSet<>();
|
||||||
|
|||||||
@@ -105,9 +105,37 @@ public class IrisLootEvent extends Event {
|
|||||||
if (!Bukkit.isPrimaryThread()) {
|
if (!Bukkit.isPrimaryThread()) {
|
||||||
Iris.warn("LootGenerateEvent was not called on the main thread, please report this issue.");
|
Iris.warn("LootGenerateEvent was not called on the main thread, please report this issue.");
|
||||||
Thread.dumpStack();
|
Thread.dumpStack();
|
||||||
J.sfut(() -> Bukkit.getPluginManager().callEvent(event)).join();
|
J.sfut(() -> {
|
||||||
} else Bukkit.getPluginManager().callEvent(event);
|
try {
|
||||||
|
Bukkit.getPluginManager().callEvent(event);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("LootGenerateEvent dispatch failed at "
|
||||||
|
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
|
||||||
|
if (e instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
if (e instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}).join();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Bukkit.getPluginManager().callEvent(event);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("LootGenerateEvent dispatch failed at "
|
||||||
|
+ world.getName() + " [" + x + "," + y + "," + z + "].", e);
|
||||||
|
if (e instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
if (e instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return event.isCancelled();
|
return event.isCancelled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.WorldCreator;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
final class BukkitPublicBackend implements WorldLifecycleBackend {
|
||||||
|
private final CapabilitySnapshot capabilities;
|
||||||
|
|
||||||
|
BukkitPublicBackend(CapabilitySnapshot capabilities) {
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||||
|
World existing = Bukkit.getWorld(request.worldName());
|
||||||
|
if (existing != null) {
|
||||||
|
return CompletableFuture.completedFuture(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldCreator creator = request.toWorldCreator();
|
||||||
|
if (request.generator() != null) {
|
||||||
|
WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider());
|
||||||
|
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
World world = creator.createWorld();
|
||||||
|
return CompletableFuture.completedFuture(world);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
|
||||||
|
} finally {
|
||||||
|
WorldLifecycleStaging.clearAll(request.worldName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unload(World world, boolean save) {
|
||||||
|
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String backendName() {
|
||||||
|
return "bukkit_public";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String describeSelectionReason() {
|
||||||
|
return "public Bukkit world lifecycle path";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
final class CapabilityResolution {
|
||||||
|
private CapabilityResolution() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method resolveCreateLevelMethod(Class<?> owner) throws NoSuchMethodException {
|
||||||
|
Method current = resolveMethod(owner, "createLevel", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 3
|
||||||
|
&& "LevelStem".equals(params[0].getSimpleName())
|
||||||
|
&& "WorldLoadingInfoAndData".equals(params[1].getSimpleName())
|
||||||
|
&& "WorldDataAndGenSettings".equals(params[2].getSimpleName());
|
||||||
|
});
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method legacy = resolveMethod(owner, "createLevel", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 4
|
||||||
|
&& "LevelStem".equals(params[0].getSimpleName())
|
||||||
|
&& "WorldLoadingInfo".equals(params[1].getSimpleName())
|
||||||
|
&& "LevelStorageAccess".equals(params[2].getSimpleName())
|
||||||
|
&& "PrimaryLevelData".equals(params[3].getSimpleName());
|
||||||
|
});
|
||||||
|
if (legacy != null) {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NoSuchMethodException(owner.getName() + "#createLevel");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method resolveLevelStorageAccessMethod(Class<?> owner) throws NoSuchMethodException {
|
||||||
|
Method exactValidate = resolveMethod(owner, "validateAndCreateAccess", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 2
|
||||||
|
&& String.class.equals(params[0])
|
||||||
|
&& "ResourceKey".equals(params[1].getSimpleName())
|
||||||
|
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
|
||||||
|
});
|
||||||
|
if (exactValidate != null) {
|
||||||
|
return exactValidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method oneArgValidate = resolveMethod(owner, "validateAndCreateAccess", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 1
|
||||||
|
&& String.class.equals(params[0])
|
||||||
|
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
|
||||||
|
});
|
||||||
|
if (oneArgValidate != null) {
|
||||||
|
return oneArgValidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method exactCreate = resolveMethod(owner, "createAccess", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 2
|
||||||
|
&& String.class.equals(params[0])
|
||||||
|
&& "ResourceKey".equals(params[1].getSimpleName())
|
||||||
|
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
|
||||||
|
});
|
||||||
|
if (exactCreate != null) {
|
||||||
|
return exactCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method oneArgCreate = resolveMethod(owner, "createAccess", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 1
|
||||||
|
&& String.class.equals(params[0])
|
||||||
|
&& "LevelStorageAccess".equals(method.getReturnType().getSimpleName());
|
||||||
|
});
|
||||||
|
if (oneArgCreate != null) {
|
||||||
|
return oneArgCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NoSuchMethodException(owner.getName() + "#validateAndCreateAccess/createAccess");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method resolvePaperWorldDataMethod(Class<?> owner) throws NoSuchMethodException {
|
||||||
|
Method current = resolveMethod(owner, "loadWorldData", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 3
|
||||||
|
&& "MinecraftServer".equals(params[0].getSimpleName())
|
||||||
|
&& "ResourceKey".equals(params[1].getSimpleName())
|
||||||
|
&& String.class.equals(params[2])
|
||||||
|
&& "LoadedWorldData".equals(method.getReturnType().getSimpleName());
|
||||||
|
});
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method legacy = resolveMethod(owner, "getLevelData", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 1 && "LevelStorageAccess".equals(params[0].getSimpleName());
|
||||||
|
});
|
||||||
|
if (legacy != null) {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NoSuchMethodException(owner.getName() + "#loadWorldData/getLevelData");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Constructor<?> resolveWorldLoadingInfoConstructor(Class<?> owner) throws NoSuchMethodException {
|
||||||
|
Constructor<?> current = resolveConstructor(owner, constructor -> {
|
||||||
|
Class<?>[] params = constructor.getParameterTypes();
|
||||||
|
return params.length == 4
|
||||||
|
&& "Environment".equals(params[0].getSimpleName())
|
||||||
|
&& "ResourceKey".equals(params[1].getSimpleName())
|
||||||
|
&& "ResourceKey".equals(params[2].getSimpleName())
|
||||||
|
&& boolean.class.equals(params[3]);
|
||||||
|
});
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
Constructor<?> legacy = resolveConstructor(owner, constructor -> {
|
||||||
|
Class<?>[] params = constructor.getParameterTypes();
|
||||||
|
return params.length == 5
|
||||||
|
&& int.class.equals(params[0])
|
||||||
|
&& String.class.equals(params[1])
|
||||||
|
&& String.class.equals(params[2])
|
||||||
|
&& "ResourceKey".equals(params[3].getSimpleName())
|
||||||
|
&& boolean.class.equals(params[4]);
|
||||||
|
});
|
||||||
|
if (legacy != null) {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NoSuchMethodException(owner.getName() + "#<init>");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Constructor<?> resolveWorldLoadingInfoAndDataConstructor(Class<?> owner) throws NoSuchMethodException {
|
||||||
|
Constructor<?> constructor = resolveConstructor(owner, candidate -> {
|
||||||
|
Class<?>[] params = candidate.getParameterTypes();
|
||||||
|
return params.length == 2
|
||||||
|
&& "WorldLoadingInfo".equals(params[0].getSimpleName())
|
||||||
|
&& "LoadedWorldData".equals(params[1].getSimpleName());
|
||||||
|
});
|
||||||
|
if (constructor == null) {
|
||||||
|
throw new NoSuchMethodException(owner.getName() + "#<init>");
|
||||||
|
}
|
||||||
|
return constructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method resolveCreateNewWorldDataMethod(Class<?> owner) throws NoSuchMethodException {
|
||||||
|
Method method = resolveMethod(owner, "createNewWorldData", candidate -> {
|
||||||
|
Class<?>[] params = candidate.getParameterTypes();
|
||||||
|
return params.length == 5
|
||||||
|
&& "DedicatedServerSettings".equals(params[0].getSimpleName())
|
||||||
|
&& "DataLoadContext".equals(params[1].getSimpleName())
|
||||||
|
&& "Registry".equals(params[2].getSimpleName())
|
||||||
|
&& boolean.class.equals(params[3])
|
||||||
|
&& boolean.class.equals(params[4]);
|
||||||
|
});
|
||||||
|
if (method == null) {
|
||||||
|
throw new NoSuchMethodException(owner.getName() + "#createNewWorldData");
|
||||||
|
}
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method resolveServerRegistryAccessMethod(Class<?> owner) throws NoSuchMethodException {
|
||||||
|
Method method = resolveMethod(owner, "registryAccess", candidate -> candidate.getParameterCount() == 0
|
||||||
|
&& !void.class.equals(candidate.getReturnType()));
|
||||||
|
if (method == null) {
|
||||||
|
throw new NoSuchMethodException(owner.getName() + "#registryAccess");
|
||||||
|
}
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method resolveMethod(Class<?> owner, String name, Predicate<Method> predicate) {
|
||||||
|
Method selected = scanMethods(owner.getMethods(), name, predicate);
|
||||||
|
if (selected != null) {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> current = owner;
|
||||||
|
while (current != null) {
|
||||||
|
selected = scanMethods(current.getDeclaredMethods(), name, predicate);
|
||||||
|
if (selected != null) {
|
||||||
|
selected.setAccessible(true);
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Field resolveField(Class<?> owner, String name) throws NoSuchFieldException {
|
||||||
|
Class<?> current = owner;
|
||||||
|
while (current != null) {
|
||||||
|
try {
|
||||||
|
Field field = current.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field;
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NoSuchFieldException(owner.getName() + "#" + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method scanMethods(Method[] methods, String name, Predicate<Method> predicate) {
|
||||||
|
for (Method method : methods) {
|
||||||
|
if (!method.getName().equals(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (predicate.test(method)) {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Constructor<?> resolveConstructor(Class<?> owner, Predicate<Constructor<?>> predicate) {
|
||||||
|
for (Constructor<?> constructor : owner.getConstructors()) {
|
||||||
|
if (predicate.test(constructor)) {
|
||||||
|
return constructor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Constructor<?> constructor : owner.getDeclaredConstructors()) {
|
||||||
|
if (predicate.test(constructor)) {
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
return constructor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,612 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class CapabilitySnapshot {
|
||||||
|
public enum PaperLikeFlavor {
|
||||||
|
CURRENT_INFO_AND_DATA,
|
||||||
|
LEGACY_STORAGE_ACCESS,
|
||||||
|
UNSUPPORTED
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ServerFamily serverFamily;
|
||||||
|
private final boolean regionizedRuntime;
|
||||||
|
private final Object worldsProvider;
|
||||||
|
private final Class<?> worldsLevelStemClass;
|
||||||
|
private final Class<?> worldsGeneratorTypeClass;
|
||||||
|
private final String worldsProviderResolution;
|
||||||
|
private final Object bukkitServer;
|
||||||
|
private final Object minecraftServer;
|
||||||
|
private final Method createLevelMethod;
|
||||||
|
private final PaperLikeFlavor paperLikeFlavor;
|
||||||
|
private final Class<?> paperWorldLoaderClass;
|
||||||
|
private final Method paperWorldDataMethod;
|
||||||
|
private final Constructor<?> worldLoadingInfoConstructor;
|
||||||
|
private final Constructor<?> worldLoadingInfoAndDataConstructor;
|
||||||
|
private final Method createNewWorldDataMethod;
|
||||||
|
private final Method levelStorageAccessMethod;
|
||||||
|
private final Field worldLoaderContextField;
|
||||||
|
private final Method serverRegistryAccessMethod;
|
||||||
|
private final Field settingsField;
|
||||||
|
private final Field optionsField;
|
||||||
|
private final Method isDemoMethod;
|
||||||
|
private final Method unloadWorldAsyncMethod;
|
||||||
|
private final Method chunkAtAsyncMethod;
|
||||||
|
private final Method removeLevelMethod;
|
||||||
|
private final String paperLikeResolution;
|
||||||
|
|
||||||
|
private CapabilitySnapshot(
|
||||||
|
ServerFamily serverFamily,
|
||||||
|
boolean regionizedRuntime,
|
||||||
|
Object worldsProvider,
|
||||||
|
Class<?> worldsLevelStemClass,
|
||||||
|
Class<?> worldsGeneratorTypeClass,
|
||||||
|
String worldsProviderResolution,
|
||||||
|
Object bukkitServer,
|
||||||
|
Object minecraftServer,
|
||||||
|
Method createLevelMethod,
|
||||||
|
PaperLikeFlavor paperLikeFlavor,
|
||||||
|
Class<?> paperWorldLoaderClass,
|
||||||
|
Method paperWorldDataMethod,
|
||||||
|
Constructor<?> worldLoadingInfoConstructor,
|
||||||
|
Constructor<?> worldLoadingInfoAndDataConstructor,
|
||||||
|
Method createNewWorldDataMethod,
|
||||||
|
Method levelStorageAccessMethod,
|
||||||
|
Field worldLoaderContextField,
|
||||||
|
Method serverRegistryAccessMethod,
|
||||||
|
Field settingsField,
|
||||||
|
Field optionsField,
|
||||||
|
Method isDemoMethod,
|
||||||
|
Method unloadWorldAsyncMethod,
|
||||||
|
Method chunkAtAsyncMethod,
|
||||||
|
Method removeLevelMethod,
|
||||||
|
String paperLikeResolution
|
||||||
|
) {
|
||||||
|
this.serverFamily = serverFamily;
|
||||||
|
this.regionizedRuntime = regionizedRuntime;
|
||||||
|
this.worldsProvider = worldsProvider;
|
||||||
|
this.worldsLevelStemClass = worldsLevelStemClass;
|
||||||
|
this.worldsGeneratorTypeClass = worldsGeneratorTypeClass;
|
||||||
|
this.worldsProviderResolution = worldsProviderResolution;
|
||||||
|
this.bukkitServer = bukkitServer;
|
||||||
|
this.minecraftServer = minecraftServer;
|
||||||
|
this.createLevelMethod = createLevelMethod;
|
||||||
|
this.paperLikeFlavor = paperLikeFlavor;
|
||||||
|
this.paperWorldLoaderClass = paperWorldLoaderClass;
|
||||||
|
this.paperWorldDataMethod = paperWorldDataMethod;
|
||||||
|
this.worldLoadingInfoConstructor = worldLoadingInfoConstructor;
|
||||||
|
this.worldLoadingInfoAndDataConstructor = worldLoadingInfoAndDataConstructor;
|
||||||
|
this.createNewWorldDataMethod = createNewWorldDataMethod;
|
||||||
|
this.levelStorageAccessMethod = levelStorageAccessMethod;
|
||||||
|
this.worldLoaderContextField = worldLoaderContextField;
|
||||||
|
this.serverRegistryAccessMethod = serverRegistryAccessMethod;
|
||||||
|
this.settingsField = settingsField;
|
||||||
|
this.optionsField = optionsField;
|
||||||
|
this.isDemoMethod = isDemoMethod;
|
||||||
|
this.unloadWorldAsyncMethod = unloadWorldAsyncMethod;
|
||||||
|
this.chunkAtAsyncMethod = chunkAtAsyncMethod;
|
||||||
|
this.removeLevelMethod = removeLevelMethod;
|
||||||
|
this.paperLikeResolution = paperLikeResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CapabilitySnapshot probe() {
|
||||||
|
Server server = Bukkit.getServer();
|
||||||
|
Object bukkitServer = server;
|
||||||
|
boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server);
|
||||||
|
ServerFamily serverFamily = detectServerFamily(server, regionizedRuntime);
|
||||||
|
|
||||||
|
Object worldsProvider = null;
|
||||||
|
Class<?> worldsLevelStemClass = null;
|
||||||
|
Class<?> worldsGeneratorTypeClass = null;
|
||||||
|
String worldsProviderResolution = "inactive";
|
||||||
|
try {
|
||||||
|
Object[] worldsProviderData = resolveWorldsProvider();
|
||||||
|
worldsProvider = worldsProviderData[0];
|
||||||
|
worldsLevelStemClass = (Class<?>) worldsProviderData[1];
|
||||||
|
worldsGeneratorTypeClass = (Class<?>) worldsProviderData[2];
|
||||||
|
worldsProviderResolution = (String) worldsProviderData[3];
|
||||||
|
} catch (Throwable e) {
|
||||||
|
worldsProviderResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
Object minecraftServer = null;
|
||||||
|
Method createLevelMethod = null;
|
||||||
|
PaperLikeFlavor paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED;
|
||||||
|
Class<?> paperWorldLoaderClass = null;
|
||||||
|
Method paperWorldDataMethod = null;
|
||||||
|
Constructor<?> worldLoadingInfoConstructor = null;
|
||||||
|
Constructor<?> worldLoadingInfoAndDataConstructor = null;
|
||||||
|
Method createNewWorldDataMethod = null;
|
||||||
|
Method levelStorageAccessMethod = null;
|
||||||
|
Field worldLoaderContextField = null;
|
||||||
|
Method serverRegistryAccessMethod = null;
|
||||||
|
Field settingsField = null;
|
||||||
|
Field optionsField = null;
|
||||||
|
Method isDemoMethod = null;
|
||||||
|
Method removeLevelMethod = null;
|
||||||
|
String paperLikeResolution = "inactive";
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (bukkitServer != null) {
|
||||||
|
Method getServerMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "getServer", method -> method.getParameterCount() == 0);
|
||||||
|
if (getServerMethod != null) {
|
||||||
|
minecraftServer = getServerMethod.invoke(bukkitServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minecraftServer != null) {
|
||||||
|
Class<?> minecraftServerClass = Class.forName("net.minecraft.server.MinecraftServer");
|
||||||
|
if (!minecraftServerClass.isInstance(minecraftServer)) {
|
||||||
|
throw new IllegalStateException("resolved server is not a MinecraftServer: " + minecraftServer.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
createLevelMethod = CapabilityResolution.resolveCreateLevelMethod(minecraftServer.getClass());
|
||||||
|
removeLevelMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "removeLevel", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 1 && "ServerLevel".equals(params[0].getSimpleName());
|
||||||
|
});
|
||||||
|
worldLoaderContextField = CapabilityResolution.resolveField(minecraftServer.getClass(), "worldLoaderContext");
|
||||||
|
serverRegistryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(minecraftServer.getClass());
|
||||||
|
settingsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "settings");
|
||||||
|
optionsField = CapabilityResolution.resolveField(minecraftServer.getClass(), "options");
|
||||||
|
isDemoMethod = CapabilityResolution.resolveMethod(minecraftServer.getClass(), "isDemo", method -> method.getParameterCount() == 0 && boolean.class.equals(method.getReturnType()));
|
||||||
|
|
||||||
|
Class<?> mainClass = Class.forName("net.minecraft.server.Main");
|
||||||
|
createNewWorldDataMethod = CapabilityResolution.resolveCreateNewWorldDataMethod(mainClass);
|
||||||
|
|
||||||
|
Class<?> paperLoaderCandidate = Class.forName("io.papermc.paper.world.PaperWorldLoader");
|
||||||
|
paperWorldLoaderClass = paperLoaderCandidate;
|
||||||
|
paperWorldDataMethod = CapabilityResolution.resolvePaperWorldDataMethod(paperLoaderCandidate);
|
||||||
|
Class<?> worldLoadingInfoClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo");
|
||||||
|
worldLoadingInfoConstructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(worldLoadingInfoClass);
|
||||||
|
|
||||||
|
if (createLevelMethod.getParameterCount() == 3) {
|
||||||
|
Class<?> worldLoadingInfoAndDataClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfoAndData");
|
||||||
|
worldLoadingInfoAndDataConstructor = CapabilityResolution.resolveWorldLoadingInfoAndDataConstructor(worldLoadingInfoAndDataClass);
|
||||||
|
paperLikeFlavor = PaperLikeFlavor.CURRENT_INFO_AND_DATA;
|
||||||
|
} else {
|
||||||
|
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
|
||||||
|
levelStorageAccessMethod = CapabilityResolution.resolveLevelStorageAccessMethod(levelStorageSourceClass);
|
||||||
|
paperLikeFlavor = PaperLikeFlavor.LEGACY_STORAGE_ACCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
paperLikeResolution = "available(flavor=" + paperLikeFlavor.name().toLowerCase(Locale.ROOT)
|
||||||
|
+ ", createLevel=" + createLevelMethod.toGenericString() + ")";
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
paperLikeResolution = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
|
||||||
|
createLevelMethod = null;
|
||||||
|
paperLikeFlavor = PaperLikeFlavor.UNSUPPORTED;
|
||||||
|
paperWorldLoaderClass = null;
|
||||||
|
paperWorldDataMethod = null;
|
||||||
|
worldLoadingInfoConstructor = null;
|
||||||
|
worldLoadingInfoAndDataConstructor = null;
|
||||||
|
createNewWorldDataMethod = null;
|
||||||
|
levelStorageAccessMethod = null;
|
||||||
|
worldLoaderContextField = null;
|
||||||
|
serverRegistryAccessMethod = null;
|
||||||
|
settingsField = null;
|
||||||
|
optionsField = null;
|
||||||
|
isDemoMethod = null;
|
||||||
|
removeLevelMethod = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method unloadWorldAsyncMethod = null;
|
||||||
|
try {
|
||||||
|
if (bukkitServer != null) {
|
||||||
|
unloadWorldAsyncMethod = CapabilityResolution.resolveMethod(bukkitServer.getClass(), "unloadWorldAsync", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 3
|
||||||
|
&& World.class.equals(params[0])
|
||||||
|
&& boolean.class.equals(params[1])
|
||||||
|
&& "Consumer".equals(params[2].getSimpleName());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
unloadWorldAsyncMethod = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method chunkAtAsyncMethod = null;
|
||||||
|
try {
|
||||||
|
chunkAtAsyncMethod = CapabilityResolution.resolveMethod(World.class, "getChunkAtAsync", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 3
|
||||||
|
&& int.class.equals(params[0])
|
||||||
|
&& int.class.equals(params[1])
|
||||||
|
&& boolean.class.equals(params[2]);
|
||||||
|
});
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
chunkAtAsyncMethod = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CapabilitySnapshot(
|
||||||
|
serverFamily,
|
||||||
|
regionizedRuntime,
|
||||||
|
worldsProvider,
|
||||||
|
worldsLevelStemClass,
|
||||||
|
worldsGeneratorTypeClass,
|
||||||
|
worldsProviderResolution,
|
||||||
|
bukkitServer,
|
||||||
|
minecraftServer,
|
||||||
|
createLevelMethod,
|
||||||
|
paperLikeFlavor,
|
||||||
|
paperWorldLoaderClass,
|
||||||
|
paperWorldDataMethod,
|
||||||
|
worldLoadingInfoConstructor,
|
||||||
|
worldLoadingInfoAndDataConstructor,
|
||||||
|
createNewWorldDataMethod,
|
||||||
|
levelStorageAccessMethod,
|
||||||
|
worldLoaderContextField,
|
||||||
|
serverRegistryAccessMethod,
|
||||||
|
settingsField,
|
||||||
|
optionsField,
|
||||||
|
isDemoMethod,
|
||||||
|
unloadWorldAsyncMethod,
|
||||||
|
chunkAtAsyncMethod,
|
||||||
|
removeLevelMethod,
|
||||||
|
paperLikeResolution
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CapabilitySnapshot forTesting(ServerFamily serverFamily, boolean regionizedRuntime, boolean worldsProviderHealthy, boolean paperLikeRuntimeHealthy) {
|
||||||
|
Object minecraftServer = paperLikeRuntimeHealthy ? new TestingPaperLikeServer("datapack-registry", "server-registry") : null;
|
||||||
|
Method createLevelMethod = null;
|
||||||
|
Field worldLoaderContextField = null;
|
||||||
|
Method serverRegistryAccessMethod = null;
|
||||||
|
try {
|
||||||
|
createLevelMethod = paperLikeRuntimeHealthy
|
||||||
|
? TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class)
|
||||||
|
: null;
|
||||||
|
worldLoaderContextField = paperLikeRuntimeHealthy
|
||||||
|
? CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext")
|
||||||
|
: null;
|
||||||
|
serverRegistryAccessMethod = paperLikeRuntimeHealthy
|
||||||
|
? CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class)
|
||||||
|
: null;
|
||||||
|
} catch (NoSuchMethodException | NoSuchFieldException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
return new CapabilitySnapshot(
|
||||||
|
serverFamily,
|
||||||
|
regionizedRuntime,
|
||||||
|
worldsProviderHealthy ? new Object() : null,
|
||||||
|
worldsProviderHealthy ? Object.class : null,
|
||||||
|
worldsProviderHealthy ? Object.class : null,
|
||||||
|
worldsProviderHealthy ? "test-provider" : "inactive",
|
||||||
|
null,
|
||||||
|
minecraftServer,
|
||||||
|
createLevelMethod,
|
||||||
|
paperLikeRuntimeHealthy ? PaperLikeFlavor.CURRENT_INFO_AND_DATA : PaperLikeFlavor.UNSUPPORTED,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
worldLoaderContextField,
|
||||||
|
serverRegistryAccessMethod,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
paperLikeRuntimeHealthy ? "available(test)" : "unsupported(test)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CapabilitySnapshot forTestingRuntimeRegistries(ServerFamily serverFamily, boolean regionizedRuntime, Object datapackDimensions, Object serverRegistryAccess) {
|
||||||
|
TestingPaperLikeServer minecraftServer = new TestingPaperLikeServer(datapackDimensions, serverRegistryAccess);
|
||||||
|
Method createLevelMethod;
|
||||||
|
Field worldLoaderContextField;
|
||||||
|
Method registryAccessMethod;
|
||||||
|
try {
|
||||||
|
createLevelMethod = TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class);
|
||||||
|
worldLoaderContextField = CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext");
|
||||||
|
registryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class);
|
||||||
|
} catch (NoSuchMethodException | NoSuchFieldException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
return new CapabilitySnapshot(
|
||||||
|
serverFamily,
|
||||||
|
regionizedRuntime,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"inactive",
|
||||||
|
null,
|
||||||
|
minecraftServer,
|
||||||
|
createLevelMethod,
|
||||||
|
PaperLikeFlavor.CURRENT_INFO_AND_DATA,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
worldLoaderContextField,
|
||||||
|
registryAccessMethod,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"available(test-runtime-registries)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerFamily serverFamily() {
|
||||||
|
return serverFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean regionizedRuntime() {
|
||||||
|
return regionizedRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object worldsProvider() {
|
||||||
|
return worldsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<?> worldsLevelStemClass() {
|
||||||
|
return worldsLevelStemClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<?> worldsGeneratorTypeClass() {
|
||||||
|
return worldsGeneratorTypeClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object bukkitServer() {
|
||||||
|
return bukkitServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object minecraftServer() {
|
||||||
|
return minecraftServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method createLevelMethod() {
|
||||||
|
return createLevelMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaperLikeFlavor paperLikeFlavor() {
|
||||||
|
return paperLikeFlavor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<?> paperWorldLoaderClass() {
|
||||||
|
return paperWorldLoaderClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method paperWorldDataMethod() {
|
||||||
|
return paperWorldDataMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Constructor<?> worldLoadingInfoConstructor() {
|
||||||
|
return worldLoadingInfoConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Constructor<?> worldLoadingInfoAndDataConstructor() {
|
||||||
|
return worldLoadingInfoAndDataConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method createNewWorldDataMethod() {
|
||||||
|
return createNewWorldDataMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method levelStorageAccessMethod() {
|
||||||
|
return levelStorageAccessMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field worldLoaderContextField() {
|
||||||
|
return worldLoaderContextField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method serverRegistryAccessMethod() {
|
||||||
|
return serverRegistryAccessMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field settingsField() {
|
||||||
|
return settingsField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field optionsField() {
|
||||||
|
return optionsField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method isDemoMethod() {
|
||||||
|
return isDemoMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method unloadWorldAsyncMethod() {
|
||||||
|
return unloadWorldAsyncMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method chunkAtAsyncMethod() {
|
||||||
|
return chunkAtAsyncMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Method removeLevelMethod() {
|
||||||
|
return removeLevelMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasWorldsProvider() {
|
||||||
|
return worldsProvider != null && worldsLevelStemClass != null && worldsGeneratorTypeClass != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPaperLikeRuntime() {
|
||||||
|
return minecraftServer != null
|
||||||
|
&& createLevelMethod != null
|
||||||
|
&& serverRegistryAccessMethod != null
|
||||||
|
&& paperLikeFlavor != PaperLikeFlavor.UNSUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String worldsProviderResolution() {
|
||||||
|
return worldsProviderResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String paperLikeResolution() {
|
||||||
|
return paperLikeResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String describe() {
|
||||||
|
return "family=" + serverFamily.id()
|
||||||
|
+ ", regionizedRuntime=" + regionizedRuntime
|
||||||
|
+ ", worldsProvider=" + worldsProviderResolution
|
||||||
|
+ ", paperLike=" + paperLikeResolution
|
||||||
|
+ ", serverRegistryAccess=" + (serverRegistryAccessMethod != null)
|
||||||
|
+ ", unloadAsync=" + (unloadWorldAsyncMethod != null)
|
||||||
|
+ ", chunkAsync=" + (chunkAtAsyncMethod != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServerFamily detectServerFamily(Server server, boolean regionizedRuntime) {
|
||||||
|
String bukkitName = server == null ? "" : server.getName();
|
||||||
|
String bukkitVersion = server == null ? "" : server.getVersion();
|
||||||
|
String serverClassName = server == null ? "" : server.getClass().getName();
|
||||||
|
boolean canvasRuntime = hasCanvasRuntime();
|
||||||
|
|
||||||
|
if (containsIgnoreCase(bukkitName, "folia")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "folia")
|
||||||
|
|| containsIgnoreCase(serverClassName, "folia")) {
|
||||||
|
return ServerFamily.FOLIA;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvasRuntime
|
||||||
|
|| containsIgnoreCase(bukkitName, "canvas")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "canvas")
|
||||||
|
|| containsIgnoreCase(serverClassName, "canvas")) {
|
||||||
|
return regionizedRuntime ? ServerFamily.CANVAS : ServerFamily.CANVAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsIgnoreCase(bukkitName, "purpur")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "purpur")
|
||||||
|
|| containsIgnoreCase(serverClassName, "purpur")) {
|
||||||
|
return ServerFamily.PURPUR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsIgnoreCase(bukkitName, "paper")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "paper")
|
||||||
|
|| containsIgnoreCase(serverClassName, "paper")
|
||||||
|
|| containsIgnoreCase(bukkitName, "pufferfish")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "pufferfish")
|
||||||
|
|| containsIgnoreCase(serverClassName, "pufferfish")) {
|
||||||
|
return ServerFamily.PAPER;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsIgnoreCase(bukkitName, "spigot")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "spigot")
|
||||||
|
|| containsIgnoreCase(serverClassName, "spigot")) {
|
||||||
|
return ServerFamily.SPIGOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsIgnoreCase(bukkitName, "craftbukkit")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "craftbukkit")
|
||||||
|
|| containsIgnoreCase(serverClassName, "craftbukkit")
|
||||||
|
|| containsIgnoreCase(bukkitName, "bukkit")
|
||||||
|
|| containsIgnoreCase(bukkitVersion, "bukkit")) {
|
||||||
|
return ServerFamily.BUKKIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionizedRuntime || J.isFolia()) {
|
||||||
|
return ServerFamily.FOLIA;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerFamily.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasCanvasRuntime() {
|
||||||
|
try {
|
||||||
|
Class.forName("io.canvasmc.canvas.region.WorldRegionizer");
|
||||||
|
return true;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsIgnoreCase(String value, String needle) {
|
||||||
|
if (value == null || needle == null || needle.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return value.toLowerCase(Locale.ROOT).contains(needle.toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object[] resolveWorldsProvider() throws Throwable {
|
||||||
|
try {
|
||||||
|
Class<?> worldsProviderClass = Class.forName("net.thenextlvl.worlds.api.WorldsProvider");
|
||||||
|
Class<?> levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem");
|
||||||
|
Class<?> generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType");
|
||||||
|
Object provider = Bukkit.getServicesManager().load(worldsProviderClass);
|
||||||
|
String resolution = provider == null ? "inactive(service not registered)" : "active(service=" + provider.getClass().getName() + ")";
|
||||||
|
return new Object[]{provider, levelStemClass, generatorTypeClass, resolution};
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<Class<?>> knownServices = Bukkit.getServicesManager().getKnownServices();
|
||||||
|
for (Class<?> serviceClass : knownServices) {
|
||||||
|
if (!"net.thenextlvl.worlds.api.WorldsProvider".equals(serviceClass.getName())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisteredServiceProvider<?> registration = Bukkit.getServicesManager().getRegistration(serviceClass);
|
||||||
|
if (registration == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object provider = registration.getProvider();
|
||||||
|
ClassLoader loader = serviceClass.getClassLoader();
|
||||||
|
if (loader == null && provider != null) {
|
||||||
|
loader = provider.getClass().getClassLoader();
|
||||||
|
}
|
||||||
|
if (loader == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> levelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem", false, loader);
|
||||||
|
Class<?> generatorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType", false, loader);
|
||||||
|
return new Object[]{provider, levelStemClass, generatorTypeClass, "active(service-scan=" + provider.getClass().getName() + ")"};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Object[]{null, null, null, "inactive(service scan found nothing)"};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestingPaperLikeServer {
|
||||||
|
private final TestingWorldLoaderContext worldLoaderContext;
|
||||||
|
private final Object registryAccess;
|
||||||
|
|
||||||
|
private TestingPaperLikeServer(Object datapackDimensions, Object registryAccess) {
|
||||||
|
this.worldLoaderContext = new TestingWorldLoaderContext(datapackDimensions);
|
||||||
|
this.registryAccess = registryAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private void createLevel(Object levelStem, Object worldLoadingInfoAndData, Object worldDataAndGenSettings) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private Object registryAccess() {
|
||||||
|
return registryAccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestingWorldLoaderContext {
|
||||||
|
private final Object datapackDimensions;
|
||||||
|
|
||||||
|
private TestingWorldLoaderContext(Object datapackDimensions) {
|
||||||
|
this.datapackDimensions = datapackDimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private Object datapackDimensions() {
|
||||||
|
return datapackDimensions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
|
||||||
|
private final CapabilitySnapshot capabilities;
|
||||||
|
|
||||||
|
PaperLikeRuntimeBackend(CapabilitySnapshot capabilities) {
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
||||||
|
return request.studio()
|
||||||
|
&& capabilities.serverFamily().isPaperLike()
|
||||||
|
&& capabilities.hasPaperLikeRuntime();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||||
|
Object legacyStorageAccess = null;
|
||||||
|
try {
|
||||||
|
World existing = Bukkit.getWorld(request.worldName());
|
||||||
|
if (existing != null) {
|
||||||
|
return CompletableFuture.completedFuture(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.generator() == null) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Runtime world creation requires a non-null chunk generator."));
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider());
|
||||||
|
WorldLifecycleSupport.stageRuntimeConfiguration(request.worldName());
|
||||||
|
|
||||||
|
Iris.info("WorldLifecycle runtime LevelStem: world=" + request.worldName()
|
||||||
|
+ ", backend=paper_like_runtime, flavor=" + capabilities.paperLikeFlavor().name().toLowerCase(Locale.ROOT)
|
||||||
|
+ ", registrySource=" + WorldLifecycleSupport.runtimeLevelStemRegistrySource(request));
|
||||||
|
Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request);
|
||||||
|
Object stemKey = WorldLifecycleSupport.createRuntimeLevelStemKey(request.worldName());
|
||||||
|
|
||||||
|
if (capabilities.paperLikeFlavor() == CapabilitySnapshot.PaperLikeFlavor.CURRENT_INFO_AND_DATA) {
|
||||||
|
Object dimensionKey = WorldLifecycleSupport.createDimensionKey(stemKey);
|
||||||
|
Object loadedWorldData = capabilities.paperWorldDataMethod().invoke(null, capabilities.minecraftServer(), dimensionKey, request.worldName());
|
||||||
|
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, true);
|
||||||
|
Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData);
|
||||||
|
Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName());
|
||||||
|
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings);
|
||||||
|
} else {
|
||||||
|
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName());
|
||||||
|
Object primaryLevelData = WorldLifecycleSupport.createLegacyPrimaryLevelData(capabilities, legacyStorageAccess, request.worldName());
|
||||||
|
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(0, request.worldName(), request.environment().name().toLowerCase(Locale.ROOT), stemKey, true);
|
||||||
|
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfo, legacyStorageAccess, primaryLevelData);
|
||||||
|
}
|
||||||
|
|
||||||
|
World loadedWorld = Bukkit.getWorld(request.worldName());
|
||||||
|
if (loadedWorld == null) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Paper-like runtime backend did not load world \"" + request.worldName() + "\"."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.completedFuture(loadedWorld);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
|
||||||
|
} finally {
|
||||||
|
WorldLifecycleStaging.clearGenerator(request.worldName());
|
||||||
|
WorldLifecycleSupport.closeLevelStorageAccess(legacyStorageAccess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unload(World world, boolean save) {
|
||||||
|
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String backendName() {
|
||||||
|
return "paper_like_runtime";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String describeSelectionReason() {
|
||||||
|
return "server family " + capabilities.serverFamily().id() + " exposes paper-like runtime world lifecycle capabilities";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public enum ServerFamily {
|
||||||
|
BUKKIT,
|
||||||
|
SPIGOT,
|
||||||
|
PAPER,
|
||||||
|
PURPUR,
|
||||||
|
FOLIA,
|
||||||
|
CANVAS,
|
||||||
|
UNKNOWN;
|
||||||
|
|
||||||
|
public boolean isPaperLike() {
|
||||||
|
return this == PAPER || this == PURPUR || this == FOLIA || this == CANVAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return name().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public interface WorldLifecycleBackend {
|
||||||
|
boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities);
|
||||||
|
|
||||||
|
CompletableFuture<World> create(WorldLifecycleRequest request);
|
||||||
|
|
||||||
|
boolean unload(World world, boolean save);
|
||||||
|
|
||||||
|
String backendName();
|
||||||
|
|
||||||
|
String describeSelectionReason();
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
public enum WorldLifecycleCaller {
|
||||||
|
STUDIO,
|
||||||
|
CREATE,
|
||||||
|
BENCHMARK
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.WorldCreator;
|
||||||
|
import org.bukkit.WorldType;
|
||||||
|
import org.bukkit.generator.BiomeProvider;
|
||||||
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
|
||||||
|
public record WorldLifecycleRequest(
|
||||||
|
String worldName,
|
||||||
|
World.Environment environment,
|
||||||
|
ChunkGenerator generator,
|
||||||
|
BiomeProvider biomeProvider,
|
||||||
|
WorldType worldType,
|
||||||
|
boolean generateStructures,
|
||||||
|
boolean hardcore,
|
||||||
|
long seed,
|
||||||
|
boolean studio,
|
||||||
|
boolean benchmark,
|
||||||
|
WorldLifecycleCaller callerKind
|
||||||
|
) {
|
||||||
|
public static WorldLifecycleRequest fromCreator(WorldCreator creator, boolean studio, boolean benchmark, WorldLifecycleCaller callerKind) {
|
||||||
|
return new WorldLifecycleRequest(
|
||||||
|
creator.name(),
|
||||||
|
creator.environment(),
|
||||||
|
creator.generator(),
|
||||||
|
creator.biomeProvider(),
|
||||||
|
creator.type(),
|
||||||
|
creator.generateStructures(),
|
||||||
|
creator.hardcore(),
|
||||||
|
creator.seed(),
|
||||||
|
studio,
|
||||||
|
benchmark,
|
||||||
|
callerKind
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldCreator toWorldCreator() {
|
||||||
|
WorldCreator creator = new WorldCreator(worldName)
|
||||||
|
.environment(environment)
|
||||||
|
.generateStructures(generateStructures)
|
||||||
|
.hardcore(hardcore)
|
||||||
|
.type(worldType)
|
||||||
|
.seed(seed)
|
||||||
|
.generator(generator);
|
||||||
|
if (biomeProvider != null) {
|
||||||
|
creator.biomeProvider(biomeProvider);
|
||||||
|
}
|
||||||
|
return creator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class WorldLifecycleService {
|
||||||
|
private static volatile WorldLifecycleService instance;
|
||||||
|
|
||||||
|
private final CapabilitySnapshot capabilities;
|
||||||
|
private final WorldsProviderBackend worldsProviderBackend;
|
||||||
|
private final PaperLikeRuntimeBackend paperLikeRuntimeBackend;
|
||||||
|
private final BukkitPublicBackend bukkitPublicBackend;
|
||||||
|
private final List<WorldLifecycleBackend> backends;
|
||||||
|
private final Map<String, String> worldBackendByName;
|
||||||
|
|
||||||
|
public WorldLifecycleService(CapabilitySnapshot capabilities) {
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
this.worldsProviderBackend = new WorldsProviderBackend(capabilities);
|
||||||
|
this.paperLikeRuntimeBackend = new PaperLikeRuntimeBackend(capabilities);
|
||||||
|
this.bukkitPublicBackend = new BukkitPublicBackend(capabilities);
|
||||||
|
this.backends = List.of(worldsProviderBackend, paperLikeRuntimeBackend, bukkitPublicBackend);
|
||||||
|
this.worldBackendByName = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldLifecycleService get() {
|
||||||
|
WorldLifecycleService current = instance;
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (WorldLifecycleService.class) {
|
||||||
|
if (instance != null) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
CapabilitySnapshot capabilities = CapabilitySnapshot.probe();
|
||||||
|
instance = new WorldLifecycleService(capabilities);
|
||||||
|
Iris.info("WorldLifecycle capabilities: %s", capabilities.describe());
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CapabilitySnapshot capabilities() {
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||||
|
WorldLifecycleBackend backend;
|
||||||
|
try {
|
||||||
|
backend = selectCreateBackend(request);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("WorldLifecycle create backend selection failed for world=\"" + request.worldName()
|
||||||
|
+ "\", caller=" + request.callerKind().name().toLowerCase() + ".", e);
|
||||||
|
return CompletableFuture.failedFuture(e);
|
||||||
|
}
|
||||||
|
Iris.info("WorldLifecycle create: world=%s, caller=%s, backend=%s, reason=%s",
|
||||||
|
request.worldName(),
|
||||||
|
request.callerKind().name().toLowerCase(),
|
||||||
|
backend.backendName(),
|
||||||
|
backend.describeSelectionReason());
|
||||||
|
return backend.create(request).whenComplete((world, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
Throwable cause = WorldLifecycleSupport.unwrap(throwable);
|
||||||
|
Iris.reportError("WorldLifecycle create failed: world=\"" + request.worldName()
|
||||||
|
+ "\", caller=" + request.callerKind().name().toLowerCase()
|
||||||
|
+ ", backend=" + backend.backendName()
|
||||||
|
+ ", family=" + capabilities.serverFamily().id() + ".", cause);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (world != null) {
|
||||||
|
worldBackendByName.put(world.getName(), backend.backendName());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public World createBlocking(WorldLifecycleRequest request) {
|
||||||
|
try {
|
||||||
|
return create(request).join();
|
||||||
|
} catch (CompletionException e) {
|
||||||
|
throw new IllegalStateException(WorldLifecycleSupport.unwrap(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean unload(World world, boolean save) {
|
||||||
|
if (!J.isPrimaryThread()) {
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
J.s(() -> {
|
||||||
|
try {
|
||||||
|
future.complete(unloadDirect(world, save));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
return unloadDirect(world, save);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean unloadDirect(World world, boolean save) {
|
||||||
|
WorldLifecycleBackend backend = selectUnloadBackend(world.getName());
|
||||||
|
Iris.info("WorldLifecycle unload: world=%s, backend=%s, reason=%s",
|
||||||
|
world.getName(),
|
||||||
|
backend.backendName(),
|
||||||
|
backend.describeSelectionReason());
|
||||||
|
boolean unloaded;
|
||||||
|
try {
|
||||||
|
unloaded = backend.unload(world, save);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("WorldLifecycle unload failed: world=\"" + world.getName()
|
||||||
|
+ "\", backend=" + backend.backendName()
|
||||||
|
+ ", family=" + capabilities.serverFamily().id() + ".", e);
|
||||||
|
if (e instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
if (e instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
if (unloaded) {
|
||||||
|
worldBackendByName.remove(world.getName());
|
||||||
|
}
|
||||||
|
return unloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String backendNameForWorld(String worldName) {
|
||||||
|
return selectUnloadBackend(worldName).backendName();
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldLifecycleBackend selectCreateBackend(WorldLifecycleRequest request) {
|
||||||
|
if (worldsProviderBackend.supports(request, capabilities)) {
|
||||||
|
return worldsProviderBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.studio() && capabilities.serverFamily().isPaperLike()) {
|
||||||
|
if (!paperLikeRuntimeBackend.supports(request, capabilities)) {
|
||||||
|
throw new IllegalStateException("World lifecycle backend paper_like_runtime is unavailable for studio create on "
|
||||||
|
+ capabilities.serverFamily().id() + ": " + capabilities.paperLikeResolution());
|
||||||
|
}
|
||||||
|
return paperLikeRuntimeBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (WorldLifecycleBackend backend : backends) {
|
||||||
|
if (backend.supports(request, capabilities)) {
|
||||||
|
return backend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("No world lifecycle backend supports request for \"" + request.worldName() + "\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldLifecycleBackend selectUnloadBackend(String worldName) {
|
||||||
|
String backendName = worldBackendByName.get(worldName);
|
||||||
|
if (backendName == null) {
|
||||||
|
return bukkitPublicBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (WorldLifecycleBackend backend : backends) {
|
||||||
|
if (backend.backendName().equals(backendName)) {
|
||||||
|
return backend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bukkitPublicBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rememberBackend(String worldName, String backendName) {
|
||||||
|
worldBackendByName.put(worldName, backendName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.bukkit.generator.BiomeProvider;
|
||||||
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class WorldLifecycleStaging {
|
||||||
|
private static final Map<String, ChunkGenerator> stagedGenerators = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<String, BiomeProvider> stagedBiomeProviders = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<String, ChunkGenerator> stagedStemGenerators = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private WorldLifecycleStaging() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void stageGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator, @Nullable BiomeProvider biomeProvider) {
|
||||||
|
stagedGenerators.put(worldName, generator);
|
||||||
|
if (biomeProvider != null) {
|
||||||
|
stagedBiomeProviders.put(worldName, biomeProvider);
|
||||||
|
} else {
|
||||||
|
stagedBiomeProviders.remove(worldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void stageStemGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator) {
|
||||||
|
stagedStemGenerators.put(worldName, generator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static ChunkGenerator consumeGenerator(@NotNull String worldName) {
|
||||||
|
return stagedGenerators.remove(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static BiomeProvider consumeBiomeProvider(@NotNull String worldName) {
|
||||||
|
return stagedBiomeProviders.remove(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static ChunkGenerator consumeStemGenerator(@NotNull String worldName) {
|
||||||
|
return stagedStemGenerators.remove(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearGenerator(@NotNull String worldName) {
|
||||||
|
stagedGenerators.remove(worldName);
|
||||||
|
stagedBiomeProviders.remove(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearStem(@NotNull String worldName) {
|
||||||
|
stagedStemGenerators.remove(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearAll(@NotNull String worldName) {
|
||||||
|
clearGenerator(worldName);
|
||||||
|
clearStem(worldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,520 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.link.Identifier;
|
||||||
|
import art.arcane.iris.core.nms.INMS;
|
||||||
|
import art.arcane.iris.core.nms.INMSBinding;
|
||||||
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
final class WorldLifecycleSupport {
|
||||||
|
private WorldLifecycleSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static Throwable unwrap(Throwable throwable) {
|
||||||
|
if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) {
|
||||||
|
return unwrap(invocationTargetException.getCause());
|
||||||
|
}
|
||||||
|
if (throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null) {
|
||||||
|
return unwrap(completionException.getCause());
|
||||||
|
}
|
||||||
|
if (throwable instanceof ExecutionException executionException && executionException.getCause() != null) {
|
||||||
|
return unwrap(executionException.getCause());
|
||||||
|
}
|
||||||
|
return throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object invoke(Method method, Object target, Object... args) throws ReflectiveOperationException {
|
||||||
|
return method.invoke(target, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object invokeNamed(Object target, String methodName, Class<?>[] parameterTypes, Object... args) throws ReflectiveOperationException {
|
||||||
|
Method method = target.getClass().getMethod(methodName, parameterTypes);
|
||||||
|
return method.invoke(target, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object read(Field field, Object target) throws IllegalAccessException {
|
||||||
|
return field.get(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stageRuntimeConfiguration(String worldName) throws ReflectiveOperationException {
|
||||||
|
Object bukkitServer = Bukkit.getServer();
|
||||||
|
if (bukkitServer == null) {
|
||||||
|
throw new IllegalStateException("Bukkit server is unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Field configurationField = CapabilityResolution.resolveField(bukkitServer.getClass(), "configuration");
|
||||||
|
Object rawConfiguration = configurationField.get(bukkitServer);
|
||||||
|
if (!(rawConfiguration instanceof YamlConfiguration configuration)) {
|
||||||
|
throw new IllegalStateException("CraftServer configuration field is unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationSection worldsSection = configuration.getConfigurationSection("worlds");
|
||||||
|
if (worldsSection == null) {
|
||||||
|
worldsSection = configuration.createSection("worlds");
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigurationSection worldSection = worldsSection.getConfigurationSection(worldName);
|
||||||
|
if (worldSection == null) {
|
||||||
|
worldSection = worldsSection.createSection(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
worldSection.set("generator", "Iris:runtime");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object getRuntimeDatapackDimensions(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
|
||||||
|
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
|
||||||
|
Method datapackDimensionsMethod = CapabilityResolution.resolveMethod(worldLoaderContext.getClass(), "datapackDimensions", method -> method.getParameterCount() == 0);
|
||||||
|
if (datapackDimensionsMethod == null) {
|
||||||
|
throw new IllegalStateException("DataLoadContext does not expose datapackDimensions().");
|
||||||
|
}
|
||||||
|
Object datapackDimensions = datapackDimensionsMethod.invoke(worldLoaderContext);
|
||||||
|
if (datapackDimensions == null) {
|
||||||
|
throw new IllegalStateException("DataLoadContext.datapackDimensions() returned null.");
|
||||||
|
}
|
||||||
|
return datapackDimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object getRuntimeServerRegistryAccess(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
|
||||||
|
Method registryAccessMethod = capabilities.serverRegistryAccessMethod();
|
||||||
|
if (registryAccessMethod == null) {
|
||||||
|
throw new IllegalStateException("MinecraftServer does not expose registryAccess().");
|
||||||
|
}
|
||||||
|
Object registryAccess = registryAccessMethod.invoke(capabilities.minecraftServer());
|
||||||
|
if (registryAccess == null) {
|
||||||
|
throw new IllegalStateException("MinecraftServer.registryAccess() returned null.");
|
||||||
|
}
|
||||||
|
return registryAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object getRuntimeLevelStemRegistry(CapabilitySnapshot capabilities) throws ReflectiveOperationException {
|
||||||
|
Object datapackDimensions = getRuntimeDatapackDimensions(capabilities);
|
||||||
|
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
|
||||||
|
.getField("LEVEL_STEM")
|
||||||
|
.get(null);
|
||||||
|
Method lookupMethod = CapabilityResolution.resolveMethod(datapackDimensions.getClass(), "lookupOrThrow", method -> method.getParameterCount() == 1);
|
||||||
|
if (lookupMethod == null) {
|
||||||
|
throw new IllegalStateException("Registry access does not expose lookupOrThrow(...).");
|
||||||
|
}
|
||||||
|
return lookupMethod.invoke(datapackDimensions, levelStemRegistryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object createRuntimeLevelStemKey(String worldName) throws ReflectiveOperationException {
|
||||||
|
String sanitized = worldName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/_-]", "_");
|
||||||
|
String path = "runtime/" + sanitized;
|
||||||
|
Identifier identifier = new Identifier("iris", path);
|
||||||
|
Object rawIdentifier = Class.forName("net.minecraft.resources.Identifier")
|
||||||
|
.getMethod("fromNamespaceAndPath", String.class, String.class)
|
||||||
|
.invoke(null, identifier.namespace(), identifier.key());
|
||||||
|
Object registryKey = Class.forName("net.minecraft.core.registries.Registries")
|
||||||
|
.getField("LEVEL_STEM")
|
||||||
|
.get(null);
|
||||||
|
Method createMethod = Class.forName("net.minecraft.resources.ResourceKey")
|
||||||
|
.getMethod("create", registryKey.getClass(), rawIdentifier.getClass());
|
||||||
|
return createMethod.invoke(null, registryKey, rawIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object createDimensionKey(Object stemKey) throws ReflectiveOperationException {
|
||||||
|
Class<?> resourceKeyClass = Class.forName("net.minecraft.resources.ResourceKey");
|
||||||
|
Method identifierMethod = CapabilityResolution.resolveMethod(resourceKeyClass, "identifier", method -> method.getParameterCount() == 0);
|
||||||
|
Object identifier = identifierMethod.invoke(stemKey);
|
||||||
|
Object dimensionRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
|
||||||
|
.getField("DIMENSION")
|
||||||
|
.get(null);
|
||||||
|
Method createMethod = resourceKeyClass.getMethod("create", dimensionRegistryKey.getClass(), identifier.getClass());
|
||||||
|
return createMethod.invoke(null, dimensionRegistryKey, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request) throws ReflectiveOperationException {
|
||||||
|
return resolveRuntimeLevelStem(capabilities, request, INMS.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object resolveRuntimeLevelStem(CapabilitySnapshot capabilities, WorldLifecycleRequest request, INMSBinding binding) throws ReflectiveOperationException {
|
||||||
|
ChunkGenerator generator = request.generator();
|
||||||
|
if (generator instanceof PlatformChunkGenerator) {
|
||||||
|
Object registryAccess = getRuntimeServerRegistryAccess(capabilities);
|
||||||
|
try {
|
||||||
|
Object levelStem = binding.createRuntimeLevelStem(registryAccess, generator);
|
||||||
|
if (levelStem == null) {
|
||||||
|
throw new IllegalStateException("Iris NMS binding returned null runtime LevelStem.");
|
||||||
|
}
|
||||||
|
return levelStem;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new IllegalStateException("Failed to create runtime LevelStem from full server registry access for world \"" + request.worldName() + "\".", unwrap(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
|
||||||
|
Object overworldKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
|
||||||
|
.getField("OVERWORLD")
|
||||||
|
.get(null);
|
||||||
|
Method getValueMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "getValue", method -> method.getParameterCount() == 1);
|
||||||
|
if (getValueMethod != null) {
|
||||||
|
Object resolved = getValueMethod.invoke(levelStemRegistry, overworldKey);
|
||||||
|
if (resolved != null) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Method getMethod = CapabilityResolution.resolveMethod(levelStemRegistry.getClass(), "get", method -> method.getParameterCount() == 1);
|
||||||
|
if (getMethod == null) {
|
||||||
|
throw new IllegalStateException("Unable to resolve OVERWORLD LevelStem from registry.");
|
||||||
|
}
|
||||||
|
Object raw = getMethod.invoke(levelStemRegistry, overworldKey);
|
||||||
|
return extractRegistryValue(raw);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new IllegalStateException("Failed to resolve fallback OVERWORLD LevelStem from datapack registry access for world \"" + request.worldName() + "\".", unwrap(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String runtimeLevelStemRegistrySource(WorldLifecycleRequest request) {
|
||||||
|
if (request.generator() instanceof PlatformChunkGenerator) {
|
||||||
|
return "full_server_registry";
|
||||||
|
}
|
||||||
|
return "datapack_level_stem_registry";
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object extractRegistryValue(Object raw) throws ReflectiveOperationException {
|
||||||
|
if (raw == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (raw instanceof Optional<?> optional) {
|
||||||
|
Object nested = optional.orElse(null);
|
||||||
|
if (nested == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return extractRegistryValue(nested);
|
||||||
|
}
|
||||||
|
Method valueMethod = CapabilityResolution.resolveMethod(raw.getClass(), "value", method -> method.getParameterCount() == 0);
|
||||||
|
if (valueMethod != null) {
|
||||||
|
return valueMethod.invoke(raw);
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyWorldDataNameAndModInfo(CapabilitySnapshot capabilities, Object worldDataAndGenSettings, String worldName) throws ReflectiveOperationException {
|
||||||
|
Method dataMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "data", method -> method.getParameterCount() == 0);
|
||||||
|
if (dataMethod == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object worldData = dataMethod.invoke(worldDataAndGenSettings);
|
||||||
|
if (worldData == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method checkNameMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "checkName", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 1 && String.class.equals(params[0]);
|
||||||
|
});
|
||||||
|
if (checkNameMethod != null) {
|
||||||
|
checkNameMethod.invoke(worldData, worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0);
|
||||||
|
Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0);
|
||||||
|
if (getModdedStatusMethod == null || getServerModNameMethod == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer());
|
||||||
|
Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0);
|
||||||
|
Method setModdedInfoMethod = CapabilityResolution.resolveMethod(worldData.getClass(), "setModdedInfo", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]);
|
||||||
|
});
|
||||||
|
if (shouldReportAsModifiedMethod == null || setModdedInfoMethod == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck));
|
||||||
|
String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer());
|
||||||
|
setModdedInfoMethod.invoke(worldData, modName, modified);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object createCurrentWorldDataAndSettings(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
|
||||||
|
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
|
||||||
|
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
|
||||||
|
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
|
||||||
|
boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer()));
|
||||||
|
Object options = read(capabilities.optionsField(), capabilities.minecraftServer());
|
||||||
|
Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 1 && String.class.equals(params[0]);
|
||||||
|
});
|
||||||
|
boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest"));
|
||||||
|
Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
|
||||||
|
Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0);
|
||||||
|
if (cookieMethod == null) {
|
||||||
|
throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie().");
|
||||||
|
}
|
||||||
|
Object worldDataAndGenSettings = cookieMethod.invoke(dataLoadOutput);
|
||||||
|
applyWorldDataNameAndModInfo(capabilities, worldDataAndGenSettings, worldName);
|
||||||
|
return worldDataAndGenSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object createLegacyPrimaryLevelData(CapabilitySnapshot capabilities, Object levelStorageAccess, String worldName) throws ReflectiveOperationException {
|
||||||
|
Object levelDataResult = capabilities.paperWorldDataMethod().invoke(null, levelStorageAccess);
|
||||||
|
Method fatalErrorMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "fatalError", method -> method.getParameterCount() == 0);
|
||||||
|
Method dataTagMethod = CapabilityResolution.resolveMethod(levelDataResult.getClass(), "dataTag", method -> method.getParameterCount() == 0);
|
||||||
|
if (fatalErrorMethod != null && Boolean.TRUE.equals(fatalErrorMethod.invoke(levelDataResult))) {
|
||||||
|
throw new IllegalStateException("Paper runtime world-data helper reported a fatal error for \"" + worldName + "\".");
|
||||||
|
}
|
||||||
|
if (dataTagMethod != null && dataTagMethod.invoke(levelDataResult) != null) {
|
||||||
|
throw new IllegalStateException("Runtime world \"" + worldName + "\" already contains level data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
|
||||||
|
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
|
||||||
|
Object levelStemRegistry = getRuntimeLevelStemRegistry(capabilities);
|
||||||
|
boolean demo = Boolean.TRUE.equals(capabilities.isDemoMethod().invoke(capabilities.minecraftServer()));
|
||||||
|
Object options = read(capabilities.optionsField(), capabilities.minecraftServer());
|
||||||
|
Method hasMethod = CapabilityResolution.resolveMethod(options.getClass(), "has", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 1 && String.class.equals(params[0]);
|
||||||
|
});
|
||||||
|
boolean bonusChest = hasMethod != null && Boolean.TRUE.equals(hasMethod.invoke(options, "bonusChest"));
|
||||||
|
Object dataLoadOutput = capabilities.createNewWorldDataMethod().invoke(null, settings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
|
||||||
|
Method cookieMethod = CapabilityResolution.resolveMethod(dataLoadOutput.getClass(), "cookie", method -> method.getParameterCount() == 0);
|
||||||
|
if (cookieMethod == null) {
|
||||||
|
throw new IllegalStateException("WorldLoader.DataLoadOutput does not expose cookie().");
|
||||||
|
}
|
||||||
|
Object primaryLevelData = cookieMethod.invoke(dataLoadOutput);
|
||||||
|
|
||||||
|
Method checkNameMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "checkName", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 1 && String.class.equals(params[0]);
|
||||||
|
});
|
||||||
|
if (checkNameMethod != null) {
|
||||||
|
checkNameMethod.invoke(primaryLevelData, worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Method getModdedStatusMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getModdedStatus", method -> method.getParameterCount() == 0);
|
||||||
|
Method getServerModNameMethod = CapabilityResolution.resolveMethod(capabilities.minecraftServer().getClass(), "getServerModName", method -> method.getParameterCount() == 0);
|
||||||
|
if (getModdedStatusMethod != null && getServerModNameMethod != null) {
|
||||||
|
Object modCheck = getModdedStatusMethod.invoke(capabilities.minecraftServer());
|
||||||
|
Method shouldReportAsModifiedMethod = CapabilityResolution.resolveMethod(modCheck.getClass(), "shouldReportAsModified", method -> method.getParameterCount() == 0);
|
||||||
|
Method setModdedInfoMethod = CapabilityResolution.resolveMethod(primaryLevelData.getClass(), "setModdedInfo", method -> {
|
||||||
|
Class<?>[] params = method.getParameterTypes();
|
||||||
|
return params.length == 2 && String.class.equals(params[0]) && boolean.class.equals(params[1]);
|
||||||
|
});
|
||||||
|
if (shouldReportAsModifiedMethod != null && setModdedInfoMethod != null) {
|
||||||
|
boolean modified = Boolean.TRUE.equals(shouldReportAsModifiedMethod.invoke(modCheck));
|
||||||
|
String modName = (String) getServerModNameMethod.invoke(capabilities.minecraftServer());
|
||||||
|
setModdedInfoMethod.invoke(primaryLevelData, modName, modified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return primaryLevelData;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object createLegacyStorageAccess(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
|
||||||
|
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
|
||||||
|
Method createDefaultMethod = levelStorageSourceClass.getMethod("createDefault", Path.class);
|
||||||
|
Object levelStorageSource = createDefaultMethod.invoke(null, Bukkit.getWorldContainer().toPath());
|
||||||
|
Method storageAccessMethod = capabilities.levelStorageAccessMethod();
|
||||||
|
if (storageAccessMethod.getParameterCount() == 1) {
|
||||||
|
return storageAccessMethod.invoke(levelStorageSource, worldName);
|
||||||
|
}
|
||||||
|
Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
|
||||||
|
.getField("OVERWORLD")
|
||||||
|
.get(null);
|
||||||
|
return storageAccessMethod.invoke(levelStorageSource, worldName, overworldStemKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void closeLevelStorageAccess(Object levelStorageAccess) {
|
||||||
|
if (levelStorageAccess == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Method closeMethod = levelStorageAccess.getClass().getMethod("close");
|
||||||
|
closeMethod.invoke(levelStorageAccess);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean unloadWorld(CapabilitySnapshot capabilities, World world, boolean save) {
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Boolean> asyncUnload = unloadWorldViaAsyncApi(capabilities, world, save);
|
||||||
|
if (asyncUnload != null) {
|
||||||
|
return resolveAsyncUnload(asyncUnload);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Bukkit.unloadWorld(world, save);
|
||||||
|
} catch (UnsupportedOperationException unsupported) {
|
||||||
|
if (capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null) {
|
||||||
|
throw unsupported;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (save) {
|
||||||
|
world.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
Method getHandleMethod = world.getClass().getMethod("getHandle");
|
||||||
|
Object serverLevel = getHandleMethod.invoke(world);
|
||||||
|
closeServerLevel(world, serverLevel);
|
||||||
|
detachServerLevel(capabilities, serverLevel, world.getName());
|
||||||
|
return Bukkit.getWorld(world.getName()) == null;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new IllegalStateException("Failed to unload world \"" + world.getName() + "\" through the selected world lifecycle backend.", unwrap(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<Boolean> unloadWorldViaAsyncApi(CapabilitySnapshot capabilities, World world, boolean save) {
|
||||||
|
if (capabilities.unloadWorldAsyncMethod() == null || capabilities.bukkitServer() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Boolean> callbackFuture = new CompletableFuture<>();
|
||||||
|
Runnable invokeTask = () -> {
|
||||||
|
Consumer<Boolean> callback = result -> callbackFuture.complete(Boolean.TRUE.equals(result));
|
||||||
|
try {
|
||||||
|
capabilities.unloadWorldAsyncMethod().invoke(capabilities.bukkitServer(), world, save, callback);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
callbackFuture.completeExceptionally(unwrap(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (J.isFolia() && !isGlobalTickThread()) {
|
||||||
|
CompletableFuture<Void> scheduled = J.sfut(invokeTask);
|
||||||
|
if (scheduled == null) {
|
||||||
|
callbackFuture.completeExceptionally(new IllegalStateException("Failed to schedule global unload task."));
|
||||||
|
return callbackFuture;
|
||||||
|
}
|
||||||
|
scheduled.whenComplete((unused, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
callbackFuture.completeExceptionally(unwrap(throwable));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return callbackFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeTask.run();
|
||||||
|
return callbackFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean resolveAsyncUnload(CompletableFuture<Boolean> asyncUnload) {
|
||||||
|
if (J.isPrimaryThread()) {
|
||||||
|
if (!asyncUnload.isDone()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Boolean.TRUE.equals(asyncUnload.join());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new IllegalStateException("Failed to consume async world unload result.", unwrap(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Boolean.TRUE.equals(asyncUnload.get(120, TimeUnit.SECONDS));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new IllegalStateException("Failed while waiting for async world unload result.", unwrap(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void closeServerLevel(World world, Object serverLevel) throws Throwable {
|
||||||
|
Method closeMethod = CapabilityResolution.resolveMethod(serverLevel.getClass(), "close", method -> method.getParameterCount() == 0);
|
||||||
|
if (closeMethod == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!J.isFolia()) {
|
||||||
|
closeMethod.invoke(serverLevel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location spawn = world.getSpawnLocation();
|
||||||
|
int chunkX = spawn == null ? 0 : spawn.getBlockX() >> 4;
|
||||||
|
int chunkZ = spawn == null ? 0 : spawn.getBlockZ() >> 4;
|
||||||
|
CompletableFuture<Void> closeFuture = new CompletableFuture<>();
|
||||||
|
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||||
|
try {
|
||||||
|
closeMethod.invoke(serverLevel);
|
||||||
|
closeFuture.complete(null);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
closeFuture.completeExceptionally(unwrap(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
throw new IllegalStateException("Failed to schedule region close task for world \"" + world.getName() + "\".");
|
||||||
|
}
|
||||||
|
closeFuture.get(90, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private static void removeWorldFromCraftServerMap(String worldName) throws ReflectiveOperationException {
|
||||||
|
Object bukkitServer = Bukkit.getServer();
|
||||||
|
if (bukkitServer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Field worldsField = CapabilityResolution.resolveField(bukkitServer.getClass(), "worlds");
|
||||||
|
Object rawWorlds = worldsField.get(bukkitServer);
|
||||||
|
if (rawWorlds instanceof Map map) {
|
||||||
|
map.remove(worldName);
|
||||||
|
map.remove(worldName.toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void detachServerLevel(CapabilitySnapshot capabilities, Object serverLevel, String worldName) throws Throwable {
|
||||||
|
Runnable detachTask = () -> {
|
||||||
|
try {
|
||||||
|
capabilities.removeLevelMethod().invoke(capabilities.minecraftServer(), serverLevel);
|
||||||
|
removeWorldFromCraftServerMap(worldName);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!J.isFolia() || isGlobalTickThread()) {
|
||||||
|
detachTask.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Void> detachFuture = J.sfut(detachTask);
|
||||||
|
if (detachFuture == null) {
|
||||||
|
throw new IllegalStateException("Failed to schedule global detach task for world \"" + worldName + "\".");
|
||||||
|
}
|
||||||
|
detachFuture.get(15, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isGlobalTickThread() {
|
||||||
|
Object server = Bukkit.getServer();
|
||||||
|
if (server == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Method method = server.getClass().getMethod("isGlobalTickThread");
|
||||||
|
return Boolean.TRUE.equals(method.invoke(server));
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.WorldType;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
final class WorldsProviderBackend implements WorldLifecycleBackend {
|
||||||
|
private final CapabilitySnapshot capabilities;
|
||||||
|
|
||||||
|
WorldsProviderBackend(CapabilitySnapshot capabilities) {
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
||||||
|
return request.studio() && capabilities.hasWorldsProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||||
|
try {
|
||||||
|
Path worldPath = new File(Bukkit.getWorldContainer(), request.worldName()).toPath();
|
||||||
|
Object builder = WorldLifecycleSupport.invokeNamed(capabilities.worldsProvider(), "levelBuilder", new Class[]{Path.class}, worldPath);
|
||||||
|
builder = WorldLifecycleSupport.invokeNamed(builder, "name", new Class[]{String.class}, request.worldName());
|
||||||
|
builder = WorldLifecycleSupport.invokeNamed(builder, "seed", new Class[]{long.class}, request.seed());
|
||||||
|
builder = WorldLifecycleSupport.invokeNamed(builder, "levelStem", new Class[]{capabilities.worldsLevelStemClass()}, resolveLevelStem(request.environment()));
|
||||||
|
builder = WorldLifecycleSupport.invokeNamed(builder, "chunkGenerator", new Class[]{org.bukkit.generator.ChunkGenerator.class}, request.generator());
|
||||||
|
builder = WorldLifecycleSupport.invokeNamed(builder, "biomeProvider", new Class[]{org.bukkit.generator.BiomeProvider.class}, request.biomeProvider());
|
||||||
|
builder = WorldLifecycleSupport.invokeNamed(builder, "generatorType", new Class[]{capabilities.worldsGeneratorTypeClass()}, resolveGeneratorType(request.worldType()));
|
||||||
|
builder = WorldLifecycleSupport.invokeNamed(builder, "structures", new Class[]{boolean.class}, request.generateStructures());
|
||||||
|
builder = WorldLifecycleSupport.invokeNamed(builder, "hardcore", new Class[]{boolean.class}, request.hardcore());
|
||||||
|
Object levelBuilder = WorldLifecycleSupport.invokeNamed(builder, "build", new Class[0]);
|
||||||
|
Object async = WorldLifecycleSupport.invokeNamed(levelBuilder, "createAsync", new Class[0]);
|
||||||
|
if (async instanceof CompletableFuture<?> future) {
|
||||||
|
return future.thenApply(world -> (World) world);
|
||||||
|
}
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Worlds provider createAsync did not return CompletableFuture."));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return CompletableFuture.failedFuture(WorldLifecycleSupport.unwrap(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unload(World world, boolean save) {
|
||||||
|
return WorldLifecycleSupport.unloadWorld(capabilities, world, save);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String backendName() {
|
||||||
|
return "worlds_provider";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String describeSelectionReason() {
|
||||||
|
return "external Worlds provider is registered and healthy";
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private Object resolveLevelStem(World.Environment environment) {
|
||||||
|
String key;
|
||||||
|
if (environment == World.Environment.NETHER) {
|
||||||
|
key = "NETHER";
|
||||||
|
} else if (environment == World.Environment.THE_END) {
|
||||||
|
key = "END";
|
||||||
|
} else {
|
||||||
|
key = "OVERWORLD";
|
||||||
|
}
|
||||||
|
Class<? extends Enum> enumClass = capabilities.worldsLevelStemClass().asSubclass(Enum.class);
|
||||||
|
return Enum.valueOf(enumClass, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private Object resolveGeneratorType(WorldType worldType) {
|
||||||
|
String typeName = worldType == null ? "NORMAL" : worldType.getName();
|
||||||
|
String key;
|
||||||
|
if ("FLAT".equalsIgnoreCase(typeName)) {
|
||||||
|
key = "FLAT";
|
||||||
|
} else if ("AMPLIFIED".equalsIgnoreCase(typeName)) {
|
||||||
|
key = "AMPLIFIED";
|
||||||
|
} else if ("LARGE_BIOMES".equalsIgnoreCase(typeName) || "LARGEBIOMES".equalsIgnoreCase(typeName)) {
|
||||||
|
key = "LARGE_BIOMES";
|
||||||
|
} else {
|
||||||
|
key = "NORMAL";
|
||||||
|
}
|
||||||
|
Class<? extends Enum> enumClass = capabilities.worldsGeneratorTypeClass().asSubclass(Enum.class);
|
||||||
|
return Enum.valueOf(enumClass, key.toUpperCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,7 +157,7 @@ public abstract class ExternalDataProvider implements Listener {
|
|||||||
protected static List<BlockProperty> YAW_FACE_BIOME_PROPERTIES = List.of(
|
protected static List<BlockProperty> YAW_FACE_BIOME_PROPERTIES = List.of(
|
||||||
BlockProperty.ofEnum(BiomeColor.class, "matchBiome", null),
|
BlockProperty.ofEnum(BiomeColor.class, "matchBiome", null),
|
||||||
BlockProperty.ofBoolean("randomYaw", false),
|
BlockProperty.ofBoolean("randomYaw", false),
|
||||||
BlockProperty.ofFloat("yaw", 0, 0, 360f, false, true),
|
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
|
||||||
BlockProperty.ofBoolean("randomFace", true),
|
BlockProperty.ofBoolean("randomFace", true),
|
||||||
new BlockProperty(
|
new BlockProperty(
|
||||||
"face",
|
"face",
|
||||||
|
|||||||
@@ -1,841 +0,0 @@
|
|||||||
package art.arcane.iris.core.link;
|
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
|
||||||
import art.arcane.iris.core.nms.INMS;
|
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.Location;
|
|
||||||
import org.bukkit.Server;
|
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
|
||||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
|
||||||
import org.bukkit.World;
|
|
||||||
import org.bukkit.WorldCreator;
|
|
||||||
import org.bukkit.WorldType;
|
|
||||||
import org.bukkit.generator.ChunkGenerator;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
public class FoliaWorldsLink {
|
|
||||||
private static volatile FoliaWorldsLink instance;
|
|
||||||
private final Object provider;
|
|
||||||
private final Class<?> levelStemClass;
|
|
||||||
private final Class<?> generatorTypeClass;
|
|
||||||
private final Object minecraftServer;
|
|
||||||
private final Method minecraftServerCreateLevelMethod;
|
|
||||||
|
|
||||||
private FoliaWorldsLink(
|
|
||||||
Object provider,
|
|
||||||
Class<?> levelStemClass,
|
|
||||||
Class<?> generatorTypeClass,
|
|
||||||
Object minecraftServer,
|
|
||||||
Method minecraftServerCreateLevelMethod
|
|
||||||
) {
|
|
||||||
this.provider = provider;
|
|
||||||
this.levelStemClass = levelStemClass;
|
|
||||||
this.generatorTypeClass = generatorTypeClass;
|
|
||||||
this.minecraftServer = minecraftServer;
|
|
||||||
this.minecraftServerCreateLevelMethod = minecraftServerCreateLevelMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
||||||
public static FoliaWorldsLink get() {
|
|
||||||
FoliaWorldsLink current = instance;
|
|
||||||
if (current != null && current.isActive()) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (FoliaWorldsLink.class) {
|
|
||||||
if (instance != null && instance.isActive()) {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object loadedProvider = null;
|
|
||||||
Class<?> loadedLevelStemClass = null;
|
|
||||||
Class<?> loadedGeneratorTypeClass = null;
|
|
||||||
Object loadedMinecraftServer = null;
|
|
||||||
Method loadedMinecraftServerCreateLevelMethod = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
Server.class.getDeclaredMethod("isGlobalTickThread");
|
|
||||||
try {
|
|
||||||
Class<?> worldsProviderClass = Class.forName("net.thenextlvl.worlds.api.WorldsProvider");
|
|
||||||
loadedLevelStemClass = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem");
|
|
||||||
loadedGeneratorTypeClass = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType");
|
|
||||||
loadedProvider = Bukkit.getServicesManager().load((Class) worldsProviderClass);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
Object[] resolved = resolveProviderFromServices();
|
|
||||||
loadedProvider = resolved[0];
|
|
||||||
loadedLevelStemClass = (Class<?>) resolved[1];
|
|
||||||
loadedGeneratorTypeClass = (Class<?>) resolved[2];
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Object bukkitServer = Bukkit.getServer();
|
|
||||||
if (bukkitServer != null) {
|
|
||||||
Method getServerMethod = bukkitServer.getClass().getMethod("getServer");
|
|
||||||
Object candidateMinecraftServer = getServerMethod.invoke(bukkitServer);
|
|
||||||
Class<?> minecraftServerClass = Class.forName("net.minecraft.server.MinecraftServer");
|
|
||||||
if (minecraftServerClass.isInstance(candidateMinecraftServer)) {
|
|
||||||
loadedMinecraftServerCreateLevelMethod = minecraftServerClass.getMethod(
|
|
||||||
"createLevel",
|
|
||||||
Class.forName("net.minecraft.world.level.dimension.LevelStem"),
|
|
||||||
Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo"),
|
|
||||||
Class.forName("net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess"),
|
|
||||||
Class.forName("net.minecraft.world.level.storage.PrimaryLevelData")
|
|
||||||
);
|
|
||||||
loadedMinecraftServer = candidateMinecraftServer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = new FoliaWorldsLink(
|
|
||||||
loadedProvider,
|
|
||||||
loadedLevelStemClass,
|
|
||||||
loadedGeneratorTypeClass,
|
|
||||||
loadedMinecraftServer,
|
|
||||||
loadedMinecraftServerCreateLevelMethod
|
|
||||||
);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isActive() {
|
|
||||||
if (!J.isFolia()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isWorldsProviderActive() || isPaperWorldLoaderActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<World> createWorld(WorldCreator creator) {
|
|
||||||
if (isWorldsProviderActive()) {
|
|
||||||
CompletableFuture<World> providerFuture = createWorldViaProvider(creator);
|
|
||||||
if (providerFuture != null) {
|
|
||||||
return providerFuture;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPaperWorldLoaderActive()) {
|
|
||||||
return createWorldViaPaperWorldLoader(creator);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean unloadWorld(World world, boolean save) {
|
|
||||||
if (world == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<Boolean> asyncWorldUnload = unloadWorldViaAsyncApi(world, save);
|
|
||||||
if (asyncWorldUnload != null) {
|
|
||||||
return resolveAsyncUnload(asyncWorldUnload);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return Bukkit.unloadWorld(world, save);
|
|
||||||
} catch (UnsupportedOperationException unsupported) {
|
|
||||||
if (minecraftServer == null) {
|
|
||||||
throw unsupported;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (save) {
|
|
||||||
world.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
Object serverLevel = invoke(world, "getHandle");
|
|
||||||
closeServerLevel(world, serverLevel);
|
|
||||||
detachServerLevel(serverLevel, world.getName());
|
|
||||||
return Bukkit.getWorld(world.getName()) == null;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new IllegalStateException("Failed to unload world \"" + world.getName() + "\" via Folia runtime world-loader bridge.", unwrap(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean resolveAsyncUnload(CompletableFuture<Boolean> asyncWorldUnload) {
|
|
||||||
if (J.isPrimaryThread()) {
|
|
||||||
if (!asyncWorldUnload.isDone()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return Boolean.TRUE.equals(asyncWorldUnload.join());
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new IllegalStateException("Failed to consume async world unload result.", unwrap(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return Boolean.TRUE.equals(asyncWorldUnload.get(120, TimeUnit.SECONDS));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new IllegalStateException("Failed while waiting for async world unload result.", unwrap(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CompletableFuture<Boolean> unloadWorldViaAsyncApi(World world, boolean save) {
|
|
||||||
Object bukkitServer = Bukkit.getServer();
|
|
||||||
if (bukkitServer == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Method unloadWorldAsyncMethod;
|
|
||||||
try {
|
|
||||||
unloadWorldAsyncMethod = bukkitServer.getClass().getMethod("unloadWorldAsync", World.class, boolean.class, Consumer.class);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<Boolean> callbackFuture = new CompletableFuture<>();
|
|
||||||
Runnable invokeTask = () -> {
|
|
||||||
Consumer<Boolean> callback = result -> callbackFuture.complete(Boolean.TRUE.equals(result));
|
|
||||||
try {
|
|
||||||
unloadWorldAsyncMethod.invoke(bukkitServer, world, save, callback);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
callbackFuture.completeExceptionally(unwrap(e));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (J.isFolia() && !isGlobalTickThread()) {
|
|
||||||
CompletableFuture<Void> scheduled = J.sfut(invokeTask);
|
|
||||||
if (scheduled == null) {
|
|
||||||
callbackFuture.completeExceptionally(new IllegalStateException("Failed to schedule global world-unload task."));
|
|
||||||
return callbackFuture;
|
|
||||||
}
|
|
||||||
scheduled.whenComplete((unused, throwable) -> {
|
|
||||||
if (throwable != null) {
|
|
||||||
callbackFuture.completeExceptionally(unwrap(throwable));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
invokeTask.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
return callbackFuture;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isWorldsProviderActive() {
|
|
||||||
return provider != null && levelStemClass != null && generatorTypeClass != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPaperWorldLoaderActive() {
|
|
||||||
return minecraftServer != null && minecraftServerCreateLevelMethod != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CompletableFuture<World> createWorldViaProvider(WorldCreator creator) {
|
|
||||||
try {
|
|
||||||
Path worldPath = new File(Bukkit.getWorldContainer(), creator.name()).toPath();
|
|
||||||
Object builder = invoke(provider, "levelBuilder", worldPath);
|
|
||||||
builder = invoke(builder, "name", creator.name());
|
|
||||||
builder = invoke(builder, "seed", creator.seed());
|
|
||||||
builder = invoke(builder, "levelStem", resolveLevelStem(creator.environment()));
|
|
||||||
builder = invoke(builder, "chunkGenerator", creator.generator());
|
|
||||||
builder = invoke(builder, "biomeProvider", creator.biomeProvider());
|
|
||||||
builder = invoke(builder, "generatorType", resolveGeneratorType(creator.type()));
|
|
||||||
builder = invoke(builder, "structures", creator.generateStructures());
|
|
||||||
builder = invoke(builder, "hardcore", creator.hardcore());
|
|
||||||
Object levelBuilder = invoke(builder, "build");
|
|
||||||
Object async = invoke(levelBuilder, "createAsync");
|
|
||||||
if (async instanceof CompletableFuture<?> future) {
|
|
||||||
return future.thenApply(world -> (World) world);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CompletableFuture.failedFuture(new IllegalStateException("Worlds provider createAsync did not return CompletableFuture."));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
return CompletableFuture.failedFuture(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CompletableFuture<World> createWorldViaPaperWorldLoader(WorldCreator creator) {
|
|
||||||
Object levelStorageAccess = null;
|
|
||||||
try {
|
|
||||||
if (creator.environment() != World.Environment.NORMAL) {
|
|
||||||
return CompletableFuture.failedFuture(new UnsupportedOperationException("PaperWorldLoader fallback only supports OVERWORLD worlds."));
|
|
||||||
}
|
|
||||||
|
|
||||||
World existing = Bukkit.getWorld(creator.name());
|
|
||||||
if (existing != null) {
|
|
||||||
return CompletableFuture.completedFuture(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
stageRuntimeGenerator(creator);
|
|
||||||
levelStorageAccess = createRuntimeStorageAccess(creator.name());
|
|
||||||
Object primaryLevelData = createPrimaryLevelData(levelStorageAccess, creator.name());
|
|
||||||
Object runtimeStemKey = createRuntimeLevelStemKey(creator.name());
|
|
||||||
Object worldLoadingInfo = createWorldLoadingInfo(creator.name(), runtimeStemKey);
|
|
||||||
Object levelStem = resolveCreateLevelStem(creator);
|
|
||||||
Object[] createLevelArgs = new Object[]{levelStem, worldLoadingInfo, levelStorageAccess, primaryLevelData};
|
|
||||||
Method createLevelMethod = minecraftServerCreateLevelMethod;
|
|
||||||
if (createLevelMethod == null || !matches(createLevelMethod.getParameterTypes(), createLevelArgs)) {
|
|
||||||
createLevelMethod = resolveMethod(minecraftServer.getClass(), "createLevel", createLevelArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
createLevelMethod.invoke(minecraftServer, createLevelArgs);
|
|
||||||
} catch (IllegalArgumentException exception) {
|
|
||||||
throw new IllegalStateException("createLevel argument mismatch. Method=" + formatMethod(createLevelMethod) + " Args=" + formatArgs(createLevelArgs), exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
World loaded = Bukkit.getWorld(creator.name());
|
|
||||||
if (loaded == null) {
|
|
||||||
Iris.clearStagedRuntimeWorldGenerator(creator.name());
|
|
||||||
closeLevelStorageAccess(levelStorageAccess);
|
|
||||||
return CompletableFuture.failedFuture(new IllegalStateException("PaperWorldLoader did not load world \"" + creator.name() + "\"."));
|
|
||||||
}
|
|
||||||
|
|
||||||
Iris.clearStagedRuntimeWorldGenerator(creator.name());
|
|
||||||
return CompletableFuture.completedFuture(loaded);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Iris.clearStagedRuntimeWorldGenerator(creator.name());
|
|
||||||
closeLevelStorageAccess(levelStorageAccess);
|
|
||||||
return CompletableFuture.failedFuture(unwrap(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object createRuntimeStorageAccess(String worldName) throws ReflectiveOperationException {
|
|
||||||
Class<?> levelStorageSourceClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource");
|
|
||||||
Object levelStorageSource = levelStorageSourceClass
|
|
||||||
.getMethod("createDefault", Path.class)
|
|
||||||
.invoke(null, Bukkit.getWorldContainer().toPath());
|
|
||||||
|
|
||||||
Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
|
|
||||||
.getField("OVERWORLD")
|
|
||||||
.get(null);
|
|
||||||
Method validateAndCreateAccess = resolveMethod(levelStorageSourceClass, "validateAndCreateAccess", worldName, overworldStemKey);
|
|
||||||
return validateAndCreateAccess.invoke(levelStorageSource, worldName, overworldStemKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object createPrimaryLevelData(Object levelStorageAccess, String worldName) throws ReflectiveOperationException {
|
|
||||||
Class<?> paperWorldLoaderClass = Class.forName("io.papermc.paper.world.PaperWorldLoader");
|
|
||||||
Class<?> levelStorageAccessClass = Class.forName("net.minecraft.world.level.storage.LevelStorageSource$LevelStorageAccess");
|
|
||||||
Object levelDataResult = paperWorldLoaderClass
|
|
||||||
.getMethod("getLevelData", levelStorageAccessClass)
|
|
||||||
.invoke(null, levelStorageAccess);
|
|
||||||
boolean fatalError = (boolean) invoke(levelDataResult, "fatalError");
|
|
||||||
if (fatalError) {
|
|
||||||
throw new IllegalStateException("PaperWorldLoader reported a fatal world-data error for \"" + worldName + "\".");
|
|
||||||
}
|
|
||||||
|
|
||||||
Object dataTag = invoke(levelDataResult, "dataTag");
|
|
||||||
if (dataTag != null) {
|
|
||||||
throw new IllegalStateException("Runtime studio world folder \"" + worldName + "\" already contains level data.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Object worldLoaderContext = getPublicField(minecraftServer, "worldLoaderContext");
|
|
||||||
Object datapackDimensions = invoke(worldLoaderContext, "datapackDimensions");
|
|
||||||
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
|
|
||||||
.getField("LEVEL_STEM")
|
|
||||||
.get(null);
|
|
||||||
Object levelStemRegistry = invoke(datapackDimensions, "lookupOrThrow", levelStemRegistryKey);
|
|
||||||
Object dedicatedSettings = getPublicField(minecraftServer, "settings");
|
|
||||||
boolean demo = (boolean) invoke(minecraftServer, "isDemo");
|
|
||||||
Object options = getPublicField(minecraftServer, "options");
|
|
||||||
boolean bonusChest = (boolean) invoke(options, "has", "bonusChest");
|
|
||||||
|
|
||||||
Class<?> mainClass = Class.forName("net.minecraft.server.Main");
|
|
||||||
Method createNewWorldDataMethod = resolveMethod(mainClass, "createNewWorldData", dedicatedSettings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
|
|
||||||
Object dataLoadOutput = createNewWorldDataMethod.invoke(null, dedicatedSettings, worldLoaderContext, levelStemRegistry, demo, bonusChest);
|
|
||||||
|
|
||||||
Object primaryLevelData = invoke(dataLoadOutput, "cookie");
|
|
||||||
invoke(primaryLevelData, "checkName", worldName);
|
|
||||||
Object modCheck = invoke(minecraftServer, "getModdedStatus");
|
|
||||||
boolean modified = (boolean) invoke(modCheck, "shouldReportAsModified");
|
|
||||||
String modName = (String) invoke(minecraftServer, "getServerModName");
|
|
||||||
invoke(primaryLevelData, "setModdedInfo", modName, modified);
|
|
||||||
return primaryLevelData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object createWorldLoadingInfo(String worldName, Object runtimeStemKey) throws ReflectiveOperationException {
|
|
||||||
Class<?> worldLoadingInfoClass = Class.forName("io.papermc.paper.world.PaperWorldLoader$WorldLoadingInfo");
|
|
||||||
Constructor<?> constructor = resolveConstructor(worldLoadingInfoClass, 0, worldName, "normal", runtimeStemKey, true);
|
|
||||||
return constructor.newInstance(0, worldName, "normal", runtimeStemKey, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object createRuntimeLevelStemKey(String worldName) throws ReflectiveOperationException {
|
|
||||||
String sanitized = worldName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9/_-]", "_");
|
|
||||||
String path = "runtime/" + sanitized;
|
|
||||||
Object identifier = Class.forName("net.minecraft.resources.Identifier")
|
|
||||||
.getMethod("fromNamespaceAndPath", String.class, String.class)
|
|
||||||
.invoke(null, "iris", path);
|
|
||||||
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
|
|
||||||
.getField("LEVEL_STEM")
|
|
||||||
.get(null);
|
|
||||||
Class<?> resourceKeyClass = Class.forName("net.minecraft.resources.ResourceKey");
|
|
||||||
Method createMethod = resolveMethod(resourceKeyClass, "create", levelStemRegistryKey, identifier);
|
|
||||||
return createMethod.invoke(null, levelStemRegistryKey, identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object resolveCreateLevelStem(WorldCreator creator) throws ReflectiveOperationException {
|
|
||||||
Object irisLevelStem = resolveIrisLevelStem(creator);
|
|
||||||
if (irisLevelStem != null) {
|
|
||||||
return irisLevelStem;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getOverworldLevelStem();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object resolveIrisLevelStem(WorldCreator creator) throws ReflectiveOperationException {
|
|
||||||
ChunkGenerator generator = creator.generator();
|
|
||||||
if (!(generator instanceof PlatformChunkGenerator)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object registryAccess = invoke(minecraftServer, "registryAccess");
|
|
||||||
Object binding = INMS.get();
|
|
||||||
Method levelStemMethod;
|
|
||||||
try {
|
|
||||||
levelStemMethod = resolveMethod(binding.getClass(), "levelStem", registryAccess, generator);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
throw new IllegalStateException("Iris NMS binding does not expose levelStem(RegistryAccess, ChunkGenerator) for runtime world \"" + creator.name() + "\".", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object levelStem;
|
|
||||||
try {
|
|
||||||
levelStem = levelStemMethod.invoke(binding, registryAccess, generator);
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
Throwable cause = unwrap(e);
|
|
||||||
throw new IllegalStateException("Iris failed to resolve runtime level stem for world \"" + creator.name() + "\".", cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (levelStem == null) {
|
|
||||||
throw new IllegalStateException("Iris resolved a null runtime level stem for world \"" + creator.name() + "\".");
|
|
||||||
}
|
|
||||||
return levelStem;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object getOverworldLevelStem() throws ReflectiveOperationException {
|
|
||||||
Object levelStemRegistryKey = Class.forName("net.minecraft.core.registries.Registries")
|
|
||||||
.getField("LEVEL_STEM")
|
|
||||||
.get(null);
|
|
||||||
Object registryAccess = invoke(minecraftServer, "registryAccess");
|
|
||||||
Object levelStemRegistry = invoke(registryAccess, "lookupOrThrow", levelStemRegistryKey);
|
|
||||||
Object overworldStemKey = Class.forName("net.minecraft.world.level.dimension.LevelStem")
|
|
||||||
.getField("OVERWORLD")
|
|
||||||
.get(null);
|
|
||||||
Object levelStem;
|
|
||||||
try {
|
|
||||||
levelStem = invoke(levelStemRegistry, "getValue", overworldStemKey);
|
|
||||||
} catch (NoSuchMethodException ignored) {
|
|
||||||
Object rawLevelStem = invoke(levelStemRegistry, "get", overworldStemKey);
|
|
||||||
levelStem = extractRegistryValue(rawLevelStem);
|
|
||||||
}
|
|
||||||
if (levelStem == null) {
|
|
||||||
throw new IllegalStateException("Unable to resolve OVERWORLD LevelStem from registry.");
|
|
||||||
}
|
|
||||||
return levelStem;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object extractRegistryValue(Object rawValue) throws ReflectiveOperationException {
|
|
||||||
if (rawValue == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (rawValue instanceof java.util.Optional<?> optionalValue) {
|
|
||||||
Object nestedValue = optionalValue.orElse(null);
|
|
||||||
if (nestedValue == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return extractRegistryValue(nestedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Method valueMethod = rawValue.getClass().getMethod("value");
|
|
||||||
return valueMethod.invoke(rawValue);
|
|
||||||
} catch (NoSuchMethodException ignored) {
|
|
||||||
return rawValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object getPublicField(Object target, String fieldName) throws ReflectiveOperationException {
|
|
||||||
Field field = target.getClass().getField(fieldName);
|
|
||||||
return field.get(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void closeLevelStorageAccess(Object levelStorageAccess) {
|
|
||||||
if (levelStorageAccess == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Method close = levelStorageAccess.getClass().getMethod("close");
|
|
||||||
close.invoke(levelStorageAccess);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void stageRuntimeGenerator(WorldCreator creator) throws ReflectiveOperationException {
|
|
||||||
ChunkGenerator generator = creator.generator();
|
|
||||||
if (generator == null) {
|
|
||||||
throw new IllegalStateException("Runtime world creation requires a non-null chunk generator.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Iris.stageRuntimeWorldGenerator(creator.name(), generator, creator.biomeProvider());
|
|
||||||
Object bukkitServer = Bukkit.getServer();
|
|
||||||
if (bukkitServer == null) {
|
|
||||||
throw new IllegalStateException("Bukkit server is unavailable.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Field configurationField = bukkitServer.getClass().getDeclaredField("configuration");
|
|
||||||
configurationField.setAccessible(true);
|
|
||||||
Object rawConfiguration = configurationField.get(bukkitServer);
|
|
||||||
if (!(rawConfiguration instanceof YamlConfiguration configuration)) {
|
|
||||||
throw new IllegalStateException("CraftServer configuration field is unavailable.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigurationSection worldsSection = configuration.getConfigurationSection("worlds");
|
|
||||||
if (worldsSection == null) {
|
|
||||||
worldsSection = configuration.createSection("worlds");
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfigurationSection worldSection = worldsSection.getConfigurationSection(creator.name());
|
|
||||||
if (worldSection == null) {
|
|
||||||
worldSection = worldsSection.createSection(creator.name());
|
|
||||||
}
|
|
||||||
|
|
||||||
worldSection.set("generator", "Iris:runtime");
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
||||||
private static void removeWorldFromCraftServerMap(String worldName) throws ReflectiveOperationException {
|
|
||||||
Object bukkitServer = Bukkit.getServer();
|
|
||||||
if (bukkitServer == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Field worldsField = bukkitServer.getClass().getDeclaredField("worlds");
|
|
||||||
worldsField.setAccessible(true);
|
|
||||||
Object worldsRaw = worldsField.get(bukkitServer);
|
|
||||||
if (worldsRaw instanceof Map worldsMap) {
|
|
||||||
worldsMap.remove(worldName);
|
|
||||||
worldsMap.remove(worldName.toLowerCase(Locale.ROOT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void closeServerLevel(World world, Object serverLevel) throws Throwable {
|
|
||||||
Method closeLevelMethod = resolveMethod(serverLevel.getClass(), "close");
|
|
||||||
if (!J.isFolia()) {
|
|
||||||
closeLevelMethod.invoke(serverLevel);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Location spawn = world.getSpawnLocation();
|
|
||||||
int chunkX = spawn == null ? 0 : spawn.getBlockX() >> 4;
|
|
||||||
int chunkZ = spawn == null ? 0 : spawn.getBlockZ() >> 4;
|
|
||||||
CompletableFuture<Void> closeFuture = new CompletableFuture<>();
|
|
||||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
|
||||||
try {
|
|
||||||
closeLevelMethod.invoke(serverLevel);
|
|
||||||
closeFuture.complete(null);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
closeFuture.completeExceptionally(unwrap(e));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!scheduled) {
|
|
||||||
throw new IllegalStateException("Failed to schedule region close task for world \"" + world.getName() + "\".");
|
|
||||||
}
|
|
||||||
closeFuture.get(90, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void detachServerLevel(Object serverLevel, String worldName) throws Throwable {
|
|
||||||
Runnable detachTask = () -> {
|
|
||||||
try {
|
|
||||||
Method removeLevelMethod = resolveMethod(minecraftServer.getClass(), "removeLevel", serverLevel);
|
|
||||||
removeLevelMethod.invoke(minecraftServer, serverLevel);
|
|
||||||
removeWorldFromCraftServerMap(worldName);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!J.isFolia()) {
|
|
||||||
detachTask.run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGlobalTickThread()) {
|
|
||||||
detachTask.run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<Void> detachFuture = J.sfut(() -> detachTask.run());
|
|
||||||
if (detachFuture == null) {
|
|
||||||
throw new IllegalStateException("Failed to schedule global detach task for world \"" + worldName + "\".");
|
|
||||||
}
|
|
||||||
detachFuture.get(15, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isGlobalTickThread() {
|
|
||||||
Server server = Bukkit.getServer();
|
|
||||||
if (server == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Method isGlobalTickThreadMethod = server.getClass().getMethod("isGlobalTickThread");
|
|
||||||
return Boolean.TRUE.equals(isGlobalTickThreadMethod.invoke(server));
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Throwable unwrap(Throwable throwable) {
|
|
||||||
if (throwable instanceof InvocationTargetException invocationTargetException && invocationTargetException.getCause() != null) {
|
|
||||||
return unwrap(invocationTargetException.getCause());
|
|
||||||
}
|
|
||||||
if (throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null) {
|
|
||||||
return unwrap(completionException.getCause());
|
|
||||||
}
|
|
||||||
if (throwable instanceof java.util.concurrent.ExecutionException executionException && executionException.getCause() != null) {
|
|
||||||
return unwrap(executionException.getCause());
|
|
||||||
}
|
|
||||||
return throwable;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object[] resolveProviderFromServices() {
|
|
||||||
Object provider = null;
|
|
||||||
Class<?> levelStem = null;
|
|
||||||
Class<?> generatorType = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
Collection<Class<?>> knownServices = Bukkit.getServicesManager().getKnownServices();
|
|
||||||
for (Class<?> serviceClass : knownServices) {
|
|
||||||
if (!"net.thenextlvl.worlds.api.WorldsProvider".equals(serviceClass.getName())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisteredServiceProvider<?> registration = Bukkit.getServicesManager().getRegistration((Class) serviceClass);
|
|
||||||
if (registration == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
provider = registration.getProvider();
|
|
||||||
ClassLoader loader = serviceClass.getClassLoader();
|
|
||||||
if (loader == null && provider != null) {
|
|
||||||
loader = provider.getClass().getClassLoader();
|
|
||||||
}
|
|
||||||
if (loader != null) {
|
|
||||||
levelStem = Class.forName("net.thenextlvl.worlds.api.generator.LevelStem", false, loader);
|
|
||||||
generatorType = Class.forName("net.thenextlvl.worlds.api.generator.GeneratorType", false, loader);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Object[]{provider, levelStem, generatorType};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object resolveLevelStem(World.Environment environment) {
|
|
||||||
String key;
|
|
||||||
if (environment == World.Environment.NETHER) {
|
|
||||||
key = "NETHER";
|
|
||||||
} else if (environment == World.Environment.THE_END) {
|
|
||||||
key = "END";
|
|
||||||
} else {
|
|
||||||
key = "OVERWORLD";
|
|
||||||
}
|
|
||||||
|
|
||||||
return enumValue(levelStemClass, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object resolveGeneratorType(WorldType worldType) {
|
|
||||||
String typeName = worldType == null ? "NORMAL" : worldType.getName();
|
|
||||||
String key;
|
|
||||||
if ("FLAT".equalsIgnoreCase(typeName)) {
|
|
||||||
key = "FLAT";
|
|
||||||
} else if ("AMPLIFIED".equalsIgnoreCase(typeName)) {
|
|
||||||
key = "AMPLIFIED";
|
|
||||||
} else if ("LARGE_BIOMES".equalsIgnoreCase(typeName) || "LARGEBIOMES".equalsIgnoreCase(typeName)) {
|
|
||||||
key = "LARGE_BIOMES";
|
|
||||||
} else {
|
|
||||||
key = "NORMAL";
|
|
||||||
}
|
|
||||||
|
|
||||||
return enumValue(generatorTypeClass, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
||||||
private static Object enumValue(Class<?> enumClass, String key) {
|
|
||||||
Class<? extends Enum> typed = enumClass.asSubclass(Enum.class);
|
|
||||||
return Enum.valueOf(typed, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Method resolveMethod(Class<?> owner, String methodName, Object... args) throws NoSuchMethodException {
|
|
||||||
Method selected = findMatchingMethod(owner.getMethods(), methodName, args);
|
|
||||||
if (selected != null) {
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
Class<?> current = owner;
|
|
||||||
while (current != null) {
|
|
||||||
selected = findMatchingMethod(current.getDeclaredMethods(), methodName, args);
|
|
||||||
if (selected != null) {
|
|
||||||
selected.setAccessible(true);
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
current = current.getSuperclass();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NoSuchMethodException(owner.getName() + "#" + methodName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Constructor<?> resolveConstructor(Class<?> owner, Object... args) throws NoSuchMethodException {
|
|
||||||
Constructor<?> selected = findMatchingConstructor(owner.getConstructors(), args);
|
|
||||||
if (selected != null) {
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
selected = findMatchingConstructor(owner.getDeclaredConstructors(), args);
|
|
||||||
if (selected != null) {
|
|
||||||
selected.setAccessible(true);
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NoSuchMethodException(owner.getName() + "#<init>");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Method findMatchingMethod(Method[] methods, String methodName, Object... args) {
|
|
||||||
Method selected = null;
|
|
||||||
for (Method method : methods) {
|
|
||||||
if (!method.getName().equals(methodName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Class<?>[] params = method.getParameterTypes();
|
|
||||||
if (params.length != args.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (matches(params, args)) {
|
|
||||||
selected = method;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String formatMethod(Method method) {
|
|
||||||
if (method == null) {
|
|
||||||
return "<null>";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
builder.append(method.getDeclaringClass().getName())
|
|
||||||
.append("#")
|
|
||||||
.append(method.getName())
|
|
||||||
.append("(");
|
|
||||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
|
||||||
for (int i = 0; i < parameterTypes.length; i++) {
|
|
||||||
if (i > 0) {
|
|
||||||
builder.append(", ");
|
|
||||||
}
|
|
||||||
builder.append(parameterTypes[i].getName());
|
|
||||||
}
|
|
||||||
builder.append(")");
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String formatArgs(Object... args) {
|
|
||||||
if (args == null) {
|
|
||||||
return "<null>";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
builder.append("[");
|
|
||||||
for (int i = 0; i < args.length; i++) {
|
|
||||||
if (i > 0) {
|
|
||||||
builder.append(", ");
|
|
||||||
}
|
|
||||||
Object argument = args[i];
|
|
||||||
builder.append(argument == null ? "null" : argument.getClass().getName());
|
|
||||||
}
|
|
||||||
builder.append("]");
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Constructor<?> findMatchingConstructor(Constructor<?>[] constructors, Object... args) {
|
|
||||||
Constructor<?> selected = null;
|
|
||||||
for (Constructor<?> constructor : constructors) {
|
|
||||||
Class<?>[] params = constructor.getParameterTypes();
|
|
||||||
if (params.length != args.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (matches(params, args)) {
|
|
||||||
selected = constructor;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Object invoke(Object target, String methodName, Object... args) throws ReflectiveOperationException {
|
|
||||||
Method selected = resolveMethod(target.getClass(), methodName, args);
|
|
||||||
return selected.invoke(target, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean matches(Class<?>[] params, Object[] args) {
|
|
||||||
for (int i = 0; i < params.length; i++) {
|
|
||||||
Object arg = args[i];
|
|
||||||
Class<?> parameterType = params[i];
|
|
||||||
if (arg == null) {
|
|
||||||
if (parameterType.isPrimitive()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Class<?> boxedParameterType = box(parameterType);
|
|
||||||
if (!boxedParameterType.isAssignableFrom(arg.getClass())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Class<?> box(Class<?> type) {
|
|
||||||
if (!type.isPrimitive()) {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
if (type == boolean.class) {
|
|
||||||
return Boolean.class;
|
|
||||||
}
|
|
||||||
if (type == byte.class) {
|
|
||||||
return Byte.class;
|
|
||||||
}
|
|
||||||
if (type == short.class) {
|
|
||||||
return Short.class;
|
|
||||||
}
|
|
||||||
if (type == int.class) {
|
|
||||||
return Integer.class;
|
|
||||||
}
|
|
||||||
if (type == long.class) {
|
|
||||||
return Long.class;
|
|
||||||
}
|
|
||||||
if (type == float.class) {
|
|
||||||
return Float.class;
|
|
||||||
}
|
|
||||||
if (type == double.class) {
|
|
||||||
return Double.class;
|
|
||||||
}
|
|
||||||
if (type == char.class) {
|
|
||||||
return Character.class;
|
|
||||||
}
|
|
||||||
return Void.class;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package art.arcane.iris.core.link.data;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.link.ExternalDataProvider;
|
||||||
|
import art.arcane.iris.core.link.Identifier;
|
||||||
|
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||||
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.util.common.data.B;
|
||||||
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
|
||||||
|
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
|
||||||
|
import net.momirealms.craftengine.core.block.ImmutableBlockState;
|
||||||
|
import net.momirealms.craftengine.core.block.properties.BooleanProperty;
|
||||||
|
import net.momirealms.craftengine.core.block.properties.IntegerProperty;
|
||||||
|
import net.momirealms.craftengine.core.block.properties.Property;
|
||||||
|
import net.momirealms.craftengine.core.util.Key;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.MissingResourceException;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class CraftEngineDataProvider extends ExternalDataProvider {
|
||||||
|
private static final BlockProperty[] FURNITURE_PROPERTIES = new BlockProperty[]{
|
||||||
|
BlockProperty.ofBoolean("randomYaw", false),
|
||||||
|
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
|
||||||
|
BlockProperty.ofBoolean("randomPitch", false),
|
||||||
|
BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true),
|
||||||
|
};
|
||||||
|
|
||||||
|
public CraftEngineDataProvider() {
|
||||||
|
super("CraftEngine");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
|
||||||
|
Key key = Key.of(blockId.namespace(), blockId.key());
|
||||||
|
net.momirealms.craftengine.core.block.CustomBlock block = CraftEngineBlocks.byId(key);
|
||||||
|
if (block != null) {
|
||||||
|
return block.properties().stream().map(CraftEngineDataProvider::convert).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
|
||||||
|
if (furniture != null) {
|
||||||
|
BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5);
|
||||||
|
properties[4] = new BlockProperty(
|
||||||
|
"variant",
|
||||||
|
String.class,
|
||||||
|
furniture.anyVariantName(),
|
||||||
|
furniture.variants().keySet(),
|
||||||
|
Function.identity()
|
||||||
|
);
|
||||||
|
return List.of(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
|
||||||
|
net.momirealms.craftengine.core.item.CustomItem<ItemStack> item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
|
||||||
|
if (item == null) {
|
||||||
|
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.buildItemStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
|
||||||
|
Key key = Key.of(blockId.namespace(), blockId.key());
|
||||||
|
if (CraftEngineBlocks.byId(key) == null && CraftEngineFurniture.byId(key) == null) {
|
||||||
|
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
|
||||||
|
}
|
||||||
|
|
||||||
|
return IrisCustomData.of(B.getAir(), ExternalDataSVC.buildState(blockId, state));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
|
||||||
|
art.arcane.iris.core.nms.container.Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
|
||||||
|
Identifier baseBlockId = statePair.getA();
|
||||||
|
KMap<String, String> state = statePair.getB();
|
||||||
|
Key key = Key.of(baseBlockId.namespace(), baseBlockId.key());
|
||||||
|
|
||||||
|
net.momirealms.craftengine.core.block.CustomBlock customBlock = CraftEngineBlocks.byId(key);
|
||||||
|
if (customBlock != null) {
|
||||||
|
ImmutableBlockState blockState = customBlock.defaultState();
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : state.entrySet()) {
|
||||||
|
Property<?> property = customBlock.getProperty(entry.getKey());
|
||||||
|
if (property == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparable<?> tag = property.optional(entry.getValue()).orElse(null);
|
||||||
|
if (tag == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockState = ImmutableBlockState.with(blockState, property, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
CraftEngineBlocks.place(block.getLocation(), blockState, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
net.momirealms.craftengine.core.entity.furniture.CustomFurniture furniture = CraftEngineFurniture.byId(key);
|
||||||
|
if (furniture == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Location location = parseYawAndPitch(engine, block, state);
|
||||||
|
String variant = state.getOrDefault("variant", furniture.anyVariantName());
|
||||||
|
CraftEngineFurniture.place(location, furniture, variant, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map<String, String> state) {
|
||||||
|
Location location = block.getLocation();
|
||||||
|
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
|
||||||
|
RNG rng = new RNG(seed);
|
||||||
|
|
||||||
|
if ("true".equals(state.get("randomYaw"))) {
|
||||||
|
location.setYaw(rng.f(0, 360));
|
||||||
|
} else if (state.containsKey("yaw")) {
|
||||||
|
location.setYaw(Float.parseFloat(state.get("yaw")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("true".equals(state.get("randomPitch"))) {
|
||||||
|
location.setPitch(rng.f(0, 360));
|
||||||
|
} else if (state.containsKey("pitch")) {
|
||||||
|
location.setPitch(Float.parseFloat(state.get("pitch")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
|
||||||
|
Stream<Key> keys = switch (dataType) {
|
||||||
|
case ENTITY -> Stream.<Key>empty();
|
||||||
|
case ITEM -> CraftEngineItems.loadedItems().keySet().stream();
|
||||||
|
case BLOCK -> Stream.concat(CraftEngineBlocks.loadedBlocks().keySet().stream(),
|
||||||
|
CraftEngineFurniture.loadedFurniture().keySet().stream());
|
||||||
|
};
|
||||||
|
return keys.map(key -> new Identifier(key.namespace(), key.value())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
|
||||||
|
Key key = Key.of(id.namespace(), id.key());
|
||||||
|
return switch (dataType) {
|
||||||
|
case ENTITY -> false;
|
||||||
|
case ITEM -> CraftEngineItems.byId(key) != null;
|
||||||
|
case BLOCK -> CraftEngineBlocks.byId(key) != null || CraftEngineFurniture.byId(key) != null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Comparable<T>> BlockProperty convert(Property<T> raw) {
|
||||||
|
return switch (raw) {
|
||||||
|
case BooleanProperty property -> BlockProperty.ofBoolean(property.name(), property.defaultValue());
|
||||||
|
case IntegerProperty property -> BlockProperty.ofLong(property.name(), property.defaultValue(), property.min, property.max, false, false);
|
||||||
|
default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ import art.arcane.iris.core.IrisSettings;
|
|||||||
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class INMS {
|
public class INMS {
|
||||||
private static final Version CURRENT = Boolean.getBoolean("iris.no-version-limit") ?
|
private static final Version CURRENT = Boolean.getBoolean("iris.no-version-limit") ?
|
||||||
@@ -69,51 +71,42 @@ public class INMS {
|
|||||||
|
|
||||||
private static INMSBinding bind() {
|
private static INMSBinding bind() {
|
||||||
String code = getNMSTag();
|
String code = getNMSTag();
|
||||||
Iris.info("Locating NMS Binding for " + code);
|
boolean disableNms = IrisSettings.get().getGeneral().isDisableNMS();
|
||||||
|
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes(code, disableNms, getFallbackBindingCodes());
|
||||||
try {
|
if ("BUKKIT".equals(code) && !disableNms) {
|
||||||
Class<?> clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding");
|
Iris.info("NMS tag resolution fell back to Bukkit; probing supported revision bindings.");
|
||||||
try {
|
|
||||||
Object b = clazz.getConstructor().newInstance();
|
|
||||||
if (b instanceof INMSBinding binding) {
|
|
||||||
Iris.info("Craftbukkit " + code + " <-> " + b.getClass().getSimpleName() + " Successfully Bound");
|
|
||||||
return binding;
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Iris.reportError(e);
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} catch (ClassNotFoundException | NoClassDefFoundError classNotFoundException) {
|
|
||||||
Iris.warn("Failed to load NMS binding class for " + code + ": " + classNotFoundException.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IrisSettings.get().getGeneral().isDisableNMS()) {
|
for (int i = 0; i < probeCodes.size(); i++) {
|
||||||
|
INMSBinding resolvedBinding = tryBind(probeCodes.get(i), i == 0);
|
||||||
|
if (resolvedBinding != null) {
|
||||||
|
return resolvedBinding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableNms) {
|
||||||
Iris.info("Craftbukkit " + code + " <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
|
Iris.info("Craftbukkit " + code + " <-> " + NMSBinding1X.class.getSimpleName() + " Successfully Bound");
|
||||||
Iris.warn("Note: NMS support is disabled. Iris is running in limited Bukkit fallback mode.");
|
Iris.warn("Note: NMS support is disabled. Iris is running in limited Bukkit fallback mode.");
|
||||||
return new NMSBinding1X();
|
return new NMSBinding1X();
|
||||||
}
|
}
|
||||||
|
|
||||||
String serverVersion = Bukkit.getServer().getBukkitVersion().split("-")[0];
|
MinecraftVersion detectedVersion = getMinecraftVersion();
|
||||||
|
String serverVersion = detectedVersion == null ? Bukkit.getServer().getVersion() : detectedVersion.value();
|
||||||
throw new IllegalStateException("Iris requires Minecraft 1.21.11 or newer. Detected server version: " + serverVersion);
|
throw new IllegalStateException("Iris requires Minecraft 1.21.11 or newer. Detected server version: " + serverVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getTag(List<Version> versions, String def) {
|
private static String getTag(List<Version> versions, String def) {
|
||||||
String[] version = Bukkit.getServer().getBukkitVersion().split("-")[0].split("\\.", 3);
|
MinecraftVersion detectedVersion = getMinecraftVersion();
|
||||||
int major = 0;
|
if (detectedVersion == null) {
|
||||||
int minor = 0;
|
return def;
|
||||||
|
|
||||||
if (version.length > 2) {
|
|
||||||
major = Integer.parseInt(version[1]);
|
|
||||||
minor = Integer.parseInt(version[2]);
|
|
||||||
} else if (version.length == 2) {
|
|
||||||
major = Integer.parseInt(version[1]);
|
|
||||||
}
|
}
|
||||||
if (CURRENT.major < major || CURRENT.minor < minor) {
|
|
||||||
|
if (detectedVersion.isNewerThan(CURRENT.major, CURRENT.minor)) {
|
||||||
return versions.getFirst().tag;
|
return versions.getFirst().tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Version p : versions) {
|
for (Version p : versions) {
|
||||||
if (p.major > major || p.minor > minor) {
|
if (!detectedVersion.isAtLeast(p.major, p.minor)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return p.tag;
|
return p.tag;
|
||||||
@@ -121,5 +114,50 @@ public class INMS {
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MinecraftVersion getMinecraftVersion() {
|
||||||
|
try {
|
||||||
|
return MinecraftVersion.detect(Bukkit.getServer());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
Iris.error("Failed to determine server minecraft version!");
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static INMSBinding tryBind(String code, boolean announce) {
|
||||||
|
if (announce) {
|
||||||
|
Iris.info("Locating NMS Binding for " + code);
|
||||||
|
} else {
|
||||||
|
Iris.info("Probing NMS Binding for " + code);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> clazz = Class.forName("art.arcane.iris.core.nms." + code + ".NMSBinding");
|
||||||
|
Object candidate = clazz.getConstructor().newInstance();
|
||||||
|
if (candidate instanceof INMSBinding binding) {
|
||||||
|
Iris.info("Craftbukkit " + code + " <-> " + candidate.getClass().getSimpleName() + " Successfully Bound");
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
} catch (ClassNotFoundException | NoClassDefFoundError classNotFoundException) {
|
||||||
|
Iris.warn("Failed to load NMS binding class for " + code + ": " + classNotFoundException.getMessage());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> getFallbackBindingCodes() {
|
||||||
|
Set<String> codes = new LinkedHashSet<>();
|
||||||
|
for (Version version : REVISION) {
|
||||||
|
if (version.tag != null && !version.tag.isBlank()) {
|
||||||
|
codes.add(version.tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codes;
|
||||||
|
}
|
||||||
|
|
||||||
private record Version(int major, int minor, String tag) {}
|
private record Version(int major, int minor, String tag) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@
|
|||||||
|
|
||||||
package art.arcane.iris.core.nms;
|
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.link.Identifier;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleRequest;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||||
import art.arcane.iris.core.nms.container.StructurePlacement;
|
import art.arcane.iris.core.nms.container.StructurePlacement;
|
||||||
@@ -40,6 +42,7 @@ import org.bukkit.block.Biome;
|
|||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||||
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
@@ -96,34 +99,33 @@ public interface INMSBinding {
|
|||||||
MCABiomeContainer newBiomeContainer(int min, int max);
|
MCABiomeContainer newBiomeContainer(int min, int max);
|
||||||
|
|
||||||
default World createWorld(WorldCreator c) {
|
default World createWorld(WorldCreator c) {
|
||||||
if (c.generator() instanceof PlatformChunkGenerator gen
|
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(c, false, false, WorldLifecycleCaller.CREATE);
|
||||||
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey()))
|
return createWorld(c, request);
|
||||||
throw new IllegalStateException("Missing dimension types to create world");
|
|
||||||
return c.createWorld();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default CompletableFuture<World> createWorldAsync(WorldCreator c) {
|
default CompletableFuture<World> createWorldAsync(WorldCreator c) {
|
||||||
try {
|
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(c, false, false, WorldLifecycleCaller.CREATE);
|
||||||
if (c.generator() instanceof PlatformChunkGenerator gen
|
return createWorldAsync(c, request);
|
||||||
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) {
|
}
|
||||||
return CompletableFuture.failedFuture(new IllegalStateException("Missing dimension types to create world"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (J.isFolia()) {
|
default World createWorld(WorldCreator c, WorldLifecycleRequest request) {
|
||||||
FoliaWorldsLink link = FoliaWorldsLink.get();
|
validateDimensionTypes(c);
|
||||||
if (link.isActive()) {
|
return WorldLifecycleService.get().createBlocking(request);
|
||||||
CompletableFuture<World> future = link.createWorld(c);
|
}
|
||||||
if (future != null) {
|
|
||||||
return future;
|
default CompletableFuture<World> createWorldAsync(WorldCreator c, WorldLifecycleRequest request) {
|
||||||
}
|
try {
|
||||||
}
|
validateDimensionTypes(c);
|
||||||
}
|
return WorldLifecycleService.get().create(request);
|
||||||
return CompletableFuture.completedFuture(createWorld(c));
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
return CompletableFuture.failedFuture(e);
|
return CompletableFuture.failedFuture(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) {
|
||||||
|
throw new UnsupportedOperationException("Active NMS binding does not support runtime LevelStem creation.");
|
||||||
|
}
|
||||||
|
|
||||||
int countCustomBiomes();
|
int countCustomBiomes();
|
||||||
|
|
||||||
default boolean supportsDataPacks() {
|
default boolean supportsDataPacks() {
|
||||||
@@ -169,4 +171,11 @@ public interface INMSBinding {
|
|||||||
void placeStructures(Chunk chunk);
|
void placeStructures(Chunk chunk);
|
||||||
|
|
||||||
KMap<Identifier, StructurePlacement> collectStructures();
|
KMap<Identifier, StructurePlacement> collectStructures();
|
||||||
|
|
||||||
|
private void validateDimensionTypes(WorldCreator c) {
|
||||||
|
if (c.generator() instanceof PlatformChunkGenerator gen
|
||||||
|
&& missingDimensionTypes(gen.getTarget().getDimension().getDimensionTypeKey())) {
|
||||||
|
throw new IllegalStateException("Missing dimension types to create world");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package art.arcane.iris.core.nms;
|
||||||
|
|
||||||
|
import org.bukkit.Server;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
final class MinecraftVersion {
|
||||||
|
private static final Pattern DECORATED_VERSION_PATTERN = Pattern.compile("\\(MC: ([0-9]+(?:\\.[0-9]+){0,2})\\)");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
private final int major;
|
||||||
|
private final int minor;
|
||||||
|
|
||||||
|
private MinecraftVersion(String value, int major, int minor) {
|
||||||
|
this.value = value;
|
||||||
|
this.major = major;
|
||||||
|
this.minor = minor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MinecraftVersion detect(Server server) {
|
||||||
|
if (server == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftVersion runtimeVersion = fromRuntimeMinecraftVersion(server);
|
||||||
|
if (runtimeVersion != null) {
|
||||||
|
return runtimeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftVersion decoratedVersion = fromDecoratedVersion(server.getVersion());
|
||||||
|
if (decoratedVersion != null) {
|
||||||
|
return decoratedVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromBukkitVersion(server.getBukkitVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
static MinecraftVersion fromRuntimeMinecraftVersion(Server server) {
|
||||||
|
try {
|
||||||
|
Method method = server.getClass().getMethod("getMinecraftVersion");
|
||||||
|
Object value = method.invoke(server);
|
||||||
|
if (value instanceof String version) {
|
||||||
|
return fromVersionToken(version);
|
||||||
|
}
|
||||||
|
} catch (ReflectiveOperationException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MinecraftVersion fromDecoratedVersion(String input) {
|
||||||
|
if (input == null || input.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher matcher = DECORATED_VERSION_PATTERN.matcher(input);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromVersionToken(matcher.group(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static MinecraftVersion fromBukkitVersion(String input) {
|
||||||
|
if (input == null || input.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String versionToken = input.split("-", 2)[0].trim();
|
||||||
|
return fromVersionToken(versionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MinecraftVersion fromVersionToken(String input) {
|
||||||
|
if (input == null || input.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = input.split("\\.");
|
||||||
|
if (parts.length < 2 || !"1".equals(parts[0])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int major = Integer.parseInt(parts[1]);
|
||||||
|
int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
|
||||||
|
return new MinecraftVersion(input, major, minor);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int major() {
|
||||||
|
return major;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int minor() {
|
||||||
|
return minor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAtLeast(int major, int minor) {
|
||||||
|
return this.major > major || (this.major == major && this.minor >= minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNewerThan(int major, int minor) {
|
||||||
|
return this.major > major || (this.major == major && this.minor > minor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package art.arcane.iris.core.nms;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
final class NmsBindingProbeSupport {
|
||||||
|
private NmsBindingProbeSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> getBindingProbeCodes(String code, boolean disableNms, Collection<String> fallbackCodes) {
|
||||||
|
List<String> probeCodes = new ArrayList<>();
|
||||||
|
if (code == null || code.isBlank()) {
|
||||||
|
return probeCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"BUKKIT".equals(code)) {
|
||||||
|
probeCodes.add(code);
|
||||||
|
return probeCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableNms || fallbackCodes == null) {
|
||||||
|
return probeCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
probeCodes.addAll(fallbackCodes);
|
||||||
|
return probeCodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ public class BlockProperty {
|
|||||||
private final Function<Object, String> nameFunction;
|
private final Function<Object, String> nameFunction;
|
||||||
private final Function<Object, Object> jsonFunction;
|
private final Function<Object, Object> jsonFunction;
|
||||||
|
|
||||||
public <T extends Comparable<T>> BlockProperty(
|
public <T extends Comparable<T>> BlockProperty(
|
||||||
String name,
|
String name,
|
||||||
Class<T> type,
|
Class<T> type,
|
||||||
T defaultValue,
|
T defaultValue,
|
||||||
@@ -42,7 +42,7 @@ public class BlockProperty {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BlockProperty ofFloat(String name, float defaultValue, float min, float max, boolean exclusiveMin, boolean exclusiveMax) {
|
public static BlockProperty ofDouble(String name, float defaultValue, float min, float max, boolean exclusiveMin, boolean exclusiveMax) {
|
||||||
return new BoundedDouble(
|
return new BoundedDouble(
|
||||||
name,
|
name,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
@@ -54,6 +54,18 @@ public class BlockProperty {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BlockProperty ofLong(String name, long defaultValue, long min, long max, boolean exclusiveMin, boolean exclusiveMax) {
|
||||||
|
return new BoundedLong(
|
||||||
|
name,
|
||||||
|
defaultValue,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
exclusiveMin,
|
||||||
|
exclusiveMax,
|
||||||
|
value -> Long.toString(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static BlockProperty ofBoolean(String name, boolean defaultValue) {
|
public static BlockProperty ofBoolean(String name, boolean defaultValue) {
|
||||||
return new BlockProperty(
|
return new BlockProperty(
|
||||||
name,
|
name,
|
||||||
@@ -122,6 +134,38 @@ public class BlockProperty {
|
|||||||
return Objects.hash(name, values, type);
|
return Objects.hash(name, values, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class BoundedLong extends BlockProperty {
|
||||||
|
private final long min;
|
||||||
|
private final long max;
|
||||||
|
private final boolean exclusiveMin;
|
||||||
|
private final boolean exclusiveMax;
|
||||||
|
|
||||||
|
public BoundedLong(
|
||||||
|
String name,
|
||||||
|
long defaultValue,
|
||||||
|
long min,
|
||||||
|
long max,
|
||||||
|
boolean exclusiveMin,
|
||||||
|
boolean exclusiveMax,
|
||||||
|
Function<Long, String> nameFunction
|
||||||
|
) {
|
||||||
|
super(name, Long.class, defaultValue, List.of(), nameFunction);
|
||||||
|
this.min = min;
|
||||||
|
this.max = max;
|
||||||
|
this.exclusiveMin = exclusiveMin;
|
||||||
|
this.exclusiveMax = exclusiveMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject buildJson() {
|
||||||
|
return super.buildJson()
|
||||||
|
.put("minimum", min)
|
||||||
|
.put("maximum", max)
|
||||||
|
.put("exclusiveMinimum", exclusiveMin)
|
||||||
|
.put("exclusiveMaximum", exclusiveMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class BoundedDouble extends BlockProperty {
|
private static class BoundedDouble extends BlockProperty {
|
||||||
private final double min, max;
|
private final double min, max;
|
||||||
private final boolean exclusiveMin, exclusiveMax;
|
private final boolean exclusiveMin, exclusiveMax;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class DataFixerV1217 extends DataFixerV1213 {
|
|||||||
Dimension.OVERWORLD, """
|
Dimension.OVERWORLD, """
|
||||||
{
|
{
|
||||||
"ambient_light": 0.0,
|
"ambient_light": 0.0,
|
||||||
|
"has_ender_dragon_fight": false,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"minecraft:audio/ambient_sounds": {
|
"minecraft:audio/ambient_sounds": {
|
||||||
"mood": {
|
"mood": {
|
||||||
@@ -42,6 +43,7 @@ public class DataFixerV1217 extends DataFixerV1213 {
|
|||||||
Dimension.NETHER, """
|
Dimension.NETHER, """
|
||||||
{
|
{
|
||||||
"ambient_light": 0.1,
|
"ambient_light": 0.1,
|
||||||
|
"has_ender_dragon_fight": false,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"minecraft:gameplay/sky_light_level": 4.0,
|
"minecraft:gameplay/sky_light_level": 4.0,
|
||||||
"minecraft:gameplay/snow_golem_melts": true,
|
"minecraft:gameplay/snow_golem_melts": true,
|
||||||
@@ -57,6 +59,7 @@ public class DataFixerV1217 extends DataFixerV1213 {
|
|||||||
Dimension.END, """
|
Dimension.END, """
|
||||||
{
|
{
|
||||||
"ambient_light": 0.25,
|
"ambient_light": 0.25,
|
||||||
|
"has_ender_dragon_fight": true,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"minecraft:audio/ambient_sounds": {
|
"minecraft:audio/ambient_sounds": {
|
||||||
"mood": {
|
"mood": {
|
||||||
@@ -96,9 +99,9 @@ public class DataFixerV1217 extends DataFixerV1213 {
|
|||||||
|
|
||||||
JSONObject particle = (JSONObject) effects.remove("particle");
|
JSONObject particle = (JSONObject) effects.remove("particle");
|
||||||
if (particle != null) {
|
if (particle != null) {
|
||||||
|
particle.put("particle", particle.remove("options"));
|
||||||
attributes.put("minecraft:visual/ambient_particles", new JSONArray()
|
attributes.put("minecraft:visual/ambient_particles", new JSONArray()
|
||||||
.put(particle.getJSONObject("options")
|
.put(particle));
|
||||||
.put("probability", particle.get("probability"))));
|
|
||||||
}
|
}
|
||||||
json.put("attributes", attributes);
|
json.put("attributes", attributes);
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ package art.arcane.iris.core.project;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.link.FoliaWorldsLink;
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||||
import art.arcane.iris.core.loader.ResourceLoader;
|
import art.arcane.iris.core.loader.ResourceLoader;
|
||||||
|
import art.arcane.iris.core.runtime.StudioOpenCoordinator;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.object.*;
|
import art.arcane.iris.engine.object.*;
|
||||||
import art.arcane.iris.engine.object.annotations.Snippet;
|
import art.arcane.iris.engine.object.annotations.Snippet;
|
||||||
@@ -63,6 +64,8 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -228,209 +231,117 @@ public class IrisProject {
|
|||||||
return foundWork;
|
return foundWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void open(VolmitSender sender, long seed, Consumer<World> onDone) throws IrisException {
|
public CompletableFuture<StudioOpenCoordinator.StudioOpenResult> open(VolmitSender sender, long seed, Consumer<World> onDone) throws IrisException {
|
||||||
if (isOpen()) {
|
if (isOpen()) {
|
||||||
close();
|
return close().thenCompose(ignored -> openInternal(sender, seed, onDone));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return openInternal(sender, seed, onDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<StudioOpenCoordinator.StudioOpenResult> openInternal(VolmitSender sender, long seed, Consumer<World> onDone) {
|
||||||
AtomicReference<String> stage = new AtomicReference<>("Queued");
|
AtomicReference<String> stage = new AtomicReference<>("Queued");
|
||||||
AtomicReference<Double> progress = new AtomicReference<>(0.01D);
|
AtomicReference<Double> progress = new AtomicReference<>(0.01D);
|
||||||
AtomicBoolean complete = new AtomicBoolean(false);
|
AtomicBoolean complete = new AtomicBoolean(false);
|
||||||
AtomicBoolean failed = new AtomicBoolean(false);
|
AtomicBoolean failed = new AtomicBoolean(false);
|
||||||
|
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> future = StudioOpenCoordinator.get().open(
|
||||||
|
StudioOpenCoordinator.StudioOpenRequest.studioProject(
|
||||||
|
this,
|
||||||
|
sender,
|
||||||
|
seed,
|
||||||
|
update -> {
|
||||||
|
if (update.stage() != null && !update.stage().isBlank()) {
|
||||||
|
stage.set(update.stage());
|
||||||
|
}
|
||||||
|
progress.set(Math.max(0D, Math.min(0.99D, update.progress())));
|
||||||
|
},
|
||||||
|
onDone
|
||||||
|
)
|
||||||
|
);
|
||||||
startStudioOpenReporter(sender, stage, progress, complete, failed);
|
startStudioOpenReporter(sender, stage, progress, complete, failed);
|
||||||
|
future.whenComplete((result, throwable) -> {
|
||||||
J.a(() -> {
|
|
||||||
World maintenanceWorld = null;
|
World maintenanceWorld = null;
|
||||||
boolean maintenanceActive = false;
|
boolean maintenanceActive = false;
|
||||||
try {
|
try {
|
||||||
stage.set("Loading dimension");
|
if (throwable != null) {
|
||||||
progress.set(0.05D);
|
|
||||||
IrisDimension d = IrisData.loadAnyDimension(getName(), null);
|
|
||||||
if (d == null) {
|
|
||||||
failed.set(true);
|
failed.set(true);
|
||||||
sender.sendMessage(C.RED + "Can't find dimension: " + getName());
|
Throwable error = throwable instanceof java.util.concurrent.CompletionException completionException && completionException.getCause() != null
|
||||||
|
? completionException.getCause()
|
||||||
|
: throwable;
|
||||||
|
Iris.reportError("Studio open failed for project \"" + getName() + "\".", error);
|
||||||
|
sender.sendMessage(C.RED + "Studio open failed: " + error.getMessage());
|
||||||
return;
|
return;
|
||||||
} else if (sender.isPlayer()) {
|
}
|
||||||
|
|
||||||
|
if (sender.isPlayer() && sender.player() != null) {
|
||||||
J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.SPECTATOR));
|
J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.SPECTATOR));
|
||||||
}
|
}
|
||||||
|
activeProvider = IrisToolbelt.access(result.world());
|
||||||
stage.set("Creating world");
|
maintenanceWorld = result.world();
|
||||||
progress.set(0.12D);
|
if (maintenanceWorld != null) {
|
||||||
activeProvider = (PlatformChunkGenerator) IrisToolbelt.createWorld()
|
IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open");
|
||||||
.seed(seed)
|
maintenanceActive = true;
|
||||||
.sender(sender)
|
|
||||||
.studio(true)
|
|
||||||
.name("iris-" + UUID.randomUUID())
|
|
||||||
.dimension(d.getLoadKey())
|
|
||||||
.studioProgressConsumer((value, currentStage) -> {
|
|
||||||
if (currentStage != null && !currentStage.isBlank()) {
|
|
||||||
stage.set(currentStage);
|
|
||||||
}
|
|
||||||
progress.set(Math.max(0D, Math.min(0.99D, value)));
|
|
||||||
})
|
|
||||||
.create().getGenerator();
|
|
||||||
|
|
||||||
if (activeProvider != null) {
|
|
||||||
maintenanceWorld = activeProvider.getTarget().getWorld().realWorld();
|
|
||||||
if (maintenanceWorld != null) {
|
|
||||||
IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open");
|
|
||||||
maintenanceActive = true;
|
|
||||||
}
|
|
||||||
onDone.accept(maintenanceWorld);
|
|
||||||
}
|
}
|
||||||
} catch (IrisException e) {
|
|
||||||
failed.set(true);
|
|
||||||
Iris.reportError(e);
|
|
||||||
sender.sendMessage(C.RED + "Failed to open studio world: " + e.getMessage());
|
|
||||||
} catch (Throwable e) {
|
|
||||||
failed.set(true);
|
|
||||||
Iris.reportError(e);
|
|
||||||
sender.sendMessage(C.RED + "Studio open failed: " + e.getMessage());
|
|
||||||
} finally {
|
} finally {
|
||||||
if (activeProvider != null) {
|
|
||||||
stage.set("Opening workspace");
|
|
||||||
progress.set(Math.max(progress.get(), 0.95D));
|
|
||||||
openVSCode(sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maintenanceActive && maintenanceWorld != null) {
|
if (maintenanceActive && maintenanceWorld != null) {
|
||||||
World worldToRelease = maintenanceWorld;
|
World worldToRelease = maintenanceWorld;
|
||||||
J.a(() -> {
|
J.a(() -> IrisToolbelt.endWorldMaintenance(worldToRelease, "studio-open"), 300);
|
||||||
J.sleep(15000);
|
|
||||||
IrisToolbelt.endWorldMaintenance(worldToRelease, "studio-open");
|
|
||||||
});
|
|
||||||
maintenanceActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maintenanceActive && maintenanceWorld != null) {
|
|
||||||
IrisToolbelt.endWorldMaintenance(maintenanceWorld, "studio-open");
|
|
||||||
}
|
}
|
||||||
complete.set(true);
|
complete.set(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startStudioOpenReporter(VolmitSender sender, AtomicReference<String> stage, AtomicReference<Double> progress, AtomicBoolean complete, AtomicBoolean failed) {
|
private void startStudioOpenReporter(VolmitSender sender, AtomicReference<String> stage, AtomicReference<Double> progress, AtomicBoolean complete, AtomicBoolean failed) {
|
||||||
J.a(() -> {
|
String[] spinner = {"|", "/", "-", "\\"};
|
||||||
String[] spinner = {"|", "/", "-", "\\"};
|
AtomicInteger spinIndex = new AtomicInteger(0);
|
||||||
int spinIndex = 0;
|
AtomicLong nextConsoleUpdate = new AtomicLong(0L);
|
||||||
long nextConsoleUpdate = 0L;
|
AtomicInteger taskId = new AtomicInteger(-1);
|
||||||
|
|
||||||
while (!complete.get()) {
|
int scheduledTaskId = J.ar(() -> {
|
||||||
double currentProgress = Math.max(0D, Math.min(0.97D, progress.get()));
|
if (complete.get()) {
|
||||||
String currentStage = stage.get();
|
J.car(taskId.get());
|
||||||
String currentSpinner = spinner[spinIndex % spinner.length];
|
if (failed.get()) {
|
||||||
|
if (sender.isPlayer()) {
|
||||||
if (sender.isPlayer()) {
|
sender.sendProgress(1D, "Studio open failed");
|
||||||
sender.sendProgress(currentProgress, "Studio " + currentSpinner + " " + currentStage);
|
} else {
|
||||||
} else {
|
sender.sendMessage(C.RED + "Studio open failed.");
|
||||||
long now = System.currentTimeMillis();
|
|
||||||
if (now >= nextConsoleUpdate) {
|
|
||||||
sender.sendMessage(C.WHITE + "Studio " + Form.pc(currentProgress, 0) + C.GRAY + " - " + currentStage);
|
|
||||||
nextConsoleUpdate = now + 1500L;
|
|
||||||
}
|
}
|
||||||
}
|
} else if (sender.isPlayer()) {
|
||||||
|
sender.sendProgress(1D, "Studio ready");
|
||||||
spinIndex++;
|
|
||||||
J.sleep(120);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed.get()) {
|
|
||||||
if (sender.isPlayer()) {
|
|
||||||
sender.sendProgress(1D, "Studio open failed");
|
|
||||||
} else {
|
} else {
|
||||||
sender.sendMessage(C.RED + "Studio open failed.");
|
sender.sendMessage(C.GREEN + "Studio ready.");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double currentProgress = Math.max(0D, Math.min(0.97D, progress.get()));
|
||||||
|
String currentStage = stage.get();
|
||||||
|
String currentSpinner = spinner[Math.floorMod(spinIndex.getAndIncrement(), spinner.length)];
|
||||||
|
|
||||||
if (sender.isPlayer()) {
|
if (sender.isPlayer()) {
|
||||||
sender.sendProgress(1D, "Studio ready");
|
sender.sendProgress(currentProgress, "Studio " + currentSpinner + " " + currentStage);
|
||||||
} else {
|
|
||||||
sender.sendMessage(C.GREEN + "Studio ready.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
if (activeProvider == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Iris.debug("Closing Active Provider");
|
|
||||||
final PlatformChunkGenerator provider = activeProvider;
|
|
||||||
final World studioWorld = provider.getTarget().getWorld().realWorld();
|
|
||||||
final File folder = provider.getTarget().getWorld().worldFolder();
|
|
||||||
final String worldName = provider.getTarget().getWorld().name();
|
|
||||||
|
|
||||||
final Runnable closeTask = () -> {
|
|
||||||
IrisToolbelt.beginWorldMaintenance(studioWorld, "studio-close", true);
|
|
||||||
try {
|
|
||||||
IrisToolbelt.evacuate(studioWorld);
|
|
||||||
provider.close();
|
|
||||||
Iris.linkMultiverseCore.removeFromConfig(worldName);
|
|
||||||
boolean unloaded = FoliaWorldsLink.get().unloadWorld(studioWorld, false);
|
|
||||||
if (!unloaded) {
|
|
||||||
Iris.warn("Failed to unload studio world \"" + worldName + "\".");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
IrisToolbelt.endWorldMaintenance(studioWorld, "studio-close");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (J.isPrimaryThread()) {
|
|
||||||
closeTask.run();
|
|
||||||
} else {
|
|
||||||
final CompletableFuture<Void> closeFuture = J.sfut(closeTask);
|
|
||||||
if (closeFuture != null) {
|
|
||||||
try {
|
|
||||||
closeFuture.join();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Iris.reportError(e);
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
closeTask.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
J.attemptAsync(() -> deleteStudioFolderWithRetry(folder, worldName));
|
|
||||||
Iris.debug("Closed Active Provider " + worldName);
|
|
||||||
activeProvider = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void deleteStudioFolderWithRetry(File folder, String worldName) {
|
|
||||||
if (folder == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long unloadWaitDeadlineMs = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20);
|
|
||||||
while (Bukkit.getWorld(worldName) != null && System.currentTimeMillis() < unloadWaitDeadlineMs) {
|
|
||||||
J.sleep(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
int attempts = 0;
|
|
||||||
while (folder.exists() && attempts < 40) {
|
|
||||||
IO.delete(folder);
|
|
||||||
if (!folder.exists()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts++;
|
long now = System.currentTimeMillis();
|
||||||
J.sleep(250);
|
long nextUpdate = nextConsoleUpdate.get();
|
||||||
|
if (now >= nextUpdate) {
|
||||||
|
sender.sendMessage(C.WHITE + "Studio " + Form.pc(currentProgress, 0) + C.GRAY + " - " + currentStage);
|
||||||
|
nextConsoleUpdate.set(now + 1500L);
|
||||||
|
}
|
||||||
|
}, 3);
|
||||||
|
|
||||||
|
taskId.set(scheduledTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<StudioOpenCoordinator.StudioCloseResult> close() {
|
||||||
|
if (activeProvider == null) {
|
||||||
|
return CompletableFuture.completedFuture(new StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!folder.exists()) {
|
return StudioOpenCoordinator.get().closeProject(this);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Iris.queueWorldDeletionOnStartup(java.util.Collections.singleton(worldName));
|
|
||||||
Iris.warn("Queued deferred deletion for studio world folder \"" + worldName + "\".");
|
|
||||||
} catch (IOException e) {
|
|
||||||
Iris.warn("Failed to queue deferred deletion for studio world folder \"" + worldName + "\".");
|
|
||||||
Iris.reportError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getCodeWorkspaceFile() {
|
public File getCodeWorkspaceFile() {
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
|
||||||
|
import io.papermc.lib.PaperLib;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
final class BukkitPublicRuntimeControlBackend implements WorldRuntimeControlBackend {
|
||||||
|
private final CapabilitySnapshot capabilities;
|
||||||
|
|
||||||
|
BukkitPublicRuntimeControlBackend(CapabilitySnapshot capabilities) {
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String backendName() {
|
||||||
|
return "bukkit_public_runtime";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String describeCapabilities() {
|
||||||
|
String chunkAsync = capabilities.chunkAtAsyncMethod() != null ? "world#getChunkAtAsync" : "paperlib";
|
||||||
|
return "time=bukkit_world#setTime, chunkAsync=" + chunkAsync + ", teleport=entity_scheduler";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OptionalLong readDayTime(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return OptionalLong.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return OptionalLong.of(world.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean writeDayTime(World world, long dayTime) {
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
world.setTime(dayTime);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncTime(World world) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
|
||||||
|
if (world == null) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("World is null."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capabilities.chunkAtAsyncMethod() != null) {
|
||||||
|
try {
|
||||||
|
Object result = capabilities.chunkAtAsyncMethod().invoke(world, chunkX, chunkZ, generate);
|
||||||
|
if (result instanceof CompletableFuture<?>) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
CompletableFuture<Chunk> future = (CompletableFuture<Chunk>) result;
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Chunk> future = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate);
|
||||||
|
if (future == null) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("PaperLib did not return a chunk future."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public final class DatapackReadinessResult {
|
||||||
|
private final String requestedPackKey;
|
||||||
|
private final List<String> resolvedDatapackFolders;
|
||||||
|
private final String externalDatapackInstallResult;
|
||||||
|
private final boolean verificationPassed;
|
||||||
|
private final List<String> verifiedPaths;
|
||||||
|
private final List<String> missingPaths;
|
||||||
|
private final boolean restartRequired;
|
||||||
|
|
||||||
|
public String toJson() {
|
||||||
|
return new GsonBuilder().setPrettyPrinting().create().toJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DatapackReadinessResult installForStudioWorld(
|
||||||
|
String requestedPackKey,
|
||||||
|
String dimensionTypeKey,
|
||||||
|
File worldFolder,
|
||||||
|
boolean verifyDataPacks,
|
||||||
|
boolean includeExternalDataPacks,
|
||||||
|
KMap<String, KList<File>> extraWorldDatapackFoldersByPack
|
||||||
|
) {
|
||||||
|
ArrayList<String> resolvedFolders = new ArrayList<>();
|
||||||
|
File datapacksFolder = ServerConfigurator.resolveDatapacksFolder(worldFolder);
|
||||||
|
resolvedFolders.add(datapacksFolder.getAbsolutePath());
|
||||||
|
if (extraWorldDatapackFoldersByPack != null) {
|
||||||
|
KList<File> extraFolders = extraWorldDatapackFoldersByPack.get(requestedPackKey);
|
||||||
|
if (extraFolders != null) {
|
||||||
|
for (File extraFolder : extraFolders) {
|
||||||
|
if (extraFolder == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String path = extraFolder.getAbsolutePath();
|
||||||
|
if (!resolvedFolders.contains(path)) {
|
||||||
|
resolvedFolders.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String externalResult = "ok";
|
||||||
|
boolean restartRequired = false;
|
||||||
|
try {
|
||||||
|
restartRequired = ServerConfigurator.installDataPacks(verifyDataPacks, includeExternalDataPacks, extraWorldDatapackFoldersByPack);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
externalResult = e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> verifiedPaths = new ArrayList<>();
|
||||||
|
ArrayList<String> missingPaths = new ArrayList<>();
|
||||||
|
String verificationDimensionTypeKey = (dimensionTypeKey == null || dimensionTypeKey.isBlank())
|
||||||
|
? requestedPackKey
|
||||||
|
: dimensionTypeKey;
|
||||||
|
for (String folderPath : resolvedFolders) {
|
||||||
|
File folder = new File(folderPath);
|
||||||
|
collectVerificationPaths(folder, verificationDimensionTypeKey, verifiedPaths, missingPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean verificationPassed = missingPaths.isEmpty() && "ok".equals(externalResult);
|
||||||
|
return new DatapackReadinessResult(
|
||||||
|
requestedPackKey,
|
||||||
|
List.copyOf(resolvedFolders),
|
||||||
|
externalResult,
|
||||||
|
verificationPassed,
|
||||||
|
List.copyOf(verifiedPaths),
|
||||||
|
List.copyOf(missingPaths),
|
||||||
|
restartRequired
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void collectVerificationPaths(File folder, String dimensionTypeKey, List<String> verifiedPaths, List<String> missingPaths) {
|
||||||
|
File packMeta = new File(folder, "iris/pack.mcmeta");
|
||||||
|
File dimensionType = new File(folder, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json");
|
||||||
|
if (packMeta.exists()) {
|
||||||
|
verifiedPaths.add(packMeta.getAbsolutePath());
|
||||||
|
} else {
|
||||||
|
missingPaths.add(packMeta.getAbsolutePath());
|
||||||
|
}
|
||||||
|
if (dimensionType.exists()) {
|
||||||
|
verifiedPaths.add(dimensionType.getAbsolutePath());
|
||||||
|
} else {
|
||||||
|
missingPaths.add(dimensionType.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
|
||||||
|
import io.papermc.lib.PaperLib;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
final class PaperLikeRuntimeControlBackend implements WorldRuntimeControlBackend {
|
||||||
|
private final CapabilitySnapshot capabilities;
|
||||||
|
private final AtomicReference<TimeAccessStrategy> timeAccessStrategy;
|
||||||
|
|
||||||
|
PaperLikeRuntimeControlBackend(CapabilitySnapshot capabilities) {
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
this.timeAccessStrategy = new AtomicReference<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String backendName() {
|
||||||
|
return "paper_like_runtime";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String describeCapabilities() {
|
||||||
|
TimeAccessStrategy strategy = timeAccessStrategy.get();
|
||||||
|
String timeAccess = strategy == null ? "deferred" : strategy.description();
|
||||||
|
String chunkAsync = capabilities.chunkAtAsyncMethod() != null ? "world#getChunkAtAsync" : "paperlib";
|
||||||
|
return "time=" + timeAccess + ", chunkAsync=" + chunkAsync + ", teleport=entity_scheduler";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OptionalLong readDayTime(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return OptionalLong.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeAccessStrategy strategy = resolveTimeAccessStrategy(world);
|
||||||
|
if (strategy == null) {
|
||||||
|
return OptionalLong.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object handle = strategy.handleMethod().invoke(world);
|
||||||
|
if (handle == null) {
|
||||||
|
return OptionalLong.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object value = strategy.readMethod().invoke(strategy.readOwner(handle), strategy.readArguments(handle));
|
||||||
|
if (value instanceof Long longValue) {
|
||||||
|
return OptionalLong.of(longValue.longValue());
|
||||||
|
}
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return OptionalLong.of(number.longValue());
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return OptionalLong.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean writeDayTime(World world, long dayTime) throws ReflectiveOperationException {
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeAccessStrategy strategy = resolveTimeAccessStrategy(world);
|
||||||
|
if (strategy == null || !strategy.writable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object handle = strategy.handleMethod().invoke(world);
|
||||||
|
if (handle == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object writeOwner = strategy.writeOwner(handle);
|
||||||
|
if (writeOwner == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy.writeMethod().invoke(writeOwner, dayTime);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncTime(World world) {
|
||||||
|
TimeAccessStrategy strategy = timeAccessStrategy.get();
|
||||||
|
if (strategy == null || strategy.syncMethod() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object craftServer = Bukkit.getServer();
|
||||||
|
if (craftServer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object serverHandle = strategy.serverHandleMethod() == null ? null : strategy.serverHandleMethod().invoke(craftServer);
|
||||||
|
if (serverHandle == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy.syncMethod().invoke(serverHandle);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
|
||||||
|
if (world == null) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("World is null."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capabilities.chunkAtAsyncMethod() != null) {
|
||||||
|
try {
|
||||||
|
Object result = capabilities.chunkAtAsyncMethod().invoke(world, chunkX, chunkZ, generate);
|
||||||
|
if (result instanceof CompletableFuture<?>) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
CompletableFuture<Chunk> future = (CompletableFuture<Chunk>) result;
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return CompletableFuture.failedFuture(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Chunk> future = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate);
|
||||||
|
if (future == null) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("PaperLib did not return a chunk future."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeAccessStrategy resolveTimeAccessStrategy(World world) {
|
||||||
|
TimeAccessStrategy current = timeAccessStrategy.get();
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (timeAccessStrategy) {
|
||||||
|
current = timeAccessStrategy.get();
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeAccessStrategy resolved = probeTimeAccessStrategy(world);
|
||||||
|
timeAccessStrategy.set(resolved);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeAccessStrategy probeTimeAccessStrategy(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return TimeAccessStrategy.unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Method handleMethod = resolveZeroArgMethod(world.getClass(), "getHandle");
|
||||||
|
if (handleMethod == null) {
|
||||||
|
return TimeAccessStrategy.unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
Object handle = handleMethod.invoke(world);
|
||||||
|
if (handle == null) {
|
||||||
|
return TimeAccessStrategy.unsupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
Method readMethod = resolveZeroArgMethod(handle.getClass(), "getDayTime");
|
||||||
|
Method writeMethod = resolveLongArgMethod(handle.getClass(), "setDayTime");
|
||||||
|
if (readMethod != null && writeMethod != null) {
|
||||||
|
return TimeAccessStrategy.forHandle(handleMethod, readMethod, writeMethod, "runtime_handle#setDayTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
Method levelDataMethod = resolveZeroArgMethod(handle.getClass(), "serverLevelData");
|
||||||
|
if (levelDataMethod == null) {
|
||||||
|
levelDataMethod = resolveZeroArgMethod(handle.getClass(), "getLevelData");
|
||||||
|
}
|
||||||
|
if (levelDataMethod != null) {
|
||||||
|
Object levelData = levelDataMethod.invoke(handle);
|
||||||
|
if (levelData != null) {
|
||||||
|
Method levelDataReadMethod = resolveZeroArgMethod(levelData.getClass(), "getDayTime");
|
||||||
|
Method levelDataWriteMethod = resolveLongArgMethod(levelData.getClass(), "setDayTime");
|
||||||
|
if (levelDataReadMethod != null && levelDataWriteMethod != null) {
|
||||||
|
return TimeAccessStrategy.forLevelData(handleMethod, levelDataMethod, levelDataReadMethod, levelDataWriteMethod, "world_data#setDayTime");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeAccessStrategy.unsupported(handleMethod);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return TimeAccessStrategy.unsupported();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method resolveZeroArgMethod(Class<?> type, String name) {
|
||||||
|
Class<?> current = type;
|
||||||
|
while (current != null) {
|
||||||
|
try {
|
||||||
|
Method method = current.getDeclaredMethod(name);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method resolveLongArgMethod(Class<?> type, String name) {
|
||||||
|
Class<?> current = type;
|
||||||
|
while (current != null) {
|
||||||
|
try {
|
||||||
|
Method method = current.getDeclaredMethod(name, long.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record TimeAccessStrategy(
|
||||||
|
Method handleMethod,
|
||||||
|
Method levelDataMethod,
|
||||||
|
Method readMethod,
|
||||||
|
Method writeMethod,
|
||||||
|
Method serverHandleMethod,
|
||||||
|
Method syncMethod,
|
||||||
|
String description
|
||||||
|
) {
|
||||||
|
static TimeAccessStrategy forHandle(Method handleMethod, Method readMethod, Method writeMethod, String description) {
|
||||||
|
Method serverHandleMethod = resolveCraftServerMethod("getHandle");
|
||||||
|
Method syncMethod = resolveServerMethod(serverHandleMethod, "forceTimeSynchronization");
|
||||||
|
return new TimeAccessStrategy(handleMethod, null, readMethod, writeMethod, serverHandleMethod, syncMethod, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TimeAccessStrategy forLevelData(Method handleMethod, Method levelDataMethod, Method readMethod, Method writeMethod, String description) {
|
||||||
|
Method serverHandleMethod = resolveCraftServerMethod("getHandle");
|
||||||
|
Method syncMethod = resolveServerMethod(serverHandleMethod, "forceTimeSynchronization");
|
||||||
|
return new TimeAccessStrategy(handleMethod, levelDataMethod, readMethod, writeMethod, serverHandleMethod, syncMethod, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TimeAccessStrategy unsupported() {
|
||||||
|
return new TimeAccessStrategy(null, null, null, null, null, null, "unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
static TimeAccessStrategy unsupported(Method handleMethod) {
|
||||||
|
return new TimeAccessStrategy(handleMethod, null, null, null, null, null, "unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean writable() {
|
||||||
|
return handleMethod != null && readMethod != null && writeMethod != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object readOwner(Object handle) throws ReflectiveOperationException {
|
||||||
|
if (levelDataMethod == null) {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return levelDataMethod.invoke(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object[] readArguments(Object handle) {
|
||||||
|
return new Object[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object writeOwner(Object handle) throws ReflectiveOperationException {
|
||||||
|
if (levelDataMethod == null) {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return levelDataMethod.invoke(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method resolveCraftServerMethod(String name) {
|
||||||
|
try {
|
||||||
|
Method method = Bukkit.getServer().getClass().getMethod(name);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method resolveServerMethod(Method serverHandleMethod, String name) {
|
||||||
|
if (serverHandleMethod == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Object craftServer = Bukkit.getServer();
|
||||||
|
if (craftServer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object serverHandle = serverHandleMethod.invoke(craftServer);
|
||||||
|
if (serverHandle == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method method = serverHandle.getClass().getMethod(name);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.volmlib.util.io.IO;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
public final class SmokeDiagnosticsService {
|
||||||
|
private static volatile SmokeDiagnosticsService instance;
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, SmokeRunReport> reports;
|
||||||
|
private final AtomicReference<String> latestRunId;
|
||||||
|
private final AtomicLong runCounter;
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
private SmokeDiagnosticsService() {
|
||||||
|
this.reports = new ConcurrentHashMap<>();
|
||||||
|
this.latestRunId = new AtomicReference<>();
|
||||||
|
this.runCounter = new AtomicLong(1L);
|
||||||
|
this.gson = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SmokeDiagnosticsService get() {
|
||||||
|
SmokeDiagnosticsService current = instance;
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (SmokeDiagnosticsService.class) {
|
||||||
|
if (instance != null) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = new SmokeDiagnosticsService();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmokeRunHandle beginRun(SmokeRunMode mode, String worldName, boolean studio, boolean headless, String playerName, boolean retainOnFailure) {
|
||||||
|
long ordinal = runCounter.getAndIncrement();
|
||||||
|
String runId = String.format("%s-%05d", mode.id(), ordinal);
|
||||||
|
SmokeRunReport report = new SmokeRunReport();
|
||||||
|
report.setRunId(runId);
|
||||||
|
report.setMode(mode.id());
|
||||||
|
report.setWorldName(worldName);
|
||||||
|
report.setStudio(studio);
|
||||||
|
report.setHeadless(headless);
|
||||||
|
report.setPlayerName(playerName);
|
||||||
|
report.setRetainOnFailure(retainOnFailure);
|
||||||
|
report.setStartedAt(System.currentTimeMillis());
|
||||||
|
report.setOutcome("running");
|
||||||
|
report.setStage("queued");
|
||||||
|
report.setLifecycleBackend(art.arcane.iris.core.lifecycle.WorldLifecycleService.get().capabilities().serverFamily().id());
|
||||||
|
report.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
|
||||||
|
reports.put(runId, report);
|
||||||
|
latestRunId.set(runId);
|
||||||
|
persist(report);
|
||||||
|
return new SmokeRunHandle(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmokeRunReport latest() {
|
||||||
|
String runId = latestRunId.get();
|
||||||
|
if (runId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmokeRunReport get(String runId) {
|
||||||
|
if (runId == null || runId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SmokeRunReport report = reports.get(runId);
|
||||||
|
if (report != null) {
|
||||||
|
return snapshot(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
return load(runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmokeRunReport latestPersisted() {
|
||||||
|
File latestFile = latestFile();
|
||||||
|
if (!latestFile.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return gson.fromJson(IO.readAll(latestFile), SmokeRunReport.class);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SmokeRunReport load(String runId) {
|
||||||
|
File file = reportFile(runId);
|
||||||
|
if (!file.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return gson.fromJson(IO.readAll(file), SmokeRunReport.class);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persist(SmokeRunReport report) {
|
||||||
|
if (report == null || !SmokeRunMode.shouldPersist(report.getMode())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String json = gson.toJson(report);
|
||||||
|
File file = reportFile(report.getRunId());
|
||||||
|
IO.writeAll(file, json);
|
||||||
|
IO.writeAll(latestFile(), json);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to persist smoke report \"" + report.getRunId() + "\".", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SmokeRunReport snapshot(SmokeRunReport report) {
|
||||||
|
String json = gson.toJson(report);
|
||||||
|
return gson.fromJson(json, SmokeRunReport.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File reportFile(String runId) {
|
||||||
|
if (Iris.instance == null) {
|
||||||
|
File root = new File("plugins/Iris/diagnostics/smoke");
|
||||||
|
root.mkdirs();
|
||||||
|
return new File(root, runId + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Iris.instance.getDataFile("diagnostics", "smoke", runId + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private File latestFile() {
|
||||||
|
if (Iris.instance == null) {
|
||||||
|
File root = new File("plugins/Iris/diagnostics/smoke");
|
||||||
|
root.mkdirs();
|
||||||
|
return new File(root, "latest.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Iris.instance.getDataFile("diagnostics", "smoke", "latest.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SmokeRunMode {
|
||||||
|
FULL("full", true),
|
||||||
|
STUDIO("studio", true),
|
||||||
|
CREATE("create", true),
|
||||||
|
BENCHMARK("benchmark", true),
|
||||||
|
STUDIO_OPEN("studio_open", false),
|
||||||
|
STUDIO_CLOSE("studio_close", false);
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final boolean persisted;
|
||||||
|
|
||||||
|
SmokeRunMode(String id, boolean persisted) {
|
||||||
|
this.id = id;
|
||||||
|
this.persisted = persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean shouldPersist(String id) {
|
||||||
|
for (SmokeRunMode mode : values()) {
|
||||||
|
if (mode.id.equals(id)) {
|
||||||
|
return mode.persisted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class SmokeRunHandle {
|
||||||
|
private final SmokeRunReport report;
|
||||||
|
|
||||||
|
private SmokeRunHandle(SmokeRunReport report) {
|
||||||
|
this.report = report;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String runId() {
|
||||||
|
return report.getRunId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmokeRunReport snapshot() {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
|
||||||
|
return SmokeDiagnosticsService.this.snapshot(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWorldName(String worldName) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setWorldName(worldName);
|
||||||
|
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLifecycleBackend(String backend) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setLifecycleBackend(backend);
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRuntimeBackend(String backend) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setRuntimeBackend(backend);
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEntryChunk(int chunkX, int chunkZ) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setEntryChunkX(chunkX);
|
||||||
|
report.setEntryChunkZ(chunkZ);
|
||||||
|
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGenerationSession(long sessionId, int activeLeases) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setGenerationSessionId(sessionId);
|
||||||
|
report.setGenerationActiveLeases(activeLeases);
|
||||||
|
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDatapackReadiness(DatapackReadinessResult readiness) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setDatapackReadiness(readiness);
|
||||||
|
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCloseState(boolean unloadCompletedLive, boolean folderDeletionCompletedLive, boolean startupCleanupQueued) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setCloseUnloadCompletedLive(unloadCompletedLive);
|
||||||
|
report.setCloseFolderDeletionCompletedLive(folderDeletionCompletedLive);
|
||||||
|
report.setCloseStartupCleanupQueued(startupCleanupQueued);
|
||||||
|
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void note(String text) {
|
||||||
|
synchronized (report) {
|
||||||
|
ArrayList<String> notes = new ArrayList<>(report.getNotes());
|
||||||
|
notes.add(text);
|
||||||
|
report.setNotes(List.copyOf(notes));
|
||||||
|
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stage(String stage) {
|
||||||
|
stage(stage, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stage(String stage, String detail) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setStage(stage);
|
||||||
|
report.setStageDetail(detail);
|
||||||
|
report.setElapsedMs(System.currentTimeMillis() - report.getStartedAt());
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void completeSuccess(String finalStage, boolean cleanupApplied) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setStage(finalStage);
|
||||||
|
report.setOutcome("success");
|
||||||
|
report.setCleanupApplied(cleanupApplied);
|
||||||
|
report.setCompletedAt(System.currentTimeMillis());
|
||||||
|
report.setElapsedMs(report.getCompletedAt() - report.getStartedAt());
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void completeFailure(String finalStage, Throwable throwable, boolean cleanupApplied) {
|
||||||
|
synchronized (report) {
|
||||||
|
report.setStage(finalStage);
|
||||||
|
report.setOutcome("failed");
|
||||||
|
report.setCleanupApplied(cleanupApplied);
|
||||||
|
report.setCompletedAt(System.currentTimeMillis());
|
||||||
|
report.setElapsedMs(report.getCompletedAt() - report.getStartedAt());
|
||||||
|
if (throwable != null) {
|
||||||
|
report.setFailureType(throwable.getClass().getName());
|
||||||
|
report.setFailureMessage(String.valueOf(throwable.getMessage()));
|
||||||
|
report.setFailureChain(failureChain(throwable));
|
||||||
|
report.setFailureStacktrace(stacktrace(throwable));
|
||||||
|
}
|
||||||
|
persist(report);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> failureChain(Throwable throwable) {
|
||||||
|
ArrayList<String> chain = new ArrayList<>();
|
||||||
|
Throwable cursor = throwable;
|
||||||
|
while (cursor != null) {
|
||||||
|
chain.add(cursor.getClass().getName() + ": " + String.valueOf(cursor.getMessage()));
|
||||||
|
cursor = cursor.getCause();
|
||||||
|
}
|
||||||
|
return List.copyOf(chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stacktrace(Throwable throwable) {
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
PrintWriter printWriter = new PrintWriter(writer);
|
||||||
|
throwable.printStackTrace(printWriter);
|
||||||
|
printWriter.flush();
|
||||||
|
return writer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static final class SmokeRunReport {
|
||||||
|
private String runId;
|
||||||
|
private String mode;
|
||||||
|
private String worldName;
|
||||||
|
private String stage;
|
||||||
|
private String stageDetail;
|
||||||
|
private long startedAt;
|
||||||
|
private long completedAt;
|
||||||
|
private long elapsedMs;
|
||||||
|
private String outcome;
|
||||||
|
private String lifecycleBackend;
|
||||||
|
private String runtimeBackend;
|
||||||
|
private long generationSessionId;
|
||||||
|
private int generationActiveLeases;
|
||||||
|
private Integer entryChunkX;
|
||||||
|
private Integer entryChunkZ;
|
||||||
|
private boolean studio;
|
||||||
|
private boolean headless;
|
||||||
|
private String playerName;
|
||||||
|
private boolean retainOnFailure;
|
||||||
|
private boolean cleanupApplied;
|
||||||
|
private boolean closeUnloadCompletedLive;
|
||||||
|
private boolean closeFolderDeletionCompletedLive;
|
||||||
|
private boolean closeStartupCleanupQueued;
|
||||||
|
private DatapackReadinessResult datapackReadiness;
|
||||||
|
private String failureType;
|
||||||
|
private String failureMessage;
|
||||||
|
private List<String> failureChain = List.of();
|
||||||
|
private String failureStacktrace;
|
||||||
|
private List<String> notes = List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
|
import art.arcane.iris.core.tools.IrisCreator;
|
||||||
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||||
|
import art.arcane.volmlib.util.io.IO;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public final class SmokeTestService {
|
||||||
|
private static volatile SmokeTestService instance;
|
||||||
|
|
||||||
|
private final SmokeDiagnosticsService diagnostics;
|
||||||
|
|
||||||
|
private SmokeTestService() {
|
||||||
|
this.diagnostics = SmokeDiagnosticsService.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SmokeTestService get() {
|
||||||
|
SmokeTestService current = instance;
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (SmokeTestService.class) {
|
||||||
|
if (instance != null) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = new SmokeTestService();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String startCreateSmoke(VolmitSender sender, String dimensionKey, long seed, boolean retainOnFailure) {
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode.CREATE,
|
||||||
|
nextWorldName("create"),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
retainOnFailure
|
||||||
|
);
|
||||||
|
J.a(() -> executeCreateSmoke(handle, sender, dimensionKey, seed, false, true));
|
||||||
|
return handle.runId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String startBenchmarkSmoke(VolmitSender sender, String dimensionKey, long seed, boolean retainOnFailure) {
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode.BENCHMARK,
|
||||||
|
nextWorldName("benchmark"),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
retainOnFailure
|
||||||
|
);
|
||||||
|
J.a(() -> executeCreateSmoke(handle, sender, dimensionKey, seed, true, true));
|
||||||
|
return handle.runId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String startStudioSmoke(VolmitSender sender, String dimensionKey, long seed, String playerName, boolean retainOnFailure) {
|
||||||
|
String normalizedPlayer = normalizePlayerName(playerName);
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode.STUDIO,
|
||||||
|
nextWorldName("studio"),
|
||||||
|
true,
|
||||||
|
normalizedPlayer == null,
|
||||||
|
normalizedPlayer,
|
||||||
|
retainOnFailure
|
||||||
|
);
|
||||||
|
J.a(() -> executeStudioSmoke(handle, sender, dimensionKey, seed, normalizedPlayer, retainOnFailure, true));
|
||||||
|
return handle.runId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String startFullSmoke(VolmitSender sender, String dimensionKey, long seed, String playerName, boolean retainOnFailure) {
|
||||||
|
String normalizedPlayer = normalizePlayerName(playerName);
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode.FULL,
|
||||||
|
nextWorldName("full"),
|
||||||
|
false,
|
||||||
|
normalizedPlayer == null,
|
||||||
|
normalizedPlayer,
|
||||||
|
retainOnFailure
|
||||||
|
);
|
||||||
|
J.a(() -> executeFullSmoke(handle, sender, dimensionKey, seed, normalizedPlayer, retainOnFailure));
|
||||||
|
return handle.runId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmokeDiagnosticsService.SmokeRunReport latest() {
|
||||||
|
SmokeDiagnosticsService.SmokeRunReport latest = diagnostics.latest();
|
||||||
|
if (latest != null) {
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics.latestPersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SmokeDiagnosticsService.SmokeRunReport get(String runId) {
|
||||||
|
return diagnostics.get(runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldInspection inspectWorld(String worldName) {
|
||||||
|
World world = Bukkit.getWorld(worldName);
|
||||||
|
if (world == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlatformChunkGenerator provider = IrisToolbelt.access(world);
|
||||||
|
boolean studio = provider != null && provider.isStudio();
|
||||||
|
boolean engineClosed = false;
|
||||||
|
boolean engineFailing = false;
|
||||||
|
long generationSessionId = 0L;
|
||||||
|
int activeLeases = 0;
|
||||||
|
if (provider != null && provider.getEngine() instanceof IrisEngine irisEngine) {
|
||||||
|
engineClosed = irisEngine.isClosed();
|
||||||
|
engineFailing = irisEngine.isFailing();
|
||||||
|
generationSessionId = irisEngine.getGenerationSessionId();
|
||||||
|
activeLeases = irisEngine.getGenerationSessions().activeLeases();
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<String> datapackFolders = new ArrayList<>();
|
||||||
|
File datapacksFolder = ServerConfigurator.resolveDatapacksFolder(world.getWorldFolder());
|
||||||
|
datapackFolders.add(datapacksFolder.getAbsolutePath());
|
||||||
|
return new WorldInspection(
|
||||||
|
world.getName(),
|
||||||
|
WorldLifecycleService.get().backendNameForWorld(world.getName()),
|
||||||
|
WorldRuntimeControlService.get().backendName(),
|
||||||
|
studio,
|
||||||
|
engineClosed,
|
||||||
|
engineFailing,
|
||||||
|
generationSessionId,
|
||||||
|
activeLeases,
|
||||||
|
List.copyOf(datapackFolders),
|
||||||
|
IrisToolbelt.isWorldMaintenanceActive(world)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeFullSmoke(
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle,
|
||||||
|
VolmitSender sender,
|
||||||
|
String dimensionKey,
|
||||||
|
long seed,
|
||||||
|
String playerName,
|
||||||
|
boolean retainOnFailure
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
handle.stage("create");
|
||||||
|
executeCreateSmoke(handle, sender, dimensionKey, seed, false, false);
|
||||||
|
handle.note("create smoke complete");
|
||||||
|
|
||||||
|
handle.stage("benchmark");
|
||||||
|
executeCreateSmoke(handle, sender, dimensionKey, seed, true, false);
|
||||||
|
handle.note("benchmark smoke complete");
|
||||||
|
|
||||||
|
handle.stage("studio");
|
||||||
|
executeStudioSmoke(handle, sender, dimensionKey, seed, playerName, retainOnFailure, false);
|
||||||
|
handle.note("studio smoke complete");
|
||||||
|
|
||||||
|
handle.completeSuccess("cleanup", true);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
handle.completeFailure("cleanup", e, !retainOnFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeCreateSmoke(
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle,
|
||||||
|
VolmitSender sender,
|
||||||
|
String dimensionKey,
|
||||||
|
long seed,
|
||||||
|
boolean benchmark,
|
||||||
|
boolean completeHandle
|
||||||
|
) {
|
||||||
|
String worldName = nextWorldName(benchmark ? "benchmark" : "create");
|
||||||
|
handle.setWorldName(worldName);
|
||||||
|
cleanupTransientPrefix("iris-smoke-");
|
||||||
|
World world = null;
|
||||||
|
PlatformChunkGenerator provider = null;
|
||||||
|
boolean cleanupApplied = false;
|
||||||
|
try {
|
||||||
|
IrisCreator creator = IrisToolbelt.createWorld()
|
||||||
|
.dimension(dimensionKey)
|
||||||
|
.name(worldName)
|
||||||
|
.seed(seed)
|
||||||
|
.sender(sender)
|
||||||
|
.studio(false)
|
||||||
|
.benchmark(benchmark)
|
||||||
|
.studioProgressConsumer((progress, stage) -> handle.stage(mapCreateStage(stage)));
|
||||||
|
world = creator.create();
|
||||||
|
provider = IrisToolbelt.access(world);
|
||||||
|
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
|
||||||
|
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
|
||||||
|
handle.setDatapackReadiness(creator.getLastDatapackReadinessResult());
|
||||||
|
captureGenerationSession(provider, handle);
|
||||||
|
|
||||||
|
if (benchmark) {
|
||||||
|
handle.stage("apply_world_rules");
|
||||||
|
WorldRuntimeControlService.get().applyStudioWorldRules(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.stage("cleanup");
|
||||||
|
cleanupWorld(world, worldName);
|
||||||
|
cleanupApplied = true;
|
||||||
|
if (completeHandle) {
|
||||||
|
handle.completeSuccess("cleanup", true);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Smoke create failed for world \"" + worldName + "\".", e);
|
||||||
|
if (!handle.snapshot().isRetainOnFailure()) {
|
||||||
|
try {
|
||||||
|
cleanupWorld(world, worldName);
|
||||||
|
cleanupApplied = true;
|
||||||
|
} catch (Throwable cleanupError) {
|
||||||
|
Iris.reportError("Smoke cleanup failed for world \"" + worldName + "\".", cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (completeHandle) {
|
||||||
|
handle.completeFailure("cleanup", e, cleanupApplied);
|
||||||
|
} else {
|
||||||
|
if (e instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeStudioSmoke(
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle,
|
||||||
|
VolmitSender sender,
|
||||||
|
String dimensionKey,
|
||||||
|
long seed,
|
||||||
|
String playerName,
|
||||||
|
boolean retainOnFailure,
|
||||||
|
boolean completeHandle
|
||||||
|
) {
|
||||||
|
String worldName = nextWorldName("studio");
|
||||||
|
handle.setWorldName(worldName);
|
||||||
|
cleanupTransientPrefix("iris-smoke-");
|
||||||
|
World world = null;
|
||||||
|
boolean cleanupApplied = false;
|
||||||
|
CompletableFuture<StudioOpenCoordinator.StudioOpenResult> future = StudioOpenCoordinator.get().open(
|
||||||
|
new StudioOpenCoordinator.StudioOpenRequest(
|
||||||
|
dimensionKey,
|
||||||
|
null,
|
||||||
|
sender,
|
||||||
|
seed,
|
||||||
|
worldName,
|
||||||
|
playerName,
|
||||||
|
false,
|
||||||
|
retainOnFailure,
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode.STUDIO,
|
||||||
|
handle,
|
||||||
|
completeHandle,
|
||||||
|
update -> handle.stage(update.stage()),
|
||||||
|
openedWorld -> {
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
StudioOpenCoordinator.StudioOpenResult result = future.join();
|
||||||
|
world = result == null ? null : result.world();
|
||||||
|
handle.stage("cleanup");
|
||||||
|
cleanupWorld(world, worldName);
|
||||||
|
cleanupApplied = true;
|
||||||
|
if (completeHandle) {
|
||||||
|
handle.completeSuccess("cleanup", true);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (world != null && !cleanupApplied) {
|
||||||
|
try {
|
||||||
|
cleanupWorld(world, worldName);
|
||||||
|
cleanupApplied = true;
|
||||||
|
} catch (Throwable cleanupError) {
|
||||||
|
Iris.reportError("Smoke cleanup failed for world \"" + worldName + "\".", cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (completeHandle && !"failed".equalsIgnoreCase(handle.snapshot().getOutcome())) {
|
||||||
|
handle.completeFailure("cleanup", e, cleanupApplied);
|
||||||
|
}
|
||||||
|
if (!completeHandle) {
|
||||||
|
if (e instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void captureGenerationSession(PlatformChunkGenerator provider, SmokeDiagnosticsService.SmokeRunHandle handle) {
|
||||||
|
if (provider == null || provider.getEngine() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.getEngine() instanceof IrisEngine irisEngine) {
|
||||||
|
handle.setGenerationSession(irisEngine.getGenerationSessionId(), irisEngine.getGenerationSessions().activeLeases());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupWorld(World world, String worldName) {
|
||||||
|
if (world != null) {
|
||||||
|
PlatformChunkGenerator provider = IrisToolbelt.access(world);
|
||||||
|
if (provider != null) {
|
||||||
|
provider.close();
|
||||||
|
}
|
||||||
|
WorldLifecycleService.get().unload(world, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
File container = Bukkit.getWorldContainer();
|
||||||
|
deleteFolder(new File(container, worldName), worldName);
|
||||||
|
deleteFolder(new File(container, worldName + "_nether"), null);
|
||||||
|
deleteFolder(new File(container, worldName + "_the_end"), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteFolder(File folder, String worldName) {
|
||||||
|
if (folder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IO.delete(folder);
|
||||||
|
if (!folder.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (worldName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Iris.queueWorldDeletionOnStartup(Collections.singleton(worldName));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Iris.reportError("Failed to queue smoke world deletion for \"" + worldName + "\".", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupTransientPrefix(String prefix) {
|
||||||
|
File container = Bukkit.getWorldContainer();
|
||||||
|
File[] children = container.listFiles();
|
||||||
|
if (children == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (File child : children) {
|
||||||
|
if (!child.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!child.getName().startsWith(prefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Bukkit.getWorld(child.getName()) != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IO.delete(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nextWorldName(String mode) {
|
||||||
|
return "iris-smoke-" + mode + "-" + UUID.randomUUID().toString().substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizePlayerName(String playerName) {
|
||||||
|
if (playerName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmed = playerName.trim();
|
||||||
|
if (trimmed.isEmpty() || trimmed.equalsIgnoreCase("none")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String mapCreateStage(String stage) {
|
||||||
|
if (stage == null || stage.isBlank()) {
|
||||||
|
return "create_world";
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = stage.trim().toLowerCase();
|
||||||
|
return switch (normalized) {
|
||||||
|
case "resolve_dimension", "resolving dimension" -> "resolve_dimension";
|
||||||
|
case "prepare_world_pack", "preparing world pack" -> "prepare_world_pack";
|
||||||
|
case "install_datapacks", "installing datapacks" -> "install_datapacks";
|
||||||
|
case "create_world", "creating world", "world created" -> "create_world";
|
||||||
|
default -> normalized.replace(' ', '_');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public record WorldInspection(
|
||||||
|
String worldName,
|
||||||
|
String lifecycleBackend,
|
||||||
|
String runtimeBackend,
|
||||||
|
boolean studio,
|
||||||
|
boolean engineClosed,
|
||||||
|
boolean engineFailing,
|
||||||
|
long generationSessionId,
|
||||||
|
int activeLeaseCount,
|
||||||
|
List<String> datapackFolders,
|
||||||
|
boolean maintenanceActive
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,660 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
|
import art.arcane.iris.core.project.IrisProject;
|
||||||
|
import art.arcane.iris.core.tools.IrisCreator;
|
||||||
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import art.arcane.volmlib.util.exceptions.IrisException;
|
||||||
|
import art.arcane.volmlib.util.io.IO;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public final class StudioOpenCoordinator {
|
||||||
|
private static volatile StudioOpenCoordinator instance;
|
||||||
|
|
||||||
|
private final SmokeDiagnosticsService diagnostics;
|
||||||
|
|
||||||
|
private StudioOpenCoordinator() {
|
||||||
|
this.diagnostics = SmokeDiagnosticsService.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StudioOpenCoordinator get() {
|
||||||
|
StudioOpenCoordinator current = instance;
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (StudioOpenCoordinator.class) {
|
||||||
|
if (instance != null) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = new StudioOpenCoordinator();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<StudioOpenResult> open(StudioOpenRequest request) {
|
||||||
|
CompletableFuture<StudioOpenResult> future = new CompletableFuture<>();
|
||||||
|
J.aBukkit(() -> executeOpen(request, future));
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<StudioCloseResult> closeProject(IrisProject project) {
|
||||||
|
CompletableFuture<StudioCloseResult> future = new CompletableFuture<>();
|
||||||
|
J.aBukkit(() -> future.complete(executeClose(project)));
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StudioCloseResult executeClose(IrisProject project) {
|
||||||
|
if (project == null) {
|
||||||
|
return new StudioCloseResult(null, true, true, false, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlatformChunkGenerator provider = project.getActiveProvider();
|
||||||
|
if (provider == null) {
|
||||||
|
return new StudioCloseResult(null, true, true, false, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
World world = provider.getTarget().getWorld().realWorld();
|
||||||
|
String worldName = world == null ? provider.getTarget().getWorld().name() : world.getName();
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle = diagnostics.beginRun(
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode.STUDIO_CLOSE,
|
||||||
|
worldName,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
StudioCloseResult result;
|
||||||
|
try {
|
||||||
|
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
|
||||||
|
if (world != null) {
|
||||||
|
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
|
||||||
|
captureGenerationSession(provider, handle);
|
||||||
|
}
|
||||||
|
result = closeWorld(provider, worldName, world, true, handle, project);
|
||||||
|
handle.setCloseState(result.unloadCompletedLive(), result.folderDeletionCompletedLive(), result.startupCleanupQueued());
|
||||||
|
if (result.failureCause() != null) {
|
||||||
|
handle.completeFailure("finalize_close", result.failureCause(), result.folderDeletionCompletedLive() || result.startupCleanupQueued());
|
||||||
|
} else {
|
||||||
|
handle.completeSuccess("finalize_close", result.folderDeletionCompletedLive() || result.startupCleanupQueued());
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
project.setActiveProvider(null);
|
||||||
|
handle.completeFailure("finalize_close", e, false);
|
||||||
|
result = new StudioCloseResult(worldName, false, false, false, e, handle.runId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeOpen(StudioOpenRequest request, CompletableFuture<StudioOpenResult> future) {
|
||||||
|
boolean ownsHandle = request.runHandle() == null;
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle = ownsHandle
|
||||||
|
? diagnostics.beginRun(
|
||||||
|
request.mode(),
|
||||||
|
request.worldName(),
|
||||||
|
true,
|
||||||
|
request.playerName() == null || request.playerName().isBlank(),
|
||||||
|
request.playerName(),
|
||||||
|
request.retainOnFailure()
|
||||||
|
)
|
||||||
|
: request.runHandle();
|
||||||
|
World world = null;
|
||||||
|
PlatformChunkGenerator provider = null;
|
||||||
|
boolean cleanupApplied = false;
|
||||||
|
try {
|
||||||
|
updateStage(handle, request, "resolve_dimension", 0.04D);
|
||||||
|
if (IrisToolbelt.getDimension(request.dimensionKey()) == null) {
|
||||||
|
throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStage(handle, request, "prepare_world_pack", 0.10D);
|
||||||
|
cleanupStaleTransientWorlds(request.worldName());
|
||||||
|
|
||||||
|
updateStage(handle, request, "install_datapacks", 0.18D);
|
||||||
|
IrisCreator creator = IrisToolbelt.createWorld()
|
||||||
|
.seed(request.seed())
|
||||||
|
.sender(request.sender())
|
||||||
|
.studio(true)
|
||||||
|
.name(request.worldName())
|
||||||
|
.dimension(request.dimensionKey())
|
||||||
|
.studioProgressConsumer((progress, stage) -> updateStage(handle, request, mapCreatorStage(stage), progress));
|
||||||
|
world = creator.create();
|
||||||
|
provider = IrisToolbelt.access(world);
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalStateException("Studio runtime provider is unavailable for world \"" + request.worldName() + "\".");
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
|
||||||
|
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
|
||||||
|
handle.setDatapackReadiness(creator.getLastDatapackReadinessResult());
|
||||||
|
captureGenerationSession(provider, handle);
|
||||||
|
|
||||||
|
updateStage(handle, request, "apply_world_rules", 0.72D);
|
||||||
|
WorldRuntimeControlService.get().applyStudioWorldRules(world);
|
||||||
|
|
||||||
|
updateStage(handle, request, "prepare_generator", 0.78D);
|
||||||
|
WorldRuntimeControlService.get().prepareGenerator(world);
|
||||||
|
|
||||||
|
Location entryAnchor = WorldRuntimeControlService.get().resolveEntryAnchor(world);
|
||||||
|
if (entryAnchor == null) {
|
||||||
|
throw new IllegalStateException("Studio entry anchor could not be resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(120L);
|
||||||
|
updateStage(handle, request, "request_entry_chunk", 0.84D);
|
||||||
|
requestEntryChunk(world, entryAnchor, deadline, handle);
|
||||||
|
|
||||||
|
updateStage(handle, request, "resolve_safe_entry", 0.90D);
|
||||||
|
Location safeEntry = resolveSafeEntry(world, entryAnchor, deadline);
|
||||||
|
if (safeEntry == null) {
|
||||||
|
throw new IllegalStateException("Studio safe entry resolution timed out.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.playerName() != null && !request.playerName().isBlank()) {
|
||||||
|
updateStage(handle, request, "teleport_player", 0.96D);
|
||||||
|
Player player = resolvePlayer(request.playerName());
|
||||||
|
if (player == null) {
|
||||||
|
throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online.");
|
||||||
|
}
|
||||||
|
|
||||||
|
long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
|
||||||
|
Boolean teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(remaining, TimeUnit.MILLISECONDS);
|
||||||
|
if (!Boolean.TRUE.equals(teleported)) {
|
||||||
|
throw new IllegalStateException("Studio teleport did not complete successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStage(handle, request, "finalize_open", 1.00D);
|
||||||
|
if (request.project() != null) {
|
||||||
|
request.project().setActiveProvider(provider);
|
||||||
|
}
|
||||||
|
if (request.openWorkspace() && request.project() != null) {
|
||||||
|
request.project().openVSCode(request.sender());
|
||||||
|
}
|
||||||
|
if (request.onDone() != null) {
|
||||||
|
request.onDone().accept(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.completeHandle()) {
|
||||||
|
handle.completeSuccess("finalize_open", false);
|
||||||
|
} else {
|
||||||
|
handle.stage("finalize_open");
|
||||||
|
}
|
||||||
|
future.complete(new StudioOpenResult(world, handle.runId(), safeEntry, creator.getLastDatapackReadinessResult()));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e);
|
||||||
|
if (!request.retainOnFailure()) {
|
||||||
|
try {
|
||||||
|
updateStage(handle, request, "cleanup", 1.00D);
|
||||||
|
StudioCloseResult cleanupResult = closeWorld(provider, request.worldName(), world, true, handle, request.project());
|
||||||
|
cleanupApplied = cleanupResult.folderDeletionCompletedLive() || cleanupResult.startupCleanupQueued();
|
||||||
|
} catch (Throwable cleanupError) {
|
||||||
|
Iris.reportError("Studio cleanup failed for world \"" + request.worldName() + "\".", cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (request.completeHandle()) {
|
||||||
|
handle.completeFailure("cleanup", e, cleanupApplied);
|
||||||
|
} else {
|
||||||
|
handle.stage("cleanup", String.valueOf(e.getMessage()));
|
||||||
|
}
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestEntryChunk(World world, Location entryAnchor, long deadline, SmokeDiagnosticsService.SmokeRunHandle handle) throws Exception {
|
||||||
|
int chunkX = entryAnchor.getBlockX() >> 4;
|
||||||
|
int chunkZ = entryAnchor.getBlockZ() >> 4;
|
||||||
|
handle.setEntryChunk(chunkX, chunkZ);
|
||||||
|
long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
|
||||||
|
waitForEntryChunk(world, chunkX, chunkZ, deadline, null).get(remaining, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location resolveSafeEntry(World world, Location entryAnchor, long deadline) throws Exception {
|
||||||
|
long remaining = Math.max(1000L, deadline - System.currentTimeMillis());
|
||||||
|
return waitForSafeEntry(world, entryAnchor, deadline, null).get(remaining, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StudioCloseResult closeWorld(
|
||||||
|
PlatformChunkGenerator provider,
|
||||||
|
String worldName,
|
||||||
|
World world,
|
||||||
|
boolean deleteFolder,
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle,
|
||||||
|
IrisProject project
|
||||||
|
) {
|
||||||
|
Throwable failure = null;
|
||||||
|
boolean unloadCompletedLive = world == null || !isWorldFamilyLoaded(worldName);
|
||||||
|
boolean folderDeletionCompletedLive = !deleteFolder;
|
||||||
|
boolean startupCleanupQueued = false;
|
||||||
|
CompletableFuture<Void> closeFuture = provider == null ? CompletableFuture.completedFuture(null) : CompletableFuture.completedFuture(null);
|
||||||
|
|
||||||
|
updateCloseStage(handle, "prepare_close");
|
||||||
|
if (world != null) {
|
||||||
|
handle.setWorldName(world.getName());
|
||||||
|
handle.setLifecycleBackend(WorldLifecycleService.get().backendNameForWorld(world.getName()));
|
||||||
|
handle.setRuntimeBackend(WorldRuntimeControlService.get().backendName());
|
||||||
|
captureGenerationSession(provider, handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (world != null) {
|
||||||
|
updateCloseStage(handle, "evacuate_players");
|
||||||
|
try {
|
||||||
|
evacuatePlayers(world);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
failure = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (world != null) {
|
||||||
|
IrisToolbelt.beginWorldMaintenance(world, "studio-close", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateCloseStage(handle, "seal_runtime");
|
||||||
|
if (project != null) {
|
||||||
|
project.setActiveProvider(null);
|
||||||
|
}
|
||||||
|
if (provider != null) {
|
||||||
|
captureGenerationSession(provider, handle);
|
||||||
|
closeFuture = provider.closeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCloseStage(handle, "request_unload");
|
||||||
|
if (worldName != null && !worldName.isBlank()) {
|
||||||
|
requestWorldFamilyUnload(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCloseStage(handle, "await_unload");
|
||||||
|
if (worldName != null && !worldName.isBlank()) {
|
||||||
|
long unloadDeadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20L);
|
||||||
|
CompletableFuture<Void> unloadFuture = waitForWorldFamilyUnload(worldName, unloadDeadline);
|
||||||
|
try {
|
||||||
|
unloadFuture.get(Math.max(1000L, unloadDeadline - System.currentTimeMillis()), TimeUnit.MILLISECONDS);
|
||||||
|
unloadCompletedLive = true;
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
unloadCompletedLive = !isWorldFamilyLoaded(worldName);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
failure = failure == null ? unwrapFailure(e) : failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
closeFuture.get(20L, TimeUnit.SECONDS);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Throwable cause = unwrapFailure(e);
|
||||||
|
if (failure == null) {
|
||||||
|
failure = cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteFolder && worldName != null && !worldName.isBlank()) {
|
||||||
|
updateCloseStage(handle, "delete_world_family");
|
||||||
|
WorldFamilyDeleteResult deleteResult = deleteWorldFamily(worldName, unloadCompletedLive);
|
||||||
|
folderDeletionCompletedLive = deleteResult.liveDeleted();
|
||||||
|
startupCleanupQueued = deleteResult.startupCleanupQueued();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCloseStage(handle, "finalize_close");
|
||||||
|
} finally {
|
||||||
|
if (world != null) {
|
||||||
|
IrisToolbelt.endWorldMaintenance(world, "studio-close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.setCloseState(unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued);
|
||||||
|
return new StudioCloseResult(worldName, unloadCompletedLive, folderDeletionCompletedLive, startupCleanupQueued, failure, handle.runId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evacuatePlayers(World world) throws Exception {
|
||||||
|
if (world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Void> future = J.sfut(() -> {
|
||||||
|
IrisToolbelt.evacuate(world);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (future != null) {
|
||||||
|
future.get(10L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestWorldFamilyUnload(String worldName) {
|
||||||
|
if (worldName == null || worldName.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
|
||||||
|
World familyWorld = Bukkit.getWorld(familyWorldName);
|
||||||
|
if (familyWorld == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Iris.linkMultiverseCore.removeFromConfig(familyWorld);
|
||||||
|
WorldLifecycleService.get().unload(familyWorld, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldFamilyDeleteResult deleteWorldFamily(String worldName, boolean unloadCompletedLive) {
|
||||||
|
if (worldName == null || worldName.isBlank()) {
|
||||||
|
return new WorldFamilyDeleteResult(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
File container = Bukkit.getWorldContainer();
|
||||||
|
boolean liveDeleted = true;
|
||||||
|
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
|
||||||
|
File folder = new File(container, familyWorldName);
|
||||||
|
if (!folder.exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
deleteWorldFolderAsync(folder, 40).get(15L, TimeUnit.SECONDS);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
liveDeleted = false;
|
||||||
|
Iris.reportError("Studio folder deletion retries failed for \"" + folder.getAbsolutePath() + "\".", unwrapFailure(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folder.exists()) {
|
||||||
|
liveDeleted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (liveDeleted) {
|
||||||
|
return new WorldFamilyDeleteResult(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Iris.queueWorldDeletionOnStartup(Collections.singleton(worldName));
|
||||||
|
return new WorldFamilyDeleteResult(false, true);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (unloadCompletedLive) {
|
||||||
|
Iris.reportError("Failed to queue deferred deletion for world \"" + worldName + "\".", e);
|
||||||
|
}
|
||||||
|
return new WorldFamilyDeleteResult(false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupStaleTransientWorlds(String worldName) {
|
||||||
|
File container = Bukkit.getWorldContainer();
|
||||||
|
LinkedHashSet<String> staleWorldNames = TransientWorldCleanupSupport.collectTransientStudioWorldNames(container);
|
||||||
|
String requestedBaseName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName);
|
||||||
|
if (requestedBaseName != null) {
|
||||||
|
staleWorldNames.add(requestedBaseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String staleWorldName : staleWorldNames) {
|
||||||
|
if (Bukkit.getWorld(staleWorldName) != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteWorldFamily(staleWorldName, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void captureGenerationSession(PlatformChunkGenerator provider, SmokeDiagnosticsService.SmokeRunHandle handle) {
|
||||||
|
if (provider == null || provider.getEngine() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.getEngine() instanceof IrisEngine irisEngine) {
|
||||||
|
handle.setGenerationSession(irisEngine.getGenerationSessionId(), irisEngine.getGenerationSessions().activeLeases());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStage(SmokeDiagnosticsService.SmokeRunHandle handle, StudioOpenRequest request, String stage, double progress) {
|
||||||
|
handle.stage(stage);
|
||||||
|
if (request.progressConsumer() != null) {
|
||||||
|
request.progressConsumer().accept(new StudioOpenProgress(progress, stage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String mapCreatorStage(String stage) {
|
||||||
|
if (stage == null || stage.isBlank()) {
|
||||||
|
return "create_world";
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = stage.trim().toLowerCase();
|
||||||
|
return switch (normalized) {
|
||||||
|
case "resolve_dimension", "resolving dimension" -> "resolve_dimension";
|
||||||
|
case "prepare_world_pack", "preparing world pack" -> "prepare_world_pack";
|
||||||
|
case "install_datapacks", "installing datapacks", "datapacks ready" -> "install_datapacks";
|
||||||
|
case "create_world", "creating world", "world created" -> "create_world";
|
||||||
|
default -> normalized.replace(' ', '_');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> waitForEntryChunk(World world, int chunkX, int chunkZ, long deadline, Throwable lastFailure) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now >= deadline) {
|
||||||
|
return CompletableFuture.failedFuture(timeoutFailure("Studio entry chunk request timed out.", lastFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
long attemptTimeout = Math.min(Math.max(1000L, deadline - now), 3000L);
|
||||||
|
CompletableFuture<org.bukkit.Chunk> request = withAttemptTimeout(
|
||||||
|
WorldRuntimeControlService.get().requestChunkAsync(world, chunkX, chunkZ, true),
|
||||||
|
attemptTimeout,
|
||||||
|
"Studio entry chunk request attempt timed out."
|
||||||
|
);
|
||||||
|
return request.handle((chunk, throwable) -> {
|
||||||
|
if (throwable == null && world.isChunkLoaded(chunkX, chunkZ)) {
|
||||||
|
return CompletableFuture.<Void>completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Throwable nextFailure = throwable == null ? lastFailure : unwrapFailure(throwable);
|
||||||
|
if (System.currentTimeMillis() >= deadline) {
|
||||||
|
return CompletableFuture.<Void>failedFuture(timeoutFailure("Studio entry chunk request timed out.", nextFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
return delayFuture(1000L).thenCompose(ignored -> waitForEntryChunk(world, chunkX, chunkZ, deadline, nextFailure));
|
||||||
|
}).thenCompose(next -> next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Location> waitForSafeEntry(World world, Location entryAnchor, long deadline, Throwable lastFailure) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now >= deadline) {
|
||||||
|
return CompletableFuture.failedFuture(timeoutFailure("Studio safe-entry resolution timed out.", lastFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
long attemptTimeout = Math.min(Math.max(1000L, deadline - now), 3000L);
|
||||||
|
CompletableFuture<Location> resolve = withAttemptTimeout(
|
||||||
|
WorldRuntimeControlService.get().resolveSafeEntry(world, entryAnchor),
|
||||||
|
attemptTimeout,
|
||||||
|
"Studio safe-entry resolution attempt timed out."
|
||||||
|
);
|
||||||
|
return resolve.handle((location, throwable) -> {
|
||||||
|
if (throwable == null && location != null) {
|
||||||
|
return CompletableFuture.completedFuture(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
Throwable nextFailure = throwable == null ? lastFailure : unwrapFailure(throwable);
|
||||||
|
if (System.currentTimeMillis() >= deadline) {
|
||||||
|
return CompletableFuture.<Location>failedFuture(timeoutFailure("Studio safe-entry resolution timed out.", nextFailure));
|
||||||
|
}
|
||||||
|
|
||||||
|
return delayFuture(250L).thenCompose(ignored -> waitForSafeEntry(world, entryAnchor, deadline, nextFailure));
|
||||||
|
}).thenCompose(next -> next);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> waitForWorldFamilyUnload(String worldName, long deadline) {
|
||||||
|
if (worldName == null || !isWorldFamilyLoaded(worldName) || System.currentTimeMillis() >= deadline) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return delayFuture(100L).thenCompose(ignored -> waitForWorldFamilyUnload(worldName, deadline));
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> deleteWorldFolderAsync(File folder, int attemptsRemaining) {
|
||||||
|
if (folder == null || !folder.exists()) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
IO.delete(folder);
|
||||||
|
if (!folder.exists()) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attemptsRemaining <= 1) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("World folder still exists after deletion retries: " + folder.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return delayFuture(250L).thenCompose(ignored -> deleteWorldFolderAsync(folder, attemptsRemaining - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> delayFuture(long delayMillis) {
|
||||||
|
long safeDelay = Math.max(0L, delayMillis);
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
}, CompletableFuture.delayedExecutor(safeDelay, TimeUnit.MILLISECONDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> CompletableFuture<T> withAttemptTimeout(CompletableFuture<T> source, long timeoutMillis, String message) {
|
||||||
|
CompletableFuture<T> future = new CompletableFuture<>();
|
||||||
|
source.whenComplete((value, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
future.completeExceptionally(unwrapFailure(throwable));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
future.complete(value);
|
||||||
|
});
|
||||||
|
delayFuture(timeoutMillis).whenComplete((ignored, throwable) -> {
|
||||||
|
if (!future.isDone()) {
|
||||||
|
future.completeExceptionally(new TimeoutException(message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IllegalStateException timeoutFailure(String message, Throwable lastFailure) {
|
||||||
|
if (lastFailure == null) {
|
||||||
|
return new IllegalStateException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new IllegalStateException(message, lastFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Throwable unwrapFailure(Throwable throwable) {
|
||||||
|
Throwable cursor = throwable;
|
||||||
|
while (cursor instanceof CompletionException || cursor instanceof ExecutionException) {
|
||||||
|
if (cursor.getCause() == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = cursor.getCause();
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Player resolvePlayer(String playerName) {
|
||||||
|
Player exact = Bukkit.getPlayerExact(playerName);
|
||||||
|
if (exact != null) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
if (player.getName().equalsIgnoreCase(playerName)) {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCloseStage(SmokeDiagnosticsService.SmokeRunHandle handle, String stage) {
|
||||||
|
handle.stage(stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWorldFamilyLoaded(String worldName) {
|
||||||
|
if (worldName == null || worldName.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
|
||||||
|
if (Bukkit.getWorld(familyWorldName) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StudioOpenRequest(
|
||||||
|
String dimensionKey,
|
||||||
|
IrisProject project,
|
||||||
|
VolmitSender sender,
|
||||||
|
long seed,
|
||||||
|
String worldName,
|
||||||
|
String playerName,
|
||||||
|
boolean openWorkspace,
|
||||||
|
boolean retainOnFailure,
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode mode,
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle runHandle,
|
||||||
|
boolean completeHandle,
|
||||||
|
Consumer<StudioOpenProgress> progressConsumer,
|
||||||
|
Consumer<World> onDone
|
||||||
|
) {
|
||||||
|
public static StudioOpenRequest studioProject(IrisProject project, VolmitSender sender, long seed, Consumer<StudioOpenProgress> progressConsumer, Consumer<World> onDone) {
|
||||||
|
String playerName = sender != null && sender.isPlayer() && sender.player() != null ? sender.player().getName() : null;
|
||||||
|
return new StudioOpenRequest(
|
||||||
|
project.getName(),
|
||||||
|
project,
|
||||||
|
sender,
|
||||||
|
seed,
|
||||||
|
"iris-" + UUID.randomUUID(),
|
||||||
|
playerName,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode.STUDIO_OPEN,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
progressConsumer,
|
||||||
|
onDone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StudioOpenProgress(double progress, String stage) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StudioOpenResult(World world, String runId, Location entryLocation, DatapackReadinessResult datapackReadiness) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StudioCloseResult(
|
||||||
|
String worldName,
|
||||||
|
boolean unloadCompletedLive,
|
||||||
|
boolean folderDeletionCompletedLive,
|
||||||
|
boolean startupCleanupQueued,
|
||||||
|
Throwable failureCause,
|
||||||
|
String runId
|
||||||
|
) {
|
||||||
|
public boolean successful() {
|
||||||
|
return failureCause == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record WorldFamilyDeleteResult(boolean liveDeleted, boolean startupCleanupQueued) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class TransientWorldCleanupSupport {
|
||||||
|
private static final Pattern TRANSIENT_STUDIO_WORLD_PATTERN = Pattern.compile("^iris-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private TransientWorldCleanupSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isTransientStudioWorldName(String worldName) {
|
||||||
|
return transientStudioBaseWorldName(worldName) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String transientStudioBaseWorldName(String worldName) {
|
||||||
|
if (worldName == null || worldName.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String candidate = worldName.trim();
|
||||||
|
if (candidate.endsWith("_nether")) {
|
||||||
|
candidate = candidate.substring(0, candidate.length() - "_nether".length());
|
||||||
|
} else if (candidate.endsWith("_the_end")) {
|
||||||
|
candidate = candidate.substring(0, candidate.length() - "_the_end".length());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TRANSIENT_STUDIO_WORLD_PATTERN.matcher(candidate).matches()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> worldFamilyNames(String worldName) {
|
||||||
|
ArrayList<String> names = new ArrayList<>();
|
||||||
|
String normalized = normalizeWorldName(worldName);
|
||||||
|
if (normalized == null) {
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
names.add(normalized);
|
||||||
|
names.add(normalized + "_nether");
|
||||||
|
names.add(normalized + "_the_end");
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LinkedHashSet<String> collectTransientStudioWorldNames(File worldContainer) {
|
||||||
|
LinkedHashSet<String> names = new LinkedHashSet<>();
|
||||||
|
if (worldContainer == null) {
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
File[] children = worldContainer.listFiles();
|
||||||
|
if (children == null) {
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (File child : children) {
|
||||||
|
if (child == null || !child.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseName = transientStudioBaseWorldName(child.getName());
|
||||||
|
if (baseName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
names.add(baseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeWorldName(String worldName) {
|
||||||
|
if (worldName == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = worldName.trim();
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.toLowerCase(Locale.ROOT).equals(normalized) ? normalized : normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.World;
|
||||||
|
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
interface WorldRuntimeControlBackend {
|
||||||
|
String backendName();
|
||||||
|
|
||||||
|
String describeCapabilities();
|
||||||
|
|
||||||
|
OptionalLong readDayTime(World world);
|
||||||
|
|
||||||
|
boolean writeDayTime(World world, long dayTime) throws ReflectiveOperationException;
|
||||||
|
|
||||||
|
void syncTime(World world);
|
||||||
|
|
||||||
|
CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate);
|
||||||
|
}
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.IrisSettings;
|
||||||
|
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
|
||||||
|
import art.arcane.iris.core.lifecycle.ServerFamily;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
|
import art.arcane.iris.core.service.BoardSVC;
|
||||||
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import art.arcane.iris.util.common.format.C;
|
||||||
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
|
import io.papermc.lib.PaperLib;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.GameRule;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Tag;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.world.TimeSkipEvent;
|
||||||
|
import org.bukkit.plugin.PluginManager;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public final class WorldRuntimeControlService {
|
||||||
|
private static volatile WorldRuntimeControlService instance;
|
||||||
|
|
||||||
|
private final CapabilitySnapshot capabilities;
|
||||||
|
private final WorldRuntimeControlBackend backend;
|
||||||
|
private final String capabilityDescription;
|
||||||
|
|
||||||
|
private WorldRuntimeControlService(CapabilitySnapshot capabilities) {
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
this.backend = selectBackend(capabilities);
|
||||||
|
this.capabilityDescription = "family=" + capabilities.serverFamily().id()
|
||||||
|
+ ", backend=" + backend.backendName()
|
||||||
|
+ ", " + backend.describeCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorldRuntimeControlService get() {
|
||||||
|
WorldRuntimeControlService current = instance;
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (WorldRuntimeControlService.class) {
|
||||||
|
if (instance != null) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
CapabilitySnapshot capabilities = WorldLifecycleService.get().capabilities();
|
||||||
|
instance = new WorldRuntimeControlService(capabilities);
|
||||||
|
Iris.info("WorldRuntimeControl capabilities: %s", instance.capabilityDescription);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String backendName() {
|
||||||
|
return backend.backendName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String capabilityDescription() {
|
||||||
|
return capabilityDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OptionalLong readDayTime(World world) {
|
||||||
|
return backend.readDayTime(world);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean applyStudioWorldRules(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Iris.linkMultiverseCore.removeFromConfig(world);
|
||||||
|
if (!IrisSettings.get().getStudio().isDisableTimeAndWeather()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBooleanGameRule(world, false, "ADVANCE_WEATHER", "DO_WEATHER_CYCLE", "WEATHER_CYCLE", "doWeatherCycle", "weatherCycle");
|
||||||
|
setBooleanGameRule(world, false, "ADVANCE_TIME", "DO_DAYLIGHT_CYCLE", "DAYLIGHT_CYCLE", "doDaylightCycle", "daylightCycle");
|
||||||
|
applyNoonTimeLock(world);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean applyNoonTimeLock(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMutableClock(world)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionalLong currentTime = readDayTime(world);
|
||||||
|
if (currentTime.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long skipAmount = (6000L - currentTime.getAsLong()) % 24000L;
|
||||||
|
if (skipAmount < 0L) {
|
||||||
|
skipAmount += 24000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeSkipEvent event = new TimeSkipEvent(world, TimeSkipEvent.SkipReason.CUSTOM, skipAmount);
|
||||||
|
PluginManager pluginManager = Bukkit.getPluginManager();
|
||||||
|
if (pluginManager != null) {
|
||||||
|
pluginManager.callEvent(event);
|
||||||
|
}
|
||||||
|
if (event.isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean written = backend.writeDayTime(world, currentTime.getAsLong() + event.getSkipAmount());
|
||||||
|
if (!written) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
backend.syncTime(world);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Runtime time control failed for world \"" + world.getName() + "\".", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
|
||||||
|
return backend.requestChunkAsync(world, chunkX, chunkZ, generate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void prepareGenerator(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
art.arcane.iris.engine.platform.PlatformChunkGenerator provider = art.arcane.iris.core.tools.IrisToolbelt.access(world);
|
||||||
|
if (provider == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
art.arcane.iris.engine.framework.Engine engine = provider.getEngine();
|
||||||
|
if (engine == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.getMantle().getComponents();
|
||||||
|
engine.getMantle().getRealRadius();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to prepare generator state for world \"" + world.getName() + "\".", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location resolveEntryAnchor(World world) {
|
||||||
|
if (world == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlatformChunkGenerator provider = IrisToolbelt.access(world);
|
||||||
|
return resolveEntryAnchor(world, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Location resolveEntryAnchor(World world, PlatformChunkGenerator provider) {
|
||||||
|
if (world == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider != null && provider.isStudio()) {
|
||||||
|
Location initialSpawn = provider.getInitialSpawnLocation(world);
|
||||||
|
if (initialSpawn != null) {
|
||||||
|
return initialSpawn.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Location spawnLocation = world.getSpawnLocation();
|
||||||
|
if (spawnLocation != null) {
|
||||||
|
return spawnLocation.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
int minY = world.getMinHeight() + 1;
|
||||||
|
int y = Math.max(minY, 96);
|
||||||
|
return new Location(world, 0.5D, y, 0.5D);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Location> resolveSafeEntry(World world, Location source) {
|
||||||
|
if (world == null || source == null) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int chunkX = source.getBlockX() >> 4;
|
||||||
|
int chunkZ = source.getBlockZ() >> 4;
|
||||||
|
return requestChunkAsync(world, chunkX, chunkZ, true).thenCompose(chunk -> {
|
||||||
|
CompletableFuture<Location> future = new CompletableFuture<>();
|
||||||
|
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||||
|
try {
|
||||||
|
future.complete(findTopSafeLocation(world, source));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule safe-entry surface resolve for " + world.getName() + "@" + chunkX + "," + chunkZ + "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return future;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> teleport(Player player, Location location) {
|
||||||
|
if (player == null || location == null) {
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
boolean scheduled = J.runEntity(player, () -> {
|
||||||
|
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(player, location);
|
||||||
|
if (teleportFuture == null) {
|
||||||
|
future.complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
teleportFuture.whenComplete((success, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
future.completeExceptionally(throwable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(success)) {
|
||||||
|
J.runEntity(player, () -> Iris.service(BoardSVC.class).updatePlayer(player));
|
||||||
|
future.complete(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
future.complete(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!scheduled) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule teleport for " + player.getName() + "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasMutableClock(World world) {
|
||||||
|
try {
|
||||||
|
Object handle = invokeNoArg(world, "getHandle");
|
||||||
|
if (handle == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object dimensionTypeHolder = invokeNoArg(handle, "dimensionTypeRegistration");
|
||||||
|
Object dimensionType = unwrapDimensionType(dimensionTypeHolder);
|
||||||
|
if (dimensionType == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !dimensionTypeHasFixedTime(dimensionType);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldRuntimeControlBackend selectBackend(CapabilitySnapshot capabilities) {
|
||||||
|
ServerFamily family = capabilities.serverFamily();
|
||||||
|
if (family.isPaperLike()) {
|
||||||
|
return new PaperLikeRuntimeControlBackend(capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BukkitPublicRuntimeControlBackend(capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Location findTopSafeLocation(World world, Location source) {
|
||||||
|
int x = source.getBlockX();
|
||||||
|
int z = source.getBlockZ();
|
||||||
|
float yaw = source.getYaw();
|
||||||
|
float pitch = source.getPitch();
|
||||||
|
|
||||||
|
for (int y : buildSafeLocationScanOrder(world, source)) {
|
||||||
|
if (isSafeStandingLocation(world, x, y, z)) {
|
||||||
|
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int[] buildSafeLocationScanOrder(World world, Location source) {
|
||||||
|
int x = source.getBlockX();
|
||||||
|
int z = source.getBlockZ();
|
||||||
|
int minY = world.getMinHeight() + 1;
|
||||||
|
int maxY = world.getMaxHeight() - 2;
|
||||||
|
int highestY = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(x, z) + 1));
|
||||||
|
int[] scanOrder = new int[maxY - minY + 1];
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
for (int y = highestY; y >= minY; y--) {
|
||||||
|
scanOrder[index++] = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = highestY + 1; y <= maxY; y++) {
|
||||||
|
scanOrder[index++] = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSafeStandingLocation(World world, int x, int y, int z) {
|
||||||
|
if (y <= world.getMinHeight() || y >= world.getMaxHeight() - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Block below = world.getBlockAt(x, y - 1, z);
|
||||||
|
Block feet = world.getBlockAt(x, y, z);
|
||||||
|
Block head = world.getBlockAt(x, y + 1, z);
|
||||||
|
Material belowType = below.getType();
|
||||||
|
if (!belowType.isSolid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Tag.LEAVES.isTagged(belowType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (belowType == Material.LAVA
|
||||||
|
|| belowType == Material.MAGMA_BLOCK
|
||||||
|
|| belowType == Material.FIRE
|
||||||
|
|| belowType == Material.SOUL_FIRE
|
||||||
|
|| belowType == Material.CAMPFIRE
|
||||||
|
|| belowType == Material.SOUL_CAMPFIRE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (feet.getType().isSolid() || head.getType().isSolid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (feet.isLiquid() || head.isLiquid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void setBooleanGameRule(World world, boolean value, String... names) {
|
||||||
|
GameRule<Boolean> gameRule = resolveBooleanGameRule(world, names);
|
||||||
|
if (gameRule != null) {
|
||||||
|
world.setGameRule(gameRule, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static GameRule<Boolean> resolveBooleanGameRule(World world, String... names) {
|
||||||
|
if (world == null || names == null || names.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> candidates = buildRuleNameCandidates(names);
|
||||||
|
for (String name : candidates) {
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Field field = GameRule.class.getField(name);
|
||||||
|
Object value = field.get(null);
|
||||||
|
if (value instanceof GameRule<?> gameRule && Boolean.class.equals(gameRule.getType())) {
|
||||||
|
return (GameRule<Boolean>) gameRule;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
GameRule<?> byName = GameRule.getByName(name);
|
||||||
|
if (byName != null && Boolean.class.equals(byName.getType())) {
|
||||||
|
return (GameRule<Boolean>) byName;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] availableRules = world.getGameRules();
|
||||||
|
if (availableRules == null || availableRules.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> normalizedCandidates = new LinkedHashSet<>();
|
||||||
|
for (String candidate : candidates) {
|
||||||
|
if (candidate != null && !candidate.isBlank()) {
|
||||||
|
normalizedCandidates.add(normalizeRuleName(candidate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String availableRule : availableRules) {
|
||||||
|
String normalizedAvailable = normalizeRuleName(availableRule);
|
||||||
|
if (!normalizedCandidates.contains(normalizedAvailable)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
GameRule<?> byName = GameRule.getByName(availableRule);
|
||||||
|
if (byName != null && Boolean.class.equals(byName.getType())) {
|
||||||
|
return (GameRule<Boolean>) byName;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> buildRuleNameCandidates(String... names) {
|
||||||
|
Set<String> candidates = new LinkedHashSet<>();
|
||||||
|
for (String name : names) {
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.add(name);
|
||||||
|
candidates.add(name.toUpperCase());
|
||||||
|
candidates.add(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeRuleName(String name) {
|
||||||
|
if (name == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder(name.length());
|
||||||
|
for (int i = 0; i < name.length(); i++) {
|
||||||
|
char current = name.charAt(i);
|
||||||
|
if (Character.isLetterOrDigit(current)) {
|
||||||
|
builder.append(Character.toLowerCase(current));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean dimensionTypeHasFixedTime(Object dimensionType) throws ReflectiveOperationException {
|
||||||
|
Object fixedTimeFlag;
|
||||||
|
try {
|
||||||
|
fixedTimeFlag = invokeNoArg(dimensionType, "hasFixedTime");
|
||||||
|
} catch (NoSuchMethodException ignored) {
|
||||||
|
Object fixedTime = invokeNoArg(dimensionType, "fixedTime");
|
||||||
|
if (fixedTime instanceof OptionalLong optionalLong) {
|
||||||
|
return optionalLong.isPresent();
|
||||||
|
}
|
||||||
|
if (fixedTime instanceof Optional<?> optional) {
|
||||||
|
return optional.isPresent();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedTimeFlag instanceof Boolean && (Boolean) fixedTimeFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object unwrapDimensionType(Object dimensionTypeHolder) throws ReflectiveOperationException {
|
||||||
|
if (dimensionTypeHolder == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> holderClass = dimensionTypeHolder.getClass();
|
||||||
|
if (holderClass.getName().startsWith("net.minecraft.world.level.dimension.")) {
|
||||||
|
return dimensionTypeHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method valueMethod = holderClass.getMethod("value");
|
||||||
|
return valueMethod.invoke(dimensionTypeHolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object invokeNoArg(Object instance, String methodName) throws ReflectiveOperationException {
|
||||||
|
Method method = instance.getClass().getMethod(methodName);
|
||||||
|
return method.invoke(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,7 +74,7 @@ public enum Mode {
|
|||||||
|
|
||||||
String[] info = new String[]{
|
String[] info = new String[]{
|
||||||
"",
|
"",
|
||||||
padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RELEASE]",
|
padd2 + color + " Iris, " + C.AQUA + "Dimension Engine " + C.RED + "[" + releaseTrain + " RC.1]",
|
||||||
padd2 + C.GRAY + " Version: " + color + version,
|
padd2 + C.GRAY + " Version: " + color + version,
|
||||||
padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)",
|
padd2 + C.GRAY + " By: " + color + "Volmit Software (Arcane Arts)",
|
||||||
padd2 + C.GRAY + " Server: " + color + serverVersion,
|
padd2 + C.GRAY + " Server: " + color + serverVersion,
|
||||||
@@ -89,10 +89,8 @@ public enum Mode {
|
|||||||
StringBuilder builder = new StringBuilder("\n\n");
|
StringBuilder builder = new StringBuilder("\n\n");
|
||||||
for (int i = 0; i < splash.length; i++) {
|
for (int i = 0; i < splash.length; i++) {
|
||||||
builder.append(splash[i]);
|
builder.append(splash[i]);
|
||||||
if (i < info.length) {
|
builder.append(info[i]);
|
||||||
builder.append(info[i]);
|
builder.append("\n");
|
||||||
}
|
|
||||||
builder.append("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Iris.info(builder.toString());
|
Iris.info(builder.toString());
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ import com.google.gson.JsonSyntaxException;
|
|||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.ServerConfigurator;
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.pack.IrisPack;
|
import art.arcane.iris.core.pack.IrisPack;
|
||||||
import art.arcane.iris.core.project.IrisProject;
|
import art.arcane.iris.core.project.IrisProject;
|
||||||
|
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -46,7 +48,10 @@ import org.zeroturnaround.zip.commons.FileUtils;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class StudioSVC implements IrisService {
|
public class StudioSVC implements IrisService {
|
||||||
@@ -55,6 +60,7 @@ public class StudioSVC implements IrisService {
|
|||||||
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
||||||
private final KMap<String, String> cacheListing = null;
|
private final KMap<String, String> cacheListing = null;
|
||||||
private IrisProject activeProject;
|
private IrisProject activeProject;
|
||||||
|
private CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> activeClose;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -78,21 +84,41 @@ public class StudioSVC implements IrisService {
|
|||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
Iris.debug("Studio Mode Active: Closing Projects");
|
Iris.debug("Studio Mode Active: Closing Projects");
|
||||||
boolean stopping = IrisToolbelt.isServerStopping();
|
boolean stopping = IrisToolbelt.isServerStopping();
|
||||||
|
LinkedHashSet<String> worldNamesToDelete = new LinkedHashSet<>(TransientWorldCleanupSupport.collectTransientStudioWorldNames(Bukkit.getWorldContainer()));
|
||||||
|
|
||||||
for (World i : Bukkit.getWorlds()) {
|
if (activeProject != null) {
|
||||||
if (IrisToolbelt.isIrisWorld(i)) {
|
PlatformChunkGenerator activeProvider = activeProject.getActiveProvider();
|
||||||
if (IrisToolbelt.isStudio(i)) {
|
if (activeProvider != null) {
|
||||||
PlatformChunkGenerator generator = IrisToolbelt.access(i);
|
String activeWorldName = activeProvider.getTarget().getWorld().name();
|
||||||
if (!stopping) {
|
if (activeWorldName != null && !activeWorldName.isBlank()) {
|
||||||
IrisToolbelt.evacuate(i);
|
worldNamesToDelete.add(activeWorldName);
|
||||||
}
|
|
||||||
|
|
||||||
if (generator != null) {
|
|
||||||
generator.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (World i : Bukkit.getWorlds()) {
|
||||||
|
if (!IrisToolbelt.isIrisWorld(i) || !IrisToolbelt.isStudio(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
worldNamesToDelete.add(i.getName());
|
||||||
|
PlatformChunkGenerator generator = IrisToolbelt.access(i);
|
||||||
|
if (!stopping) {
|
||||||
|
destroyStudioWorld(i, generator);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generator != null) {
|
||||||
|
try {
|
||||||
|
generator.close();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to close studio generator for \"" + i.getName() + "\" during shutdown.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeProject = null;
|
||||||
|
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimension installIntoWorld(VolmitSender sender, String type, File folder) {
|
public IrisDimension installIntoWorld(VolmitSender sender, String type, File folder) {
|
||||||
@@ -348,20 +374,46 @@ public class StudioSVC implements IrisService {
|
|||||||
open(sender, seed, dimm, (w) -> {
|
open(sender, seed, dimm, (w) -> {
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Iris.reportError(e);
|
Iris.reportError("Failed to open studio world \"" + dimm + "\".", e);
|
||||||
sender.sendMessage("Failed to open studio world: " + e.getMessage());
|
sender.sendMessage("Failed to open studio world: " + e.getMessage());
|
||||||
Iris.error("Studio world creation failed: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void open(VolmitSender sender, long seed, String dimm, Consumer<World> onDone) throws IrisException {
|
public void open(VolmitSender sender, long seed, String dimm, Consumer<World> onDone) throws IrisException {
|
||||||
if (isProjectOpen()) {
|
CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> pendingClose = close();
|
||||||
close();
|
pendingClose.whenComplete((closeResult, closeThrowable) -> {
|
||||||
}
|
if (closeThrowable != null) {
|
||||||
|
Iris.reportError("Failed while closing an existing studio project before opening \"" + dimm + "\".", closeThrowable);
|
||||||
|
J.s(() -> sender.sendMessage("Failed to close the existing studio project: " + closeThrowable.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimm));
|
if (closeResult != null && closeResult.failureCause() != null) {
|
||||||
activeProject = project;
|
Throwable failure = closeResult.failureCause();
|
||||||
project.open(sender, seed, onDone);
|
Iris.reportError("Failed while closing an existing studio project before opening \"" + dimm + "\".", failure);
|
||||||
|
J.s(() -> sender.sendMessage("Failed to close the existing studio project: " + failure.getMessage()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisProject project = new IrisProject(new File(getWorkspaceFolder(), dimm));
|
||||||
|
activeProject = project;
|
||||||
|
try {
|
||||||
|
project.open(sender, seed, onDone).whenComplete((result, throwable) -> {
|
||||||
|
if (throwable == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeProject == project && !project.isOpen()) {
|
||||||
|
activeProject = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IrisException e) {
|
||||||
|
if (activeProject == project) {
|
||||||
|
activeProject = null;
|
||||||
|
}
|
||||||
|
J.s(() -> sender.sendMessage("Failed to open studio world: " + e.getMessage()));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openVSCode(VolmitSender sender, String dim) {
|
public void openVSCode(VolmitSender sender, String dim) {
|
||||||
@@ -376,11 +428,89 @@ public class StudioSVC implements IrisService {
|
|||||||
return Iris.instance.getDataFileList(WORKSPACE_NAME, sub);
|
return Iris.instance.getDataFileList(WORKSPACE_NAME, sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public CompletableFuture<art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult> close() {
|
||||||
if (isProjectOpen()) {
|
if (activeClose != null && !activeClose.isDone()) {
|
||||||
Iris.debug("Closing Active Project");
|
return activeClose;
|
||||||
activeProject.close();
|
}
|
||||||
activeProject = null;
|
|
||||||
|
if (activeProject == null) {
|
||||||
|
return CompletableFuture.completedFuture(new art.arcane.iris.core.runtime.StudioOpenCoordinator.StudioCloseResult(null, true, true, false, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
Iris.debug("Closing Active Project");
|
||||||
|
IrisProject project = activeProject;
|
||||||
|
activeProject = null;
|
||||||
|
activeClose = project.close();
|
||||||
|
activeClose.whenComplete((result, throwable) -> activeClose = null);
|
||||||
|
return activeClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void destroyStudioWorld(World world, PlatformChunkGenerator generator) {
|
||||||
|
try {
|
||||||
|
IrisToolbelt.evacuate(world);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to evacuate studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generator != null) {
|
||||||
|
try {
|
||||||
|
generator.close();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to close studio generator for \"" + world.getName() + "\" during shutdown cleanup.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WorldLifecycleService.get().unload(world, false);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError("Failed to unload studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTransientStudioFolders(world.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteTransientStudioFolders(String worldName) {
|
||||||
|
if (worldName == null || worldName.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File container = Bukkit.getWorldContainer();
|
||||||
|
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(worldName)) {
|
||||||
|
File folder = new File(container, familyWorldName);
|
||||||
|
if (!folder.exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IO.delete(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void queueStudioWorldDeletionOnStartup(LinkedHashSet<String> worldNamesToDelete) {
|
||||||
|
if (worldNamesToDelete.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedHashSet<String> normalizedNames = new LinkedHashSet<>();
|
||||||
|
for (String worldName : worldNamesToDelete) {
|
||||||
|
String baseWorldName = TransientWorldCleanupSupport.transientStudioBaseWorldName(worldName);
|
||||||
|
if (baseWorldName != null) {
|
||||||
|
normalizedNames.add(baseWorldName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (worldName != null && !worldName.isBlank()) {
|
||||||
|
normalizedNames.add(worldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedNames.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Iris.queueWorldDeletionOnStartup(List.copyOf(normalizedNames));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Iris.reportError("Failed to queue studio world deletion on startup.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,16 @@ import art.arcane.iris.core.IrisRuntimeSchedulerMode;
|
|||||||
import art.arcane.iris.core.IrisWorlds;
|
import art.arcane.iris.core.IrisWorlds;
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.ServerConfigurator;
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
import art.arcane.iris.core.link.FoliaWorldsLink;
|
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleRequest;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||||
import art.arcane.iris.core.service.BoardSVC;
|
import art.arcane.iris.core.service.BoardSVC;
|
||||||
import art.arcane.iris.core.service.StudioSVC;
|
import art.arcane.iris.core.service.StudioSVC;
|
||||||
import art.arcane.iris.engine.IrisNoisemapPrebakePipeline;
|
import art.arcane.iris.core.runtime.DatapackReadinessResult;
|
||||||
import art.arcane.iris.engine.framework.SeedManager;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -42,7 +44,6 @@ import art.arcane.volmlib.util.format.Form;
|
|||||||
import art.arcane.volmlib.util.io.IO;
|
import art.arcane.volmlib.util.io.IO;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import art.arcane.volmlib.util.scheduling.O;
|
|
||||||
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
|
||||||
import io.papermc.lib.PaperLib;
|
import io.papermc.lib.PaperLib;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -52,21 +53,22 @@ import org.bukkit.block.Block;
|
|||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.world.TimeSkipEvent;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.time.Duration;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalLong;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.IntSupplier;
|
import java.util.function.IntSupplier;
|
||||||
@@ -79,9 +81,6 @@ import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML;
|
|||||||
@Data
|
@Data
|
||||||
@Accessors(fluent = true, chain = true)
|
@Accessors(fluent = true, chain = true)
|
||||||
public class IrisCreator {
|
public class IrisCreator {
|
||||||
private static final int STUDIO_PREWARM_RADIUS_CHUNKS = 1;
|
|
||||||
private static final Duration STUDIO_PREWARM_TIMEOUT = Duration.ofSeconds(45L);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify an area to pregenerate during creation
|
* Specify an area to pregenerate during creation
|
||||||
*/
|
*/
|
||||||
@@ -114,6 +113,11 @@ public class IrisCreator {
|
|||||||
*/
|
*/
|
||||||
private boolean benchmark = false;
|
private boolean benchmark = false;
|
||||||
private BiConsumer<Double, String> studioProgressConsumer;
|
private BiConsumer<Double, String> studioProgressConsumer;
|
||||||
|
private DatapackReadinessResult lastDatapackReadinessResult;
|
||||||
|
|
||||||
|
public DatapackReadinessResult getLastDatapackReadinessResult() {
|
||||||
|
return lastDatapackReadinessResult;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean removeFromBukkitYml(String name) throws IOException {
|
public static boolean removeFromBukkitYml(String name) throws IOException {
|
||||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
||||||
@@ -144,7 +148,7 @@ public class IrisCreator {
|
|||||||
throw new IrisException("You cannot invoke create() on the main thread.");
|
throw new IrisException("You cannot invoke create() on the main thread.");
|
||||||
}
|
}
|
||||||
|
|
||||||
reportStudioProgress(0.02D, "Preparing studio open");
|
reportStudioProgress(0.02D, "resolve_dimension");
|
||||||
|
|
||||||
if (studio()) {
|
if (studio()) {
|
||||||
World existing = Bukkit.getWorld(name());
|
World existing = Bukkit.getWorld(name());
|
||||||
@@ -155,7 +159,7 @@ public class IrisCreator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reportStudioProgress(0.08D, "Resolving dimension");
|
reportStudioProgress(0.08D, "resolve_dimension");
|
||||||
IrisDimension d = IrisToolbelt.getDimension(dimension());
|
IrisDimension d = IrisToolbelt.getDimension(dimension());
|
||||||
|
|
||||||
if (d == null) {
|
if (d == null) {
|
||||||
@@ -165,7 +169,7 @@ public class IrisCreator {
|
|||||||
if (sender == null)
|
if (sender == null)
|
||||||
sender = Iris.getSender();
|
sender = Iris.getSender();
|
||||||
|
|
||||||
reportStudioProgress(0.16D, "Preparing world pack");
|
reportStudioProgress(0.16D, "prepare_world_pack");
|
||||||
if (!studio() || benchmark) {
|
if (!studio() || benchmark) {
|
||||||
Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name()));
|
Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name()));
|
||||||
}
|
}
|
||||||
@@ -174,12 +178,10 @@ public class IrisCreator {
|
|||||||
Iris.info("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
|
Iris.info("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
|
||||||
+ ", regionizedRuntime=" + FoliaScheduler.isRegionizedRuntime(Bukkit.getServer()));
|
+ ", regionizedRuntime=" + FoliaScheduler.isRegionizedRuntime(Bukkit.getServer()));
|
||||||
}
|
}
|
||||||
prebakeNoisemapsBeforeWorldCreate(d);
|
|
||||||
|
|
||||||
reportStudioProgress(0.28D, "Installing datapacks");
|
reportStudioProgress(0.28D, "install_datapacks");
|
||||||
AtomicDouble pp = new AtomicDouble(0);
|
AtomicDouble pp = new AtomicDouble(0);
|
||||||
O<Boolean> done = new O<>();
|
AtomicBoolean done = new AtomicBoolean(false);
|
||||||
done.set(false);
|
|
||||||
WorldCreator wc = new IrisWorldCreator()
|
WorldCreator wc = new IrisWorldCreator()
|
||||||
.dimension(dimension)
|
.dimension(dimension)
|
||||||
.name(name)
|
.name(name)
|
||||||
@@ -199,104 +201,63 @@ public class IrisCreator {
|
|||||||
extraWorldDatapackFoldersByPack = new KMap<>();
|
extraWorldDatapackFoldersByPack = new KMap<>();
|
||||||
extraWorldDatapackFoldersByPack.put(d.getLoadKey(), studioDatapackFolders);
|
extraWorldDatapackFoldersByPack.put(d.getLoadKey(), studioDatapackFolders);
|
||||||
}
|
}
|
||||||
if (ServerConfigurator.installDataPacks(verifyDataPacks, includeExternalDataPacks, extraWorldDatapackFoldersByPack)) {
|
lastDatapackReadinessResult = DatapackReadinessResult.installForStudioWorld(
|
||||||
throw new IrisException("Datapacks were missing!");
|
d.getLoadKey(),
|
||||||
|
d.getDimensionTypeKey(),
|
||||||
|
new File(Bukkit.getWorldContainer(), name()),
|
||||||
|
verifyDataPacks,
|
||||||
|
includeExternalDataPacks,
|
||||||
|
extraWorldDatapackFoldersByPack
|
||||||
|
);
|
||||||
|
if (!"ok".equals(lastDatapackReadinessResult.getExternalDatapackInstallResult())) {
|
||||||
|
throw new IrisException("Datapack external install failed: " + lastDatapackReadinessResult.getExternalDatapackInstallResult());
|
||||||
}
|
}
|
||||||
reportStudioProgress(0.40D, "Datapacks ready");
|
if (lastDatapackReadinessResult.isRestartRequired()) {
|
||||||
|
throw new IrisException("Datapack install requested a server restart for "
|
||||||
|
+ d.getLoadKey()
|
||||||
|
+ ". folders="
|
||||||
|
+ lastDatapackReadinessResult.getResolvedDatapackFolders());
|
||||||
|
}
|
||||||
|
if (!lastDatapackReadinessResult.isVerificationPassed()) {
|
||||||
|
throw new IrisException("Datapack readiness verification failed for "
|
||||||
|
+ d.getLoadKey()
|
||||||
|
+ ". missingPaths="
|
||||||
|
+ lastDatapackReadinessResult.getMissingPaths()
|
||||||
|
+ ", folders="
|
||||||
|
+ lastDatapackReadinessResult.getResolvedDatapackFolders());
|
||||||
|
}
|
||||||
|
reportStudioProgress(0.40D, "install_datapacks");
|
||||||
|
|
||||||
PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator();
|
PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator();
|
||||||
if (access == null) throw new IrisException("Access is null. Something bad happened.");
|
if (access == null) throw new IrisException("Access is null. Something bad happened.");
|
||||||
|
AtomicInteger createProgressTask = startCreateProgressReporter(access, done);
|
||||||
J.a(() -> {
|
|
||||||
IntSupplier g = () -> {
|
|
||||||
if (access.getEngine() == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return access.getEngine().getGenerated();
|
|
||||||
};
|
|
||||||
if(!benchmark) {
|
|
||||||
int req = access.getSpawnChunks().join();
|
|
||||||
for (int c = 0; c < req && !done.get(); c = g.getAsInt()) {
|
|
||||||
double v = (double) c / req;
|
|
||||||
if (studioProgressConsumer != null) {
|
|
||||||
reportStudioProgress(0.40D + (0.42D * v), "Generating spawn");
|
|
||||||
J.sleep(16);
|
|
||||||
} else if (sender.isPlayer()) {
|
|
||||||
sender.sendProgress(v, "Generating");
|
|
||||||
J.sleep(16);
|
|
||||||
} else {
|
|
||||||
sender.sendMessage(C.WHITE + "Generating " + Form.pc(v) + ((C.GRAY + " (" + (req - c) + " Left)")));
|
|
||||||
J.sleep(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
World world;
|
World world;
|
||||||
reportStudioProgress(0.46D, "Creating world");
|
reportStudioProgress(0.46D, "create_world");
|
||||||
try {
|
try {
|
||||||
world = J.sfut(() -> INMS.get().createWorldAsync(wc))
|
WorldLifecycleCaller callerKind = benchmark ? WorldLifecycleCaller.BENCHMARK : studio() ? WorldLifecycleCaller.STUDIO : WorldLifecycleCaller.CREATE;
|
||||||
|
WorldLifecycleRequest request = WorldLifecycleRequest.fromCreator(wc, studio(), benchmark, callerKind);
|
||||||
|
world = J.sfut(() -> INMS.get().createWorldAsync(wc, request))
|
||||||
.thenCompose(Function.identity())
|
.thenCompose(Function.identity())
|
||||||
.get();
|
.get();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
done.set(true);
|
done.set(true);
|
||||||
|
cancelRepeatingTask(createProgressTask);
|
||||||
if (J.isFolia() && containsCreateWorldUnsupportedOperation(e)) {
|
if (J.isFolia() && containsCreateWorldUnsupportedOperation(e)) {
|
||||||
if (FoliaWorldsLink.get().isActive()) {
|
throw new IrisException("Runtime world creation is blocked and the selected world lifecycle backend could not create the world.", e);
|
||||||
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);
|
throw new IrisException("Failed to create world with backend family " + WorldLifecycleService.get().capabilities().serverFamily().id() + "!", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
done.set(true);
|
done.set(true);
|
||||||
reportStudioProgress(0.86D, "World created");
|
cancelRepeatingTask(createProgressTask);
|
||||||
|
reportStudioProgress(0.86D, "create_world");
|
||||||
|
|
||||||
if (sender.isPlayer() && !benchmark) {
|
if (!studio && !benchmark) {
|
||||||
Player senderPlayer = sender.player();
|
|
||||||
if (senderPlayer == null) {
|
|
||||||
Iris.warn("Studio opened, but sender player reference is unavailable for teleport.");
|
|
||||||
} else {
|
|
||||||
Location studioEntryLocation = resolveStudioEntryLocation(world);
|
|
||||||
if (studioEntryLocation == null) {
|
|
||||||
sender.sendMessage(C.YELLOW + "Studio opened, but entry location could not be resolved safely.");
|
|
||||||
} else {
|
|
||||||
prewarmStudioEntryChunks(world, studioEntryLocation, STUDIO_PREWARM_RADIUS_CHUNKS, STUDIO_PREWARM_TIMEOUT);
|
|
||||||
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(senderPlayer, studioEntryLocation);
|
|
||||||
if (teleportFuture != null) {
|
|
||||||
teleportFuture.thenAccept(success -> {
|
|
||||||
if (Boolean.TRUE.equals(success)) {
|
|
||||||
J.runEntity(senderPlayer, () -> Iris.service(BoardSVC.class).updatePlayer(senderPlayer));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
teleportFuture.exceptionally(throwable -> {
|
|
||||||
Iris.warn("Failed to schedule studio teleport task for " + senderPlayer.getName() + ".");
|
|
||||||
Iris.reportError(throwable);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (studio || benchmark) {
|
|
||||||
Runnable applyStudioWorldSettings = () -> {
|
|
||||||
Iris.linkMultiverseCore.removeFromConfig(world);
|
|
||||||
|
|
||||||
if (IrisSettings.get().getStudio().isDisableTimeAndWeather()) {
|
|
||||||
setBooleanGameRule(world, false, "ADVANCE_WEATHER", "DO_WEATHER_CYCLE", "WEATHER_CYCLE", "doWeatherCycle", "weatherCycle");
|
|
||||||
setBooleanGameRule(world, false, "ADVANCE_TIME", "DO_DAYLIGHT_CYCLE", "DAYLIGHT_CYCLE", "doDaylightCycle", "daylightCycle");
|
|
||||||
world.setTime(6000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
J.s(applyStudioWorldSettings);
|
|
||||||
} else {
|
|
||||||
addToBukkitYml();
|
addToBukkitYml();
|
||||||
J.s(() -> Iris.linkMultiverseCore.updateWorld(world, dimension));
|
J.s(() -> Iris.linkMultiverseCore.updateWorld(world, dimension));
|
||||||
}
|
}
|
||||||
reportStudioProgress(0.93D, "Applying world settings");
|
|
||||||
|
|
||||||
if (pregen != null) {
|
if (pregen != null) {
|
||||||
CompletableFuture<Boolean> ff = new CompletableFuture<>();
|
CompletableFuture<Boolean> ff = new CompletableFuture<>();
|
||||||
@@ -305,28 +266,18 @@ public class IrisCreator {
|
|||||||
.onProgress(pp::set)
|
.onProgress(pp::set)
|
||||||
.whenDone(() -> ff.complete(true));
|
.whenDone(() -> ff.complete(true));
|
||||||
|
|
||||||
|
AtomicBoolean dx = new AtomicBoolean(false);
|
||||||
|
AtomicInteger pregenProgressTask = startPregenProgressReporter(pp, dx);
|
||||||
try {
|
try {
|
||||||
AtomicBoolean dx = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
J.a(() -> {
|
|
||||||
while (!dx.get()) {
|
|
||||||
if (sender.isPlayer()) {
|
|
||||||
sender.sendProgress(pp.get(), "Pregenerating");
|
|
||||||
J.sleep(16);
|
|
||||||
} else {
|
|
||||||
sender.sendMessage(C.WHITE + "Pregenerating " + Form.pc(pp.get()));
|
|
||||||
J.sleep(1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ff.get();
|
ff.get();
|
||||||
dx.set(true);
|
dx.set(true);
|
||||||
|
cancelRepeatingTask(pregenProgressTask);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
dx.set(true);
|
||||||
|
cancelRepeatingTask(pregenProgressTask);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reportStudioProgress(0.98D, "Finalizing");
|
|
||||||
return world;
|
return world;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,360 +291,89 @@ public class IrisCreator {
|
|||||||
try {
|
try {
|
||||||
consumer.accept(clamped, stage);
|
consumer.accept(clamped, stage);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError(e);
|
Iris.reportError("Studio progress consumer failed for world \"" + name() + "\".", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prebakeNoisemapsBeforeWorldCreate(IrisDimension dimension) {
|
private AtomicInteger startCreateProgressReporter(PlatformChunkGenerator access, AtomicBoolean done) {
|
||||||
IrisSettings.IrisSettingsPregen pregenSettings = IrisSettings.get().getPregen();
|
AtomicInteger taskId = new AtomicInteger(-1);
|
||||||
if (!pregenSettings.isStartupNoisemapPrebake()) {
|
if (benchmark) {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
IntSupplier generatedSupplier = () -> {
|
||||||
|
if (access.getEngine() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return access.getEngine().getGenerated();
|
||||||
|
};
|
||||||
|
access.getSpawnChunks().whenComplete((required, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
Iris.reportError("Failed to resolve studio spawn chunk target for world \"" + name() + "\".", throwable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (done.get() || required == null || required <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int interval = studioProgressConsumer != null || sender.isPlayer() ? 1 : 20;
|
||||||
|
taskId.set(J.ar(() -> {
|
||||||
|
if (done.get()) {
|
||||||
|
cancelRepeatingTask(taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int generated = generatedSupplier.getAsInt();
|
||||||
|
if (generated >= required) {
|
||||||
|
cancelRepeatingTask(taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double progress = (double) generated / required;
|
||||||
|
if (studioProgressConsumer != null) {
|
||||||
|
reportStudioProgress(0.40D + (0.42D * progress), "create_world");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sender.isPlayer()) {
|
||||||
|
sender.sendProgress(progress, "Generating");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(C.WHITE + "Generating " + Form.pc(progress) + ((C.GRAY + " (" + (required - generated) + " Left)")));
|
||||||
|
}, interval));
|
||||||
|
});
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AtomicInteger startPregenProgressReporter(AtomicDouble progress, AtomicBoolean done) {
|
||||||
|
AtomicInteger taskId = new AtomicInteger(-1);
|
||||||
|
int interval = sender.isPlayer() ? 1 : 20;
|
||||||
|
taskId.set(J.ar(() -> {
|
||||||
|
if (done.get()) {
|
||||||
|
cancelRepeatingTask(taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sender.isPlayer()) {
|
||||||
|
sender.sendProgress(progress.get(), "Pregenerating");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(C.WHITE + "Pregenerating " + Form.pc(progress.get()));
|
||||||
|
}, interval));
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelRepeatingTask(AtomicInteger taskId) {
|
||||||
|
if (taskId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (studio() && !benchmark) {
|
int id = taskId.getAndSet(-1);
|
||||||
boolean startupPrebakeReady = IrisNoisemapPrebakePipeline.awaitInstalledPacksPrebakeForStudio();
|
if (id >= 0) {
|
||||||
if (startupPrebakeReady) {
|
J.car(id);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
File targetDataFolder = new File(Bukkit.getWorldContainer(), name());
|
|
||||||
if (studio() && !benchmark) {
|
|
||||||
IrisData studioData = dimension.getLoader();
|
|
||||||
if (studioData != null) {
|
|
||||||
targetDataFolder = studioData.getDataFolder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IrisData targetData = IrisData.get(targetDataFolder);
|
|
||||||
SeedManager seedManager = new SeedManager(seed());
|
|
||||||
IrisNoisemapPrebakePipeline.prebake(targetData, seedManager, name(), dimension.getLoadKey());
|
|
||||||
} catch (Throwable throwable) {
|
|
||||||
Iris.warn("Failed pre-create noisemap pre-bake for " + name() + "/" + dimension.getLoadKey() + ": " + throwable.getMessage());
|
|
||||||
Iris.reportError(throwable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Location resolveStudioEntryLocation(World world) {
|
|
||||||
CompletableFuture<Location> locationFuture = J.sfut(() -> {
|
|
||||||
Location spawnLocation = world.getSpawnLocation();
|
|
||||||
if (spawnLocation != null) {
|
|
||||||
return spawnLocation.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
int x = 0;
|
|
||||||
int z = 0;
|
|
||||||
int y = Math.max(world.getMinHeight() + 1, 96);
|
|
||||||
return new Location(world, x + 0.5D, y, z + 0.5D);
|
|
||||||
});
|
|
||||||
if (locationFuture == null) {
|
|
||||||
Iris.warn("Failed to schedule studio entry-location resolve task on the global scheduler for world \"" + world.getName() + "\".");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Location rawLocation = locationFuture.get(15, TimeUnit.SECONDS);
|
|
||||||
return resolveTopSafeStudioLocation(world, rawLocation);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Iris.warn("Failed to resolve studio entry location for world \"" + world.getName() + "\".");
|
|
||||||
Iris.reportError(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Location resolveTopSafeStudioLocation(World world, Location rawLocation) {
|
|
||||||
if (world == null || rawLocation == null) {
|
|
||||||
return rawLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
int chunkX = rawLocation.getBlockX() >> 4;
|
|
||||||
int chunkZ = rawLocation.getBlockZ() >> 4;
|
|
||||||
try {
|
|
||||||
CompletableFuture<Chunk> chunkFuture = PaperLib.getChunkAtAsync(world, chunkX, chunkZ, false);
|
|
||||||
if (chunkFuture != null) {
|
|
||||||
chunkFuture.get(10, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return rawLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!world.isChunkLoaded(chunkX, chunkZ)) {
|
|
||||||
return rawLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<Location> regionFuture = new CompletableFuture<>();
|
|
||||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
|
||||||
try {
|
|
||||||
regionFuture.complete(findTopSafeStudioLocation(world, rawLocation));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
regionFuture.completeExceptionally(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!scheduled) {
|
|
||||||
return rawLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Location resolved = regionFuture.get(15, TimeUnit.SECONDS);
|
|
||||||
return resolved == null ? rawLocation : resolved;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Iris.warn("Failed to resolve safe studio entry surface for world \"" + world.getName() + "\".");
|
|
||||||
Iris.reportError(e);
|
|
||||||
return rawLocation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Location findTopSafeStudioLocation(World world, Location source) {
|
|
||||||
int x = source.getBlockX();
|
|
||||||
int z = source.getBlockZ();
|
|
||||||
int minY = world.getMinHeight() + 1;
|
|
||||||
int maxY = world.getMaxHeight() - 2;
|
|
||||||
int sourceY = source.getBlockY();
|
|
||||||
int startY = Math.max(minY, Math.min(maxY, sourceY));
|
|
||||||
float yaw = source.getYaw();
|
|
||||||
float pitch = source.getPitch();
|
|
||||||
|
|
||||||
int upperBound = Math.min(maxY, startY + 32);
|
|
||||||
for (int y = startY; y <= upperBound; y++) {
|
|
||||||
if (isSafeStandingLocation(world, x, y, z)) {
|
|
||||||
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int lowerBound = Math.max(minY, startY - 64);
|
|
||||||
for (int y = startY - 1; y >= lowerBound; y--) {
|
|
||||||
if (isSafeStandingLocation(world, x, y, z)) {
|
|
||||||
return new Location(world, x + 0.5D, y, z + 0.5D, yaw, pitch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int fallbackY = Math.max(minY, Math.min(maxY, source.getBlockY()));
|
|
||||||
return new Location(world, x + 0.5D, fallbackY, z + 0.5D, yaw, pitch);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSafeStandingLocation(World world, int x, int y, int z) {
|
|
||||||
if (y <= world.getMinHeight() || y >= world.getMaxHeight() - 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Block below = world.getBlockAt(x, y - 1, z);
|
|
||||||
Block feet = world.getBlockAt(x, y, z);
|
|
||||||
Block head = world.getBlockAt(x, y + 1, z);
|
|
||||||
|
|
||||||
Material belowType = below.getType();
|
|
||||||
if (!belowType.isSolid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Tag.LEAVES.isTagged(belowType)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (belowType == Material.LAVA
|
|
||||||
|| belowType == Material.MAGMA_BLOCK
|
|
||||||
|| belowType == Material.FIRE
|
|
||||||
|| belowType == Material.SOUL_FIRE
|
|
||||||
|| belowType == Material.CAMPFIRE
|
|
||||||
|| belowType == Material.SOUL_CAMPFIRE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (feet.getType().isSolid() || head.getType().isSolid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (feet.isLiquid() || head.isLiquid()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void prewarmStudioEntryChunks(World world, Location entry, int radiusChunks, Duration timeout) throws IrisException {
|
|
||||||
if (world == null || entry == null) {
|
|
||||||
throw new IrisException("Studio prewarm failed: world or entry location is null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
int centerChunkX = entry.getBlockX() >> 4;
|
|
||||||
int centerChunkZ = entry.getBlockZ() >> 4;
|
|
||||||
List<StudioChunkCoordinate> chunkTargets = resolveStudioPrewarmTargets(centerChunkX, centerChunkZ, radiusChunks);
|
|
||||||
if (chunkTargets.isEmpty()) {
|
|
||||||
throw new IrisException("Studio prewarm failed: no target chunks were resolved.");
|
|
||||||
}
|
|
||||||
|
|
||||||
int loadedBefore = 0;
|
|
||||||
Map<StudioChunkCoordinate, CompletableFuture<Chunk>> futures = new LinkedHashMap<>();
|
|
||||||
for (StudioChunkCoordinate coordinate : chunkTargets) {
|
|
||||||
if (world.isChunkLoaded(coordinate.getX(), coordinate.getZ())) {
|
|
||||||
loadedBefore++;
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<Chunk> chunkFuture = PaperLib.getChunkAtAsync(world, coordinate.getX(), coordinate.getZ(), true);
|
|
||||||
if (chunkFuture == null) {
|
|
||||||
throw new IrisException("Studio prewarm failed: async chunk future was null for " + coordinate + ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
futures.put(coordinate, chunkFuture);
|
|
||||||
}
|
|
||||||
|
|
||||||
int total = chunkTargets.size();
|
|
||||||
int completed = 0;
|
|
||||||
Set<StudioChunkCoordinate> remaining = new LinkedHashSet<>(chunkTargets);
|
|
||||||
long startNanos = System.nanoTime();
|
|
||||||
long timeoutNanos = Math.max(1L, timeout.toNanos());
|
|
||||||
reportStudioProgress(0.88D, "Prewarming entry chunks (0/" + total + ")");
|
|
||||||
|
|
||||||
while (!remaining.isEmpty()) {
|
|
||||||
long elapsedNanos = System.nanoTime() - startNanos;
|
|
||||||
if (elapsedNanos >= timeoutNanos) {
|
|
||||||
StudioPrewarmDiagnostics diagnostics = buildStudioPrewarmDiagnostics(world, chunkTargets, remaining, loadedBefore, elapsedNanos);
|
|
||||||
throw new IrisException("Studio prewarm timed out: " + diagnostics.toMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean progressed = false;
|
|
||||||
List<StudioChunkCoordinate> completedCoordinates = new ArrayList<>();
|
|
||||||
for (StudioChunkCoordinate coordinate : remaining) {
|
|
||||||
CompletableFuture<Chunk> chunkFuture = futures.get(coordinate);
|
|
||||||
if (chunkFuture == null || !chunkFuture.isDone()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Chunk loadedChunk = chunkFuture.get();
|
|
||||||
if (loadedChunk == null) {
|
|
||||||
throw new IrisException("Studio prewarm failed: chunk " + coordinate + " resolved to null.");
|
|
||||||
}
|
|
||||||
} catch (IrisException e) {
|
|
||||||
throw e;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new IrisException("Studio prewarm failed while loading chunk " + coordinate + ".", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
completedCoordinates.add(coordinate);
|
|
||||||
progressed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!completedCoordinates.isEmpty()) {
|
|
||||||
for (StudioChunkCoordinate completedCoordinate : completedCoordinates) {
|
|
||||||
remaining.remove(completedCoordinate);
|
|
||||||
}
|
|
||||||
|
|
||||||
completed += completedCoordinates.size();
|
|
||||||
double ratio = (double) completed / (double) total;
|
|
||||||
reportStudioProgress(0.88D + (0.04D * ratio), "Prewarming entry chunks (" + completed + "/" + total + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!progressed) {
|
|
||||||
J.sleep(20);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
long elapsedNanos = System.nanoTime() - startNanos;
|
|
||||||
StudioPrewarmDiagnostics diagnostics = buildStudioPrewarmDiagnostics(world, chunkTargets, new LinkedHashSet<>(), loadedBefore, elapsedNanos);
|
|
||||||
Iris.info("Studio prewarm complete: " + diagnostics.toMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
private StudioPrewarmDiagnostics buildStudioPrewarmDiagnostics(
|
|
||||||
World world,
|
|
||||||
List<StudioChunkCoordinate> chunkTargets,
|
|
||||||
Set<StudioChunkCoordinate> timedOutChunks,
|
|
||||||
int loadedBefore,
|
|
||||||
long elapsedNanos
|
|
||||||
) {
|
|
||||||
int loadedAfter = 0;
|
|
||||||
for (StudioChunkCoordinate coordinate : chunkTargets) {
|
|
||||||
if (world.isChunkLoaded(coordinate.getX(), coordinate.getZ())) {
|
|
||||||
loadedAfter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int generatedDuring = Math.max(0, loadedAfter - loadedBefore);
|
|
||||||
List<String> timedOut = new ArrayList<>();
|
|
||||||
for (StudioChunkCoordinate timedOutChunk : timedOutChunks) {
|
|
||||||
timedOut.add(timedOutChunk.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(Math.max(0L, elapsedNanos));
|
|
||||||
return new StudioPrewarmDiagnostics(elapsedMs, loadedBefore, loadedAfter, generatedDuring, timedOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<StudioChunkCoordinate> resolveStudioPrewarmTargets(int centerChunkX, int centerChunkZ, int radiusChunks) {
|
|
||||||
int safeRadius = Math.max(0, radiusChunks);
|
|
||||||
List<StudioChunkCoordinate> targets = new ArrayList<>();
|
|
||||||
targets.add(new StudioChunkCoordinate(centerChunkX, centerChunkZ));
|
|
||||||
|
|
||||||
for (int x = -safeRadius; x <= safeRadius; x++) {
|
|
||||||
for (int z = -safeRadius; z <= safeRadius; z++) {
|
|
||||||
if (x == 0 && z == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
targets.add(new StudioChunkCoordinate(centerChunkX + x, centerChunkZ + z));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return targets;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StudioChunkCoordinate {
|
|
||||||
private final int x;
|
|
||||||
private final int z;
|
|
||||||
|
|
||||||
private StudioChunkCoordinate(int x, int z) {
|
|
||||||
this.x = x;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getX() {
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getZ() {
|
|
||||||
return z;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object other) {
|
|
||||||
if (this == other) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(other instanceof StudioChunkCoordinate coordinate)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return x == coordinate.x && z == coordinate.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return 31 * x + z;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return x + "," + z;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class StudioPrewarmDiagnostics {
|
|
||||||
private final long elapsedMs;
|
|
||||||
private final int loadedBefore;
|
|
||||||
private final int loadedAfter;
|
|
||||||
private final int generatedDuring;
|
|
||||||
private final List<String> timedOutChunks;
|
|
||||||
|
|
||||||
private StudioPrewarmDiagnostics(long elapsedMs, int loadedBefore, int loadedAfter, int generatedDuring, List<String> timedOutChunks) {
|
|
||||||
this.elapsedMs = elapsedMs;
|
|
||||||
this.loadedBefore = loadedBefore;
|
|
||||||
this.loadedAfter = loadedAfter;
|
|
||||||
this.generatedDuring = generatedDuring;
|
|
||||||
this.timedOutChunks = new ArrayList<>(timedOutChunks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String toMessage() {
|
|
||||||
return "elapsedMs=" + elapsedMs
|
|
||||||
+ ", loadedBefore=" + loadedBefore
|
|
||||||
+ ", loadedAfter=" + loadedAfter
|
|
||||||
+ ", generatedDuring=" + generatedDuring
|
|
||||||
+ ", timedOut=" + timedOutChunks;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,138 +393,6 @@ public class IrisCreator {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static void setBooleanGameRule(World world, boolean value, String... names) {
|
|
||||||
GameRule<Boolean> gameRule = resolveBooleanGameRule(world, names);
|
|
||||||
if (gameRule != null) {
|
|
||||||
world.setGameRule(gameRule, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static GameRule<Boolean> resolveBooleanGameRule(World world, String... names) {
|
|
||||||
if (world == null || names == null || names.length == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> candidates = buildRuleNameCandidates(names);
|
|
||||||
for (String name : candidates) {
|
|
||||||
if (name == null || name.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Field field = GameRule.class.getField(name);
|
|
||||||
Object value = field.get(null);
|
|
||||||
if (value instanceof GameRule<?> gameRule && Boolean.class.equals(gameRule.getType())) {
|
|
||||||
return (GameRule<Boolean>) gameRule;
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
GameRule<?> byName = GameRule.getByName(name);
|
|
||||||
if (byName != null && Boolean.class.equals(byName.getType())) {
|
|
||||||
return (GameRule<Boolean>) byName;
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] availableRules = world.getGameRules();
|
|
||||||
if (availableRules == null || availableRules.length == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> normalizedCandidates = new LinkedHashSet<>();
|
|
||||||
for (String candidate : candidates) {
|
|
||||||
if (candidate != null && !candidate.isBlank()) {
|
|
||||||
normalizedCandidates.add(normalizeRuleName(candidate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String availableRule : availableRules) {
|
|
||||||
String normalizedAvailable = normalizeRuleName(availableRule);
|
|
||||||
if (!normalizedCandidates.contains(normalizedAvailable)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
GameRule<?> byName = GameRule.getByName(availableRule);
|
|
||||||
if (byName != null && Boolean.class.equals(byName.getType())) {
|
|
||||||
return (GameRule<Boolean>) byName;
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Set<String> buildRuleNameCandidates(String... names) {
|
|
||||||
Set<String> candidates = new LinkedHashSet<>();
|
|
||||||
for (String name : names) {
|
|
||||||
if (name == null || name.isBlank()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates.add(name);
|
|
||||||
candidates.add(name.toLowerCase(Locale.ROOT));
|
|
||||||
|
|
||||||
String lowerCamel = toLowerCamel(name);
|
|
||||||
if (!lowerCamel.isEmpty()) {
|
|
||||||
candidates.add(lowerCamel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String toLowerCamel(String name) {
|
|
||||||
if (name == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String raw = name.trim();
|
|
||||||
if (raw.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] parts = raw.split("_+");
|
|
||||||
if (parts.length == 0) {
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
builder.append(parts[0].toLowerCase(Locale.ROOT));
|
|
||||||
for (int i = 1; i < parts.length; i++) {
|
|
||||||
String part = parts[i].toLowerCase(Locale.ROOT);
|
|
||||||
if (part.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
builder.append(Character.toUpperCase(part.charAt(0)));
|
|
||||||
if (part.length() > 1) {
|
|
||||||
builder.append(part.substring(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String normalizeRuleName(String name) {
|
|
||||||
if (name == null || name.isBlank()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder builder = new StringBuilder(name.length());
|
|
||||||
for (int i = 0; i < name.length(); i++) {
|
|
||||||
char c = name.charAt(i);
|
|
||||||
if (Character.isLetterOrDigit(c)) {
|
|
||||||
builder.append(Character.toLowerCase(c));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addToBukkitYml() {
|
private void addToBukkitYml() {
|
||||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
||||||
String gen = "Iris:" + dimension;
|
String gen = "Iris:" + dimension;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package art.arcane.iris.core.tools;
|
|||||||
|
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.core.link.FoliaWorldsLink;
|
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -100,10 +100,10 @@ public class IrisPackBenchmarking {
|
|||||||
}
|
}
|
||||||
|
|
||||||
J.s(() -> {
|
J.s(() -> {
|
||||||
var world = Bukkit.getWorld("benchmark");
|
org.bukkit.World world = Bukkit.getWorld("benchmark");
|
||||||
if (world == null) return;
|
if (world == null) return;
|
||||||
IrisToolbelt.evacuate(world);
|
IrisToolbelt.evacuate(world);
|
||||||
FoliaWorldsLink.get().unloadWorld(world, true);
|
WorldLifecycleService.get().unload(world, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
stopwatch.end();
|
stopwatch.end();
|
||||||
|
|||||||
@@ -90,9 +90,10 @@ public class IrisEngine implements Engine {
|
|||||||
private final int art;
|
private final int art;
|
||||||
private final AtomicCache<IrisEngineData> engineData = new AtomicCache<>();
|
private final AtomicCache<IrisEngineData> engineData = new AtomicCache<>();
|
||||||
private final AtomicBoolean cleaning;
|
private final AtomicBoolean cleaning;
|
||||||
private final AtomicBoolean noisemapPrebakeRunning;
|
|
||||||
private final ChronoLatch cleanLatch;
|
private final ChronoLatch cleanLatch;
|
||||||
private final SeedManager seedManager;
|
private final SeedManager seedManager;
|
||||||
|
private final GenerationSessionManager generationSessions;
|
||||||
|
private final AtomicBoolean closing;
|
||||||
private CompletableFuture<Long> hash32;
|
private CompletableFuture<Long> hash32;
|
||||||
private EngineMode mode;
|
private EngineMode mode;
|
||||||
private EngineEffects effects;
|
private EngineEffects effects;
|
||||||
@@ -113,6 +114,8 @@ public class IrisEngine implements Engine {
|
|||||||
getEngineData();
|
getEngineData();
|
||||||
verifySeed();
|
verifySeed();
|
||||||
this.seedManager = new SeedManager(target.getWorld().getRawWorldSeed());
|
this.seedManager = new SeedManager(target.getWorld().getRawWorldSeed());
|
||||||
|
this.generationSessions = new GenerationSessionManager();
|
||||||
|
this.closing = new AtomicBoolean(false);
|
||||||
bud = new AtomicInteger(0);
|
bud = new AtomicInteger(0);
|
||||||
buds = new AtomicInteger(0);
|
buds = new AtomicInteger(0);
|
||||||
metrics = new EngineMetrics(32);
|
metrics = new EngineMetrics(32);
|
||||||
@@ -127,7 +130,6 @@ public class IrisEngine implements Engine {
|
|||||||
mantle = new IrisEngineMantle(this);
|
mantle = new IrisEngineMantle(this);
|
||||||
context = new IrisContext(this);
|
context = new IrisContext(this);
|
||||||
cleaning = new AtomicBoolean(false);
|
cleaning = new AtomicBoolean(false);
|
||||||
noisemapPrebakeRunning = new AtomicBoolean(false);
|
|
||||||
modeFallbackLogged = new AtomicBoolean(false);
|
modeFallbackLogged = new AtomicBoolean(false);
|
||||||
if (studio) {
|
if (studio) {
|
||||||
getData().dump();
|
getData().dump();
|
||||||
@@ -164,6 +166,13 @@ public class IrisEngine implements Engine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void prehotload() {
|
private void prehotload() {
|
||||||
|
closing.set(true);
|
||||||
|
try {
|
||||||
|
generationSessions.sealAndAwait("hotload", 15000L);
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
|
||||||
EngineWorldManager currentWorldManager = worldManager;
|
EngineWorldManager currentWorldManager = worldManager;
|
||||||
worldManager = null;
|
worldManager = null;
|
||||||
if (currentWorldManager != null) {
|
if (currentWorldManager != null) {
|
||||||
@@ -193,6 +202,8 @@ public class IrisEngine implements Engine {
|
|||||||
|
|
||||||
private void setupEngine() {
|
private void setupEngine() {
|
||||||
try {
|
try {
|
||||||
|
generationSessions.activateNextSession();
|
||||||
|
closing.set(false);
|
||||||
Iris.debug("Setup Engine " + getCacheID());
|
Iris.debug("Setup Engine " + getCacheID());
|
||||||
cacheId = RNG.r.nextInt();
|
cacheId = RNG.r.nextInt();
|
||||||
complex = ensureComplex();
|
complex = ensureComplex();
|
||||||
@@ -215,7 +226,6 @@ public class IrisEngine implements Engine {
|
|||||||
.toArray(File[]::new);
|
.toArray(File[]::new);
|
||||||
hash32.complete(IO.hashRecursive(roots));
|
hash32.complete(IO.hashRecursive(roots));
|
||||||
});
|
});
|
||||||
scheduleStartupNoisemapPrebake();
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.error("FAILED TO SETUP ENGINE!");
|
Iris.error("FAILED TO SETUP ENGINE!");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -297,30 +307,6 @@ public class IrisEngine implements Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleStartupNoisemapPrebake() {
|
|
||||||
if (!IrisSettings.get().getPregen().isStartupNoisemapPrebake()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (studio) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!noisemapPrebakeRunning.compareAndSet(false, true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
J.a(() -> {
|
|
||||||
try {
|
|
||||||
IrisNoisemapPrebakePipeline.prebake(this);
|
|
||||||
} catch (Throwable throwable) {
|
|
||||||
Iris.reportError(throwable);
|
|
||||||
} finally {
|
|
||||||
noisemapPrebakeRunning.set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
|
public void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
|
||||||
getMantle().generateMatter(x, z, multicore, context);
|
getMantle().generateMatter(x, z, multicore, context);
|
||||||
@@ -532,8 +518,14 @@ public class IrisEngine implements Engine {
|
|||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
PregeneratorJob.shutdownInstance();
|
PregeneratorJob.shutdownInstance();
|
||||||
|
closing.set(true);
|
||||||
closed = true;
|
closed = true;
|
||||||
J.car(art);
|
J.car(art);
|
||||||
|
try {
|
||||||
|
generationSessions.sealAndAwait("close", 15000L, true);
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
EngineWorldManager currentWorldManager = getWorldManager();
|
EngineWorldManager currentWorldManager = getWorldManager();
|
||||||
if (currentWorldManager != null) {
|
if (currentWorldManager != null) {
|
||||||
currentWorldManager.close();
|
currentWorldManager.close();
|
||||||
@@ -574,6 +566,10 @@ public class IrisEngine implements Engine {
|
|||||||
return closed;
|
return closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isClosing() {
|
||||||
|
return closing.get();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void recycle() {
|
public void recycle() {
|
||||||
if (!cleanLatch.flip()) {
|
if (!cleanLatch.flip()) {
|
||||||
@@ -602,13 +598,14 @@ public class IrisEngine implements Engine {
|
|||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
@Override
|
@Override
|
||||||
public void generate(int x, int z, Hunk<BlockData> vblocks, Hunk<Biome> vbiomes, boolean multicore) throws WrongEngineBroException {
|
public void generate(int x, int z, Hunk<BlockData> vblocks, Hunk<Biome> vbiomes, boolean multicore) throws WrongEngineBroException {
|
||||||
if (closed) {
|
if (closing.get() || closed) {
|
||||||
throw new WrongEngineBroException();
|
throw new GenerationSessionException("Generation session is closed for world \"" + getWorld().name() + "\".", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.touch();
|
try (GenerationSessionLease lease = acquireGenerationLease("chunk_generate")) {
|
||||||
getEngineData().getStatistics().generatedChunk();
|
context.touch();
|
||||||
try {
|
context.setGenerationSessionId(lease.sessionId());
|
||||||
|
getEngineData().getStatistics().generatedChunk();
|
||||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||||
Hunk<BlockData> blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t));
|
Hunk<BlockData> blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t));
|
||||||
|
|
||||||
@@ -634,12 +631,19 @@ public class IrisEngine implements Engine {
|
|||||||
if (generated.get() == 661) {
|
if (generated.get() == 661) {
|
||||||
J.a(() -> getData().savePrefetch(this));
|
J.a(() -> getData().savePrefetch(this));
|
||||||
}
|
}
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
throw e;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError(e);
|
Iris.reportError(e);
|
||||||
fail("Failed to generate " + x + ", " + z, e);
|
fail("Failed to generate " + x + ", " + z, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GenerationSessionManager getGenerationSessions() {
|
||||||
|
return generationSessions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveEngineData() {
|
public void saveEngineData() {
|
||||||
//TODO: Method this file
|
//TODO: Method this file
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -111,6 +111,10 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
|
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
|
default boolean isClosing() {
|
||||||
|
return isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
IrisContext getContext();
|
IrisContext getContext();
|
||||||
|
|
||||||
double getMaxBiomeObjectDensity();
|
double getMaxBiomeObjectDensity();
|
||||||
@@ -121,6 +125,24 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat
|
|||||||
|
|
||||||
boolean isClosed();
|
boolean isClosed();
|
||||||
|
|
||||||
|
default GenerationSessionManager getGenerationSessions() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
default GenerationSessionLease acquireGenerationLease(String operation) throws GenerationSessionException {
|
||||||
|
GenerationSessionManager generationSessions = getGenerationSessions();
|
||||||
|
if (generationSessions == null) {
|
||||||
|
return GenerationSessionLease.noop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return generationSessions.acquire(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
default long getGenerationSessionId() {
|
||||||
|
GenerationSessionManager generationSessions = getGenerationSessions();
|
||||||
|
return generationSessions == null ? 0L : generationSessions.currentSessionId();
|
||||||
|
}
|
||||||
|
|
||||||
EngineWorldManager getWorldManager();
|
EngineWorldManager getWorldManager();
|
||||||
|
|
||||||
default UUID getBiomeID(int x, int z) {
|
default UUID getBiomeID(int x, int z) {
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public interface EngineMode extends Staged {
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default void generate(int x, int z, Hunk<BlockData> blocks, Hunk<Biome> biomes, boolean multicore) {
|
default void generate(int x, int z, Hunk<BlockData> blocks, Hunk<Biome> biomes, boolean multicore) {
|
||||||
|
IrisContext context = IrisContext.getOr(getEngine());
|
||||||
boolean cacheContext = true;
|
boolean cacheContext = true;
|
||||||
if (J.isFolia()) {
|
if (J.isFolia()) {
|
||||||
org.bukkit.World world = getEngine().getWorld().realWorld();
|
org.bukkit.World world = getEngine().getWorld().realWorld();
|
||||||
@@ -79,8 +80,8 @@ public interface EngineMode extends Staged {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
|
ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE;
|
||||||
ChunkContext ctx = new ChunkContext(x, z, getComplex(), cacheContext, prefillPlan, getEngine().getMetrics());
|
ChunkContext ctx = new ChunkContext(x, z, getComplex(), context.getGenerationSessionId(), cacheContext, prefillPlan, getEngine().getMetrics());
|
||||||
IrisContext.getOr(getEngine()).setChunkContext(ctx);
|
context.setChunkContext(ctx);
|
||||||
|
|
||||||
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
||||||
for (EngineStage i : stages) {
|
for (EngineStage i : stages) {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
public class GenerationSessionException extends WrongEngineBroException {
|
||||||
|
private final boolean expectedTeardown;
|
||||||
|
|
||||||
|
public GenerationSessionException(String message) {
|
||||||
|
this(message, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenerationSessionException(String message, boolean expectedTeardown) {
|
||||||
|
super(message);
|
||||||
|
this.expectedTeardown = expectedTeardown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpectedTeardown() {
|
||||||
|
return expectedTeardown;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
public final class GenerationSessionLease implements AutoCloseable {
|
||||||
|
private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L);
|
||||||
|
|
||||||
|
private final GenerationSessionManager manager;
|
||||||
|
private final GenerationSessionManager.GenerationSessionState state;
|
||||||
|
private final long sessionId;
|
||||||
|
private boolean released;
|
||||||
|
|
||||||
|
GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state, long sessionId) {
|
||||||
|
this.manager = manager;
|
||||||
|
this.state = state;
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
this.released = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GenerationSessionLease noop() {
|
||||||
|
return NOOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long sessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (released || state == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
released = true;
|
||||||
|
manager.releaseLease(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
public final class GenerationSessionManager {
|
||||||
|
private final AtomicLong sessionSequence;
|
||||||
|
private final AtomicReference<GenerationSessionState> current;
|
||||||
|
private final Object drainMonitor;
|
||||||
|
|
||||||
|
public GenerationSessionManager() {
|
||||||
|
this.sessionSequence = new AtomicLong(0L);
|
||||||
|
this.current = new AtomicReference<>(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
|
||||||
|
this.drainMonitor = new Object();
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenerationSessionLease acquire(String operation) throws GenerationSessionException {
|
||||||
|
while (true) {
|
||||||
|
GenerationSessionState state = current.get();
|
||||||
|
if (state == null || !state.accepting().get()) {
|
||||||
|
throw rejected(operation, state == null ? null : state);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.activeLeases().incrementAndGet();
|
||||||
|
if (state != current.get()) {
|
||||||
|
state.activeLeases().decrementAndGet();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.accepting().get()) {
|
||||||
|
releaseLease(state);
|
||||||
|
throw rejected(operation, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GenerationSessionLease(this, state, state.sessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long currentSessionId() {
|
||||||
|
GenerationSessionState state = current.get();
|
||||||
|
return state == null ? 0L : state.sessionId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int activeLeases() {
|
||||||
|
GenerationSessionState state = current.get();
|
||||||
|
return state == null ? 0 : state.activeLeases().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sealAndAwait(String reason, long timeoutMs) throws GenerationSessionException {
|
||||||
|
sealAndAwait(reason, timeoutMs, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sealAndAwait(String reason, long timeoutMs, boolean teardown) throws GenerationSessionException {
|
||||||
|
GenerationSessionState state = current.get();
|
||||||
|
if (state == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.accepting().set(false);
|
||||||
|
state.teardown().set(teardown);
|
||||||
|
state.sealReason().set(reason);
|
||||||
|
long deadline = System.currentTimeMillis() + Math.max(0L, timeoutMs);
|
||||||
|
synchronized (drainMonitor) {
|
||||||
|
while (state.activeLeases().get() > 0) {
|
||||||
|
long remaining = deadline - System.currentTimeMillis();
|
||||||
|
if (remaining <= 0L) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
drainMonitor.wait(remaining);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new GenerationSessionException("Generation session " + state.sessionId() + " was interrupted while draining for " + reason + ".", teardown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.activeLeases().get() > 0) {
|
||||||
|
throw new GenerationSessionException("Generation session " + state.sessionId() + " failed to drain for " + reason + " after " + timeoutMs + "ms. Active leases=" + state.activeLeases().get() + ".", teardown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void activateNextSession() {
|
||||||
|
current.set(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long nextSessionId() {
|
||||||
|
return sessionSequence.incrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseLease(GenerationSessionState state) {
|
||||||
|
int remaining = state.activeLeases().decrementAndGet();
|
||||||
|
if (remaining <= 0) {
|
||||||
|
synchronized (drainMonitor) {
|
||||||
|
drainMonitor.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenerationSessionException rejected(String operation, GenerationSessionState state) {
|
||||||
|
long sessionId = state == null ? currentSessionId() : state.sessionId();
|
||||||
|
boolean teardown = state != null && state.teardown().get();
|
||||||
|
String reason = state == null ? null : state.sealReason().get();
|
||||||
|
if (teardown && reason != null && !reason.isBlank()) {
|
||||||
|
return new GenerationSessionException("Generation session " + sessionId + " rejected new work for " + operation + " during " + reason + ".", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GenerationSessionException("Generation session " + sessionId + " rejected new work for " + operation + ".", teardown);
|
||||||
|
}
|
||||||
|
|
||||||
|
record GenerationSessionState(long sessionId, AtomicBoolean accepting, AtomicInteger activeLeases, AtomicBoolean teardown, AtomicReference<String> sealReason) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import art.arcane.iris.core.tools.IrisToolbelt;
|
|||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisObject;
|
import art.arcane.iris.engine.object.IrisObject;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.iris.util.project.context.ChunkContext;
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
import art.arcane.iris.util.common.format.C;
|
import art.arcane.iris.util.common.format.C;
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
@@ -90,7 +91,12 @@ public interface Locator<T> {
|
|||||||
static Locator<IrisBiome> caveOrMantleBiome(String loadKey) {
|
static Locator<IrisBiome> caveOrMantleBiome(String loadKey) {
|
||||||
return (e, c) -> {
|
return (e, c) -> {
|
||||||
AtomicBoolean found = new AtomicBoolean(false);
|
AtomicBoolean found = new AtomicBoolean(false);
|
||||||
e.generateMatter(c.getX(), c.getZ(), true, new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), false));
|
try (GenerationSessionLease lease = e.acquireGenerationLease("locator_generate_matter")) {
|
||||||
|
IrisContext.getOr(e).setGenerationSessionId(lease.sessionId());
|
||||||
|
e.generateMatter(c.getX(), c.getZ(), true, new ChunkContext(c.getX() << 4, c.getZ() << 4, e.getComplex(), lease.sessionId(), false, ChunkContext.PrefillPlan.NONE, null));
|
||||||
|
} catch (GenerationSessionException sessionException) {
|
||||||
|
throw new IllegalStateException(sessionException);
|
||||||
|
}
|
||||||
e.getMantle().getMantle().iterateChunk(c.getX(), c.getZ(), MatterCavern.class, (x, y, z, t) -> {
|
e.getMantle().getMantle().iterateChunk(c.getX(), c.getZ(), MatterCavern.class, (x, y, z, t) -> {
|
||||||
if (found.get()) {
|
if (found.get()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -19,4 +19,11 @@
|
|||||||
package art.arcane.iris.engine.framework;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
public class WrongEngineBroException extends Exception {
|
public class WrongEngineBroException extends Exception {
|
||||||
|
public WrongEngineBroException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WrongEngineBroException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-8
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine.mantle.components;
|
package art.arcane.iris.engine.mantle.components;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
@@ -81,12 +82,13 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||||
|
IrisComplex complex = context.getComplex();
|
||||||
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(FIELD_SIZE * FIELD_SIZE);
|
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(FIELD_SIZE * FIELD_SIZE);
|
||||||
BlendScratch blendScratch = BLEND_SCRATCH.get();
|
BlendScratch blendScratch = BLEND_SCRATCH.get();
|
||||||
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
|
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
|
||||||
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
||||||
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, resolverState, caveBiomeCache);
|
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState, caveBiomeCache);
|
||||||
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
|
getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
|
||||||
for (WeightedProfile weightedProfile : weightedProfiles) {
|
for (WeightedProfile weightedProfile : weightedProfiles) {
|
||||||
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights);
|
carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights);
|
||||||
@@ -99,7 +101,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights);
|
carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
|
private List<WeightedProfile> resolveWeightedProfiles(int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
|
||||||
BlendScratch blendScratch = BLEND_SCRATCH.get();
|
BlendScratch blendScratch = BLEND_SCRATCH.get();
|
||||||
IrisCaveProfile[] profileField = blendScratch.profileField;
|
IrisCaveProfile[] profileField = blendScratch.profileField;
|
||||||
Map<IrisCaveProfile, double[]> tileProfileWeights = blendScratch.tileProfileWeights;
|
Map<IrisCaveProfile, double[]> tileProfileWeights = blendScratch.tileProfileWeights;
|
||||||
@@ -107,7 +109,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles;
|
IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles;
|
||||||
double[] kernelProfileWeights = blendScratch.kernelProfileWeights;
|
double[] kernelProfileWeights = blendScratch.kernelProfileWeights;
|
||||||
activeProfiles.clear();
|
activeProfiles.clear();
|
||||||
fillProfileField(profileField, chunkX, chunkZ, resolverState, caveBiomeCache);
|
fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, caveBiomeCache);
|
||||||
|
|
||||||
for (int tileX = 0; tileX < TILE_COUNT; tileX++) {
|
for (int tileX = 0; tileX < TILE_COUNT; tileX++) {
|
||||||
for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) {
|
for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) {
|
||||||
@@ -313,7 +315,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
return (tileX * TILE_COUNT) + tileZ;
|
return (tileX * TILE_COUNT) + tileZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
|
private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
|
||||||
int startX = (chunkX << 4) - BLEND_RADIUS;
|
int startX = (chunkX << 4) - BLEND_RADIUS;
|
||||||
int startZ = (chunkZ << 4) - BLEND_RADIUS;
|
int startZ = (chunkZ << 4) - BLEND_RADIUS;
|
||||||
|
|
||||||
@@ -321,7 +323,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
int worldX = startX + fieldX;
|
int worldX = startX + fieldX;
|
||||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||||
int worldZ = startZ + fieldZ;
|
int worldZ = startZ + fieldZ;
|
||||||
profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, resolverState, caveBiomeCache);
|
profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, complex, resolverState, caveBiomeCache);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,14 +338,14 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
|
private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache) {
|
||||||
IrisCaveProfile resolved = null;
|
IrisCaveProfile resolved = null;
|
||||||
IrisCaveProfile dimensionProfile = getDimension().getCaveProfile();
|
IrisCaveProfile dimensionProfile = getDimension().getCaveProfile();
|
||||||
if (isProfileEnabled(dimensionProfile)) {
|
if (isProfileEnabled(dimensionProfile)) {
|
||||||
resolved = dimensionProfile;
|
resolved = dimensionProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisRegion region = getComplex().getRegionStream().get(worldX, worldZ);
|
IrisRegion region = complex.getRegionStream().get(worldX, worldZ);
|
||||||
if (region != null) {
|
if (region != null) {
|
||||||
IrisCaveProfile regionProfile = region.getCaveProfile();
|
IrisCaveProfile regionProfile = region.getCaveProfile();
|
||||||
if (isProfileEnabled(regionProfile)) {
|
if (isProfileEnabled(regionProfile)) {
|
||||||
@@ -351,7 +353,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisBiome surfaceBiome = getComplex().getTrueBiomeStream().get(worldX, worldZ);
|
IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(worldX, worldZ);
|
||||||
if (surfaceBiome != null) {
|
if (surfaceBiome != null) {
|
||||||
IrisCaveProfile surfaceProfile = surfaceBiome.getCaveProfile();
|
IrisCaveProfile surfaceProfile = surfaceBiome.getCaveProfile();
|
||||||
if (isProfileEnabled(surfaceProfile)) {
|
if (isProfileEnabled(surfaceProfile)) {
|
||||||
|
|||||||
+4
-2
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine.mantle.components;
|
package art.arcane.iris.engine.mantle.components;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
@@ -39,11 +40,12 @@ public class MantleFluidBodyComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||||
|
IrisComplex complex = context.getComplex();
|
||||||
RNG rng = new RNG(Cache.key(x, z) + seed() + 405666);
|
RNG rng = new RNG(Cache.key(x, z) + seed() + 405666);
|
||||||
int xxx = 8 + (x << 4);
|
int xxx = 8 + (x << 4);
|
||||||
int zzz = 8 + (z << 4);
|
int zzz = 8 + (z << 4);
|
||||||
IrisRegion region = getComplex().getRegionStream().get(xxx, zzz);
|
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
|
||||||
IrisBiome biome = getComplex().getTrueBiomeStream().get(xxx, zzz);
|
IrisBiome biome = complex.getTrueBiomeStream().get(xxx, zzz);
|
||||||
generate(writer, rng, x, z, region, biome);
|
generate(writer, rng, x, z, region, biome);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-95
@@ -20,8 +20,8 @@ package art.arcane.iris.engine.mantle.components;
|
|||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
|
||||||
import art.arcane.iris.engine.data.cache.Cache;
|
import art.arcane.iris.engine.data.cache.Cache;
|
||||||
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
||||||
@@ -40,8 +40,6 @@ import art.arcane.volmlib.util.math.RNG;
|
|||||||
import art.arcane.volmlib.util.matter.MatterStructurePOI;
|
import art.arcane.volmlib.util.matter.MatterStructurePOI;
|
||||||
import art.arcane.iris.util.project.noise.CNG;
|
import art.arcane.iris.util.project.noise.CNG;
|
||||||
import art.arcane.iris.util.project.noise.NoiseType;
|
import art.arcane.iris.util.project.noise.NoiseType;
|
||||||
import art.arcane.iris.util.common.parallel.BurstExecutor;
|
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
|
||||||
import org.bukkit.util.BlockVector;
|
import org.bukkit.util.BlockVector;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -64,12 +62,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||||
|
IrisComplex complex = context.getComplex();
|
||||||
boolean traceRegen = isRegenTraceThread();
|
boolean traceRegen = isRegenTraceThread();
|
||||||
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
|
RNG rng = applyNoise(x, z, Cache.key(x, z) + seed());
|
||||||
int xxx = 8 + (x << 4);
|
int xxx = 8 + (x << 4);
|
||||||
int zzz = 8 + (z << 4);
|
int zzz = 8 + (z << 4);
|
||||||
IrisRegion region = getComplex().getRegionStream().get(xxx, zzz);
|
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
|
||||||
IrisBiome surfaceBiome = getComplex().getTrueBiomeStream().get(xxx, zzz);
|
IrisBiome surfaceBiome = complex.getTrueBiomeStream().get(xxx, zzz);
|
||||||
int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true);
|
int surfaceY = getEngineMantle().getEngine().getHeight(xxx, zzz, true);
|
||||||
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
|
IrisBiome caveBiome = resolveCaveObjectBiome(xxx, zzz, surfaceY, surfaceBiome);
|
||||||
if (traceRegen) {
|
if (traceRegen) {
|
||||||
@@ -82,7 +81,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
+ " regionSurfacePlacers=" + region.getSurfaceObjects().size()
|
+ " regionSurfacePlacers=" + region.getSurfaceObjects().size()
|
||||||
+ " regionCavePlacers=" + region.getCarvingObjects().size());
|
+ " regionCavePlacers=" + region.getCarvingObjects().size());
|
||||||
}
|
}
|
||||||
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, traceRegen);
|
ObjectPlacementSummary summary = placeObjects(writer, rng, x, z, surfaceBiome, caveBiome, region, complex, traceRegen);
|
||||||
if (traceRegen) {
|
if (traceRegen) {
|
||||||
Iris.info("Regen object layer done: chunk=" + x + "," + z
|
Iris.info("Regen object layer done: chunk=" + x + "," + z
|
||||||
+ " biomeSurfacePlacersChecked=" + summary.biomeSurfacePlacersChecked()
|
+ " biomeSurfacePlacersChecked=" + summary.biomeSurfacePlacersChecked()
|
||||||
@@ -142,7 +141,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ChunkCoordinates
|
@ChunkCoordinates
|
||||||
private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, boolean traceRegen) {
|
private ObjectPlacementSummary placeObjects(MantleWriter writer, RNG rng, int x, int z, IrisBiome surfaceBiome, IrisBiome caveBiome, IrisRegion region, IrisComplex complex, boolean traceRegen) {
|
||||||
int biomeSurfaceChecked = 0;
|
int biomeSurfaceChecked = 0;
|
||||||
int biomeSurfaceTriggered = 0;
|
int biomeSurfaceTriggered = 0;
|
||||||
int biomeCaveChecked = 0;
|
int biomeCaveChecked = 0;
|
||||||
@@ -175,7 +174,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
if (chance) {
|
if (chance) {
|
||||||
biomeSurfaceTriggered++;
|
biomeSurfaceTriggered++;
|
||||||
try {
|
try {
|
||||||
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, biomeSurfaceExclusionDepth, traceRegen, x, z, "biome-surface");
|
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, biomeSurfaceExclusionDepth, complex, traceRegen, x, z, "biome-surface");
|
||||||
attempts += result.attempts();
|
attempts += result.attempts();
|
||||||
placed += result.placed();
|
placed += result.placed();
|
||||||
rejected += result.rejected();
|
rejected += result.rejected();
|
||||||
@@ -209,7 +208,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
if (chance) {
|
if (chance) {
|
||||||
biomeCaveTriggered++;
|
biomeCaveTriggered++;
|
||||||
try {
|
try {
|
||||||
ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, biomeCaveProfile, traceRegen, x, z, "biome-cave");
|
ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, biomeCaveProfile, complex, traceRegen, x, z, "biome-cave");
|
||||||
attempts += result.attempts();
|
attempts += result.attempts();
|
||||||
placed += result.placed();
|
placed += result.placed();
|
||||||
rejected += result.rejected();
|
rejected += result.rejected();
|
||||||
@@ -240,7 +239,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
if (chance) {
|
if (chance) {
|
||||||
regionSurfaceTriggered++;
|
regionSurfaceTriggered++;
|
||||||
try {
|
try {
|
||||||
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, regionSurfaceExclusionDepth, traceRegen, x, z, "region-surface");
|
ObjectPlacementResult result = placeObject(writer, rng, x << 4, z << 4, i, regionSurfaceExclusionDepth, complex, traceRegen, x, z, "region-surface");
|
||||||
attempts += result.attempts();
|
attempts += result.attempts();
|
||||||
placed += result.placed();
|
placed += result.placed();
|
||||||
rejected += result.rejected();
|
rejected += result.rejected();
|
||||||
@@ -274,7 +273,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
if (chance) {
|
if (chance) {
|
||||||
regionCaveTriggered++;
|
regionCaveTriggered++;
|
||||||
try {
|
try {
|
||||||
ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, regionCaveProfile, traceRegen, x, z, "region-cave");
|
ObjectPlacementResult result = placeCaveObject(writer, rng, x, z, i, regionCaveProfile, complex, traceRegen, x, z, "region-cave");
|
||||||
attempts += result.attempts();
|
attempts += result.attempts();
|
||||||
placed += result.placed();
|
placed += result.placed();
|
||||||
rejected += result.rejected();
|
rejected += result.rejected();
|
||||||
@@ -316,6 +315,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
int z,
|
int z,
|
||||||
IrisObjectPlacement objectPlacement,
|
IrisObjectPlacement objectPlacement,
|
||||||
int surfaceObjectExclusionDepth,
|
int surfaceObjectExclusionDepth,
|
||||||
|
IrisComplex complex,
|
||||||
boolean traceRegen,
|
boolean traceRegen,
|
||||||
int chunkX,
|
int chunkX,
|
||||||
int chunkZ,
|
int chunkZ,
|
||||||
@@ -330,7 +330,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
for (int i = 0; i < density; i++) {
|
for (int i = 0; i < density; i++) {
|
||||||
attempts++;
|
attempts++;
|
||||||
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng));
|
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng));
|
||||||
if (v == null) {
|
if (v == null) {
|
||||||
nullObjects++;
|
nullObjects++;
|
||||||
if (traceRegen) {
|
if (traceRegen) {
|
||||||
@@ -398,6 +398,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
int chunkZ,
|
int chunkZ,
|
||||||
IrisObjectPlacement objectPlacement,
|
IrisObjectPlacement objectPlacement,
|
||||||
IrisCaveProfile caveProfile,
|
IrisCaveProfile caveProfile,
|
||||||
|
IrisComplex complex,
|
||||||
boolean traceRegen,
|
boolean traceRegen,
|
||||||
int metricChunkX,
|
int metricChunkX,
|
||||||
int metricChunkZ,
|
int metricChunkZ,
|
||||||
@@ -419,7 +420,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
|
|
||||||
for (int i = 0; i < density; i++) {
|
for (int i = 0; i < density; i++) {
|
||||||
attempts++;
|
attempts++;
|
||||||
IrisObject object = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng));
|
IrisObject object = objectPlacement.getScale().get(rng, objectPlacement.getObject(complex, rng));
|
||||||
if (object == null) {
|
if (object == null) {
|
||||||
nullObjects++;
|
nullObjects++;
|
||||||
if (traceRegen) {
|
if (traceRegen) {
|
||||||
@@ -903,15 +904,15 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected int computeRadius() {
|
protected int computeRadius() {
|
||||||
var dimension = getDimension();
|
IrisDimension dimension = getDimension();
|
||||||
|
|
||||||
AtomicInteger xg = new AtomicInteger();
|
AtomicInteger xg = new AtomicInteger();
|
||||||
AtomicInteger zg = new AtomicInteger();
|
AtomicInteger zg = new AtomicInteger();
|
||||||
|
|
||||||
KSet<String> objects = new KSet<>();
|
KSet<String> objects = new KSet<>();
|
||||||
KMap<IrisObjectScale, KList<String>> scalars = new KMap<>();
|
KMap<IrisObjectScale, KList<String>> scalars = new KMap<>();
|
||||||
for (var region : dimension.getAllRegions(this::getData)) {
|
for (IrisRegion region : dimension.getAllRegions(this::getData)) {
|
||||||
for (var j : region.getObjects()) {
|
for (IrisObjectPlacement j : region.getObjects()) {
|
||||||
if (j.getScale().canScaleBeyond()) {
|
if (j.getScale().canScaleBeyond()) {
|
||||||
scalars.put(j.getScale(), j.getPlace());
|
scalars.put(j.getScale(), j.getPlace());
|
||||||
} else {
|
} else {
|
||||||
@@ -919,8 +920,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (var biome : dimension.getAllBiomes(this::getData)) {
|
for (IrisBiome biome : dimension.getAllBiomes(this::getData)) {
|
||||||
for (var j : biome.getObjects()) {
|
for (IrisObjectPlacement j : biome.getObjects()) {
|
||||||
if (j.getScale().canScaleBeyond()) {
|
if (j.getScale().canScaleBeyond()) {
|
||||||
scalars.put(j.getScale(), j.getPlace());
|
scalars.put(j.getScale(), j.getPlace());
|
||||||
} else {
|
} else {
|
||||||
@@ -929,93 +930,59 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BurstExecutor e = getEngineMantle().getTarget().getBurster().burst(objects.size());
|
|
||||||
boolean maintenanceFolia = false;
|
|
||||||
if (J.isFolia()) {
|
|
||||||
var world = getEngineMantle().getEngine().getWorld().realWorld();
|
|
||||||
maintenanceFolia = world != null && IrisToolbelt.isWorldMaintenanceActive(world);
|
|
||||||
}
|
|
||||||
if (maintenanceFolia) {
|
|
||||||
Iris.info("MantleObjectComponent radius scan using single-threaded mode during maintenance regen.");
|
|
||||||
e.setMulticore(false);
|
|
||||||
}
|
|
||||||
KMap<String, BlockVector> sizeCache = new KMap<>();
|
KMap<String, BlockVector> sizeCache = new KMap<>();
|
||||||
for (String i : objects) {
|
for (String i : objects) {
|
||||||
e.queue(() -> {
|
updateRadiusBounds(sizeCache, xg, zg, i, 1D);
|
||||||
try {
|
|
||||||
BlockVector bv = sizeCache.computeIfAbsent(i, (k) -> {
|
|
||||||
try {
|
|
||||||
return IrisObject.sampleSize(getData().getObjectLoader().findFile(i));
|
|
||||||
} catch (IOException ex) {
|
|
||||||
Iris.reportError(ex);
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bv == null) {
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) {
|
|
||||||
Iris.warn("Object " + i + " has a large size (" + bv + ") and may increase memory usage!");
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (xg) {
|
|
||||||
xg.getAndSet(Math.max(bv.getBlockX(), xg.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (zg) {
|
|
||||||
zg.getAndSet(Math.max(bv.getBlockZ(), zg.get()));
|
|
||||||
}
|
|
||||||
} catch (Throwable ed) {
|
|
||||||
Iris.reportError(ed);
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Map.Entry<IrisObjectScale, KList<String>> entry : scalars.entrySet()) {
|
for (Map.Entry<IrisObjectScale, KList<String>> entry : scalars.entrySet()) {
|
||||||
double ms = entry.getKey().getMaximumScale();
|
double ms = entry.getKey().getMaximumScale();
|
||||||
for (String j : entry.getValue()) {
|
for (String j : entry.getValue()) {
|
||||||
e.queue(() -> {
|
updateRadiusBounds(sizeCache, xg, zg, j, ms);
|
||||||
try {
|
|
||||||
BlockVector bv = sizeCache.computeIfAbsent(j, (k) -> {
|
|
||||||
try {
|
|
||||||
return IrisObject.sampleSize(getData().getObjectLoader().findFile(j));
|
|
||||||
} catch (IOException ioException) {
|
|
||||||
Iris.reportError(ioException);
|
|
||||||
ioException.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bv == null) {
|
|
||||||
throw new RuntimeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) {
|
|
||||||
Iris.warn("Object " + j + " has a large size (" + bv + ") and may increase memory usage! (Object scaled up to " + Form.pc(ms, 2) + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (xg) {
|
|
||||||
xg.getAndSet((int) Math.max(Math.ceil(bv.getBlockX() * ms), xg.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (zg) {
|
|
||||||
zg.getAndSet((int) Math.max(Math.ceil(bv.getBlockZ() * ms), zg.get()));
|
|
||||||
}
|
|
||||||
} catch (Throwable ee) {
|
|
||||||
Iris.reportError(ee);
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.complete();
|
|
||||||
return Math.max(xg.get(), zg.get());
|
return Math.max(xg.get(), zg.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateRadiusBounds(
|
||||||
|
KMap<String, BlockVector> sizeCache,
|
||||||
|
AtomicInteger xg,
|
||||||
|
AtomicInteger zg,
|
||||||
|
String objectKey,
|
||||||
|
double scale
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
BlockVector bv = loadObjectSize(sizeCache, objectKey);
|
||||||
|
if (bv == null) {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.max(bv.getBlockX(), bv.getBlockZ()) > 128) {
|
||||||
|
if (scale > 1D) {
|
||||||
|
Iris.warn("Object " + objectKey + " has a large size (" + bv + ") and may increase memory usage! (Object scaled up to " + Form.pc(scale, 2) + ")");
|
||||||
|
} else {
|
||||||
|
Iris.warn("Object " + objectKey + " has a large size (" + bv + ") and may increase memory usage!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xg.getAndSet(Math.max((int) Math.ceil(bv.getBlockX() * scale), xg.get()));
|
||||||
|
zg.getAndSet(Math.max((int) Math.ceil(bv.getBlockZ() * scale), zg.get()));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlockVector loadObjectSize(KMap<String, BlockVector> sizeCache, String objectKey) {
|
||||||
|
return sizeCache.computeIfAbsent(objectKey, k -> {
|
||||||
|
try {
|
||||||
|
return IrisObject.sampleSize(getData().getObjectLoader().findFile(objectKey));
|
||||||
|
} catch (IOException e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import art.arcane.iris.util.common.data.B;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
final class BlockDataMergeSupport {
|
||||||
|
private BlockDataMergeSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static BlockData merge(BlockData base, BlockData update) {
|
||||||
|
return merge(base, update, B::get);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BlockData merge(BlockData base, BlockData update, Function<String, BlockData> resolver) {
|
||||||
|
try {
|
||||||
|
return base.merge(update);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
BlockData normalizedBase = resolve(base, resolver);
|
||||||
|
BlockData normalizedUpdate = resolve(update, resolver);
|
||||||
|
|
||||||
|
if (normalizedBase != null && normalizedUpdate != null) {
|
||||||
|
try {
|
||||||
|
return normalizedBase.merge(normalizedUpdate);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return normalizedUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedUpdate != null) {
|
||||||
|
return normalizedUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockData resolve(BlockData data, Function<String, BlockData> resolver) {
|
||||||
|
if (data == null || resolver == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String serialized = data.getAsString(false);
|
||||||
|
if (serialized == null || serialized.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolver.apply(serialized);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -564,33 +564,52 @@ public class IrisDimension extends IrisRegistrant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dimension getBaseDimension() {
|
public Dimension getBaseDimension() {
|
||||||
return switch (getEnvironment()) {
|
return switch (getEnvironment()) {
|
||||||
case NETHER -> Dimension.NETHER;
|
case NETHER -> Dimension.NETHER;
|
||||||
case THE_END -> Dimension.END;
|
case THE_END -> Dimension.END;
|
||||||
default -> Dimension.OVERWORLD;
|
default -> Dimension.OVERWORLD;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDimensionTypeKey() {
|
public String getDimensionTypeKey() {
|
||||||
return getDimensionType().key();
|
return sanitizeDimensionTypeKeyValue(getLoadKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimensionType getDimensionType() {
|
public static String sanitizeDimensionTypeKeyValue(String value) {
|
||||||
return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
|
if (value == null || value.isBlank()) {
|
||||||
}
|
return "dimension";
|
||||||
|
}
|
||||||
public void installDimensionType(IDataFixer fixer, KList<File> folders) {
|
|
||||||
IrisDimensionType type = getDimensionType();
|
String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/");
|
||||||
String json = type.toJson(fixer);
|
sanitized = sanitized.replaceAll("[^a-z0-9_\\-./]", "_");
|
||||||
|
sanitized = sanitized.replaceAll("/+", "/");
|
||||||
Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + type.key() + '"');
|
sanitized = sanitized.replaceAll("^/+", "");
|
||||||
for (File datapacks : folders) {
|
sanitized = sanitized.replaceAll("/+$", "");
|
||||||
File output = new File(datapacks, "iris/data/iris/dimension_type/" + type.key() + ".json");
|
if (sanitized.contains("..")) {
|
||||||
output.getParentFile().mkdirs();
|
sanitized = sanitized.replace("..", "_");
|
||||||
try {
|
}
|
||||||
IO.writeAll(output, json);
|
|
||||||
} catch (IOException e) {
|
sanitized = sanitized.replace("/", "_");
|
||||||
|
return sanitized.isBlank() ? "dimension" : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisDimensionType getDimensionType() {
|
||||||
|
return new IrisDimensionType(getBaseDimension(), getDimensionOptions(), getLogicalHeight(), getMaxHeight() - getMinHeight(), getMinHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void installDimensionType(IDataFixer fixer, KList<File> folders) {
|
||||||
|
IrisDimensionType type = getDimensionType();
|
||||||
|
String json = type.toJson(fixer);
|
||||||
|
String dimensionTypeKey = getDimensionTypeKey();
|
||||||
|
|
||||||
|
Iris.verbose(" Installing Data Pack Dimension Type: \"iris:" + dimensionTypeKey + '"');
|
||||||
|
for (File datapacks : folders) {
|
||||||
|
File output = new File(datapacks, "iris/data/iris/dimension_type/" + dimensionTypeKey + ".json");
|
||||||
|
output.getParentFile().mkdirs();
|
||||||
|
try {
|
||||||
|
IO.writeAll(output, json);
|
||||||
|
} catch (IOException e) {
|
||||||
Iris.reportError(e);
|
Iris.reportError(e);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -969,7 +969,7 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
BlockData newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata).clone();
|
BlockData newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata).clone();
|
||||||
|
|
||||||
if (newData.getMaterial() == data.getMaterial() && !(newData instanceof IrisCustomData || data instanceof IrisCustomData))
|
if (newData.getMaterial() == data.getMaterial() && !(newData instanceof IrisCustomData || data instanceof IrisCustomData))
|
||||||
data = data.merge(newData);
|
data = BlockDataMergeSupport.merge(data, newData);
|
||||||
else
|
else
|
||||||
data = newData;
|
data = newData;
|
||||||
|
|
||||||
@@ -1093,7 +1093,7 @@ public class IrisObject extends IrisRegistrant {
|
|||||||
BlockData newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata).clone();
|
BlockData newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata).clone();
|
||||||
|
|
||||||
if (newData.getMaterial() == d.getMaterial()) {
|
if (newData.getMaterial() == d.getMaterial()) {
|
||||||
d = d.merge(newData);
|
d = BlockDataMergeSupport.merge(d, newData);
|
||||||
} else {
|
} else {
|
||||||
d = newData;
|
d = newData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import art.arcane.iris.engine.data.cache.AtomicCache;
|
|||||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
import art.arcane.iris.engine.object.StudioMode;
|
import art.arcane.iris.engine.object.StudioMode;
|
||||||
@@ -93,10 +94,12 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
private final AtomicInteger a = new AtomicInteger(0);
|
private final AtomicInteger a = new AtomicInteger(0);
|
||||||
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
|
private final CompletableFuture<Integer> spawnChunks = new CompletableFuture<>();
|
||||||
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
|
private final AtomicCache<EngineTarget> targetCache = new AtomicCache<>();
|
||||||
|
private final AtomicReference<CompletableFuture<Void>> closeFuture = new AtomicReference<>();
|
||||||
private volatile Engine engine;
|
private volatile Engine engine;
|
||||||
private volatile Looper hotloader;
|
private volatile Looper hotloader;
|
||||||
private volatile StudioMode lastMode;
|
private volatile StudioMode lastMode;
|
||||||
private volatile DummyBiomeProvider dummyBiomeProvider;
|
private volatile DummyBiomeProvider dummyBiomeProvider;
|
||||||
|
private volatile boolean closing;
|
||||||
@Setter
|
@Setter
|
||||||
private volatile StudioGenerator studioGenerator;
|
private volatile StudioGenerator studioGenerator;
|
||||||
|
|
||||||
@@ -118,6 +121,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
new KList<>(".iris"),
|
new KList<>(".iris"),
|
||||||
new KList<>()
|
new KList<>()
|
||||||
);
|
);
|
||||||
|
this.closing = false;
|
||||||
Bukkit.getServer().getPluginManager().registerEvents(this, Iris.instance);
|
Bukkit.getServer().getPluginManager().registerEvents(this, Iris.instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +146,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
try {
|
try {
|
||||||
INMS.get().inject(world.getSeed(), engine, world);
|
INMS.get().inject(world.getSeed(), engine, world);
|
||||||
Iris.info("Injected Iris Biome Source into " + world.getName());
|
Iris.info("Injected Iris Biome Source into " + world.getName());
|
||||||
|
J.s(() -> updateSpawnLocation(world), 1);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError(e);
|
Iris.reportError(e);
|
||||||
Iris.error("Failed to inject biome source into " + world.getName());
|
Iris.error("Failed to inject biome source into " + world.getName());
|
||||||
@@ -156,15 +161,65 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public Location getFixedSpawnLocation(@NotNull World world, @NotNull Random random) {
|
public Location getFixedSpawnLocation(@NotNull World world, @NotNull Random random) {
|
||||||
Location location = new Location(world, 0, 64, 0);
|
return getInitialSpawnLocation(world);
|
||||||
PaperLib.getChunkAtAsync(location)
|
}
|
||||||
.thenAccept(c -> {
|
|
||||||
World w = c.getWorld();
|
@Override
|
||||||
if (!w.getSpawnLocation().equals(location))
|
public Location getInitialSpawnLocation(World world) {
|
||||||
return;
|
int minY = world.getMinHeight() + 1;
|
||||||
w.setSpawnLocation(location.add(0, w.getHighestBlockYAt(location) - 64, 0));
|
int maxY = world.getMaxHeight() - 2;
|
||||||
});
|
int y = Math.max(minY, Math.min(maxY, 96));
|
||||||
return location;
|
return new Location(world, 0.5D, y, 0.5D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSpawnLocation(World world) {
|
||||||
|
Location initialSpawn = getInitialSpawnLocation(world);
|
||||||
|
int chunkX = initialSpawn.getBlockX() >> 4;
|
||||||
|
int chunkZ = initialSpawn.getBlockZ() >> 4;
|
||||||
|
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
|
||||||
|
if (chunkFuture == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkFuture.thenAccept(chunk ->
|
||||||
|
J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> applySpawnLocation(chunk.getWorld(), initialSpawn)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySpawnLocation(World world, Location initialSpawn) {
|
||||||
|
Location currentSpawn = world.getSpawnLocation();
|
||||||
|
if (currentSpawn == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!studio && (currentSpawn.getBlockX() != initialSpawn.getBlockX() || currentSpawn.getBlockZ() != initialSpawn.getBlockZ())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int minY = world.getMinHeight() + 1;
|
||||||
|
int maxY = world.getMaxHeight() - 2;
|
||||||
|
int y = Math.max(minY, Math.min(maxY, world.getHighestBlockYAt(initialSpawn)));
|
||||||
|
world.setSpawnLocation(new Location(world, initialSpawn.getX(), y, initialSpawn.getZ(), initialSpawn.getYaw(), initialSpawn.getPitch()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private CompletableFuture<Chunk> requestChunkAsync(World world, int chunkX, int chunkZ, boolean generate) {
|
||||||
|
try {
|
||||||
|
Object result = World.class
|
||||||
|
.getMethod("getChunkAtAsync", int.class, int.class, boolean.class)
|
||||||
|
.invoke(world, chunkX, chunkZ, generate);
|
||||||
|
if (result instanceof CompletableFuture<?>) {
|
||||||
|
return (CompletableFuture<Chunk>) result;
|
||||||
|
}
|
||||||
|
if (PaperLib.isPaper()) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Paper World#getChunkAtAsync returned a non-future result."));
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
if (PaperLib.isPaper()) {
|
||||||
|
return CompletableFuture.failedFuture(new IllegalStateException("Paper World#getChunkAtAsync is unavailable.", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaperLib.getChunkAtAsync(world, chunkX, chunkZ, generate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupEngine() {
|
private void setupEngine() {
|
||||||
@@ -530,18 +585,48 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
withExclusiveControl(() -> {
|
closeAsync();
|
||||||
if (isStudio()) {
|
}
|
||||||
hotloader.interrupt();
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> closeAsync() {
|
||||||
|
CompletableFuture<Void> existing = closeFuture.get();
|
||||||
|
if (existing != null && !existing.isDone()) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
closing = true;
|
||||||
|
CompletableFuture<Void> future = withExclusiveControlFuture(() -> {
|
||||||
|
Looper activeHotloader = hotloader;
|
||||||
|
hotloader = null;
|
||||||
|
if (isStudio() && activeHotloader != null) {
|
||||||
|
activeHotloader.interrupt();
|
||||||
|
try {
|
||||||
|
activeHotloader.join(1000L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
Iris.reportError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final Engine engine = getEngine();
|
Engine currentEngine = engine;
|
||||||
if (engine != null && !engine.isClosed())
|
if (currentEngine != null && !currentEngine.isClosed()) {
|
||||||
engine.close();
|
currentEngine.close();
|
||||||
|
}
|
||||||
folder.clear();
|
folder.clear();
|
||||||
populators.clear();
|
populators.clear();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
if (!closeFuture.compareAndSet(existing, future)) {
|
||||||
|
CompletableFuture<Void> winningFuture = closeFuture.get();
|
||||||
|
return winningFuture == null ? future : winningFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
future.whenComplete((ignored, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
closeFuture.compareAndSet(future, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -551,7 +636,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hotload() {
|
public void hotload() {
|
||||||
if (!isStudio()) {
|
if (!isStudio() || closing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,6 +655,22 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Void> withExclusiveControlFuture(Runnable r) {
|
||||||
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
J.a(() -> {
|
||||||
|
try {
|
||||||
|
loadLock.acquire(LOAD_LOCKS);
|
||||||
|
r.run();
|
||||||
|
future.complete(null);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
} finally {
|
||||||
|
loadLock.release(LOAD_LOCKS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void touch(World world) {
|
public void touch(World world) {
|
||||||
getEngine(world);
|
getEngine(world);
|
||||||
@@ -577,6 +678,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) {
|
public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) {
|
||||||
|
if (closing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Engine engine = getEngine(world);
|
Engine engine = getEngine(world);
|
||||||
computeStudioGenerator();
|
computeStudioGenerator();
|
||||||
@@ -592,6 +697,21 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
}
|
}
|
||||||
|
|
||||||
Iris.debug("Generated " + x + " " + z);
|
Iris.debug("Generated " + x + " " + z);
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
if (closing || isExpectedTeardown(engine, e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Iris.error("======================================");
|
||||||
|
e.printStackTrace();
|
||||||
|
Iris.reportErrorChunk(x, z, e, "CHUNK");
|
||||||
|
Iris.error("======================================");
|
||||||
|
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
for (int j = 0; j < 16; j++) {
|
||||||
|
d.setBlock(i, 0, j, Material.RED_GLAZED_TERRACOTTA.createBlockData());
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.error("======================================");
|
Iris.error("======================================");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -606,6 +726,19 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isExpectedTeardown(Engine currentEngine, Throwable throwable) {
|
||||||
|
if (throwable instanceof GenerationSessionException generationSessionException && generationSessionException.isExpectedTeardown()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentEngine != null && currentEngine.isClosing()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
World realWorld = this.world.realWorld();
|
||||||
|
return realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
|
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
|
||||||
Engine currentEngine = engine;
|
Engine currentEngine = engine;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import art.arcane.iris.engine.framework.Engine;
|
|||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
import art.arcane.iris.engine.framework.Hotloadable;
|
import art.arcane.iris.engine.framework.Hotloadable;
|
||||||
import art.arcane.iris.util.common.data.DataProvider;
|
import art.arcane.iris.util.common.data.DataProvider;
|
||||||
|
import org.bukkit.Location;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@@ -53,9 +54,21 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
|
|||||||
|
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
|
default CompletableFuture<Void> closeAsync() {
|
||||||
|
close();
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
boolean isStudio();
|
boolean isStudio();
|
||||||
|
|
||||||
|
default boolean isClosing() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void touch(World world);
|
void touch(World world);
|
||||||
|
|
||||||
CompletableFuture<Integer> getSpawnChunks();
|
CompletableFuture<Integer> getSpawnChunks();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
Location getInitialSpawnLocation(World world);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,6 +431,10 @@ public class VolmitSender implements CommandSender {
|
|||||||
return m.removeDuplicates().convert((iff) -> iff.replaceAll("\\Q \\E", " ")).toString("\n");
|
return m.removeDuplicates().convert((iff) -> iff.replaceAll("\\Q \\E", " ")).toString("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String escapeMiniMessageQuotedText(String text) {
|
||||||
|
return text.replace("\\", "\\\\").replace("'", "\\'");
|
||||||
|
}
|
||||||
|
|
||||||
public void sendHeader(String name, int overrideLength) {
|
public void sendHeader(String name, int overrideLength) {
|
||||||
int len = overrideLength;
|
int len = overrideLength;
|
||||||
int h = name.length() + 2;
|
int h = name.length() + 2;
|
||||||
@@ -469,7 +473,8 @@ public class VolmitSender implements CommandSender {
|
|||||||
if (v.getNodes().isNotEmpty()) {
|
if (v.getNodes().isNotEmpty()) {
|
||||||
sendHeader(v.getPath() + (page > 0 ? (" {" + (page + 1) + "}") : ""));
|
sendHeader(v.getPath() + (page > 0 ? (" {" + (page + 1) + "}") : ""));
|
||||||
if (isPlayer() && v.getParent() != null) {
|
if (isPlayer() && v.getParent() != null) {
|
||||||
sendMessageRaw("<hover:show_text:'" + "<#2b7a3f>Click to go back to <#32bfad>" + Form.capitalize(v.getParent().getName()) + " Help" + "'><click:run_command:" + v.getParent().getPath() + "><font:minecraft:uniform><#6fe98f>〈 Back</click></hover>");
|
String backHover = escapeMiniMessageQuotedText("<#2b7a3f>Click to go back to <#32bfad>" + Form.capitalize(v.getParent().getName()) + " Help");
|
||||||
|
sendMessageRaw("<hover:show_text:'" + backHover + "'><click:run_command:" + v.getParent().getPath() + "><font:minecraft:uniform><#6fe98f>〈 Back</click></hover>");
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomicBoolean next = new AtomicBoolean(false);
|
AtomicBoolean next = new AtomicBoolean(false);
|
||||||
@@ -481,13 +486,15 @@ public class VolmitSender implements CommandSender {
|
|||||||
int l = 75 - (page > 0 ? 10 : 0) - (next.get() ? 10 : 0);
|
int l = 75 - (page > 0 ? 10 : 0) - (next.get() ? 10 : 0);
|
||||||
|
|
||||||
if (page > 0) {
|
if (page > 0) {
|
||||||
s += "<hover:show_text:'<green>Click to go back to page " + page + "'><click:run_command:" + v.getPath() + " help=" + page + "><gradient:#34eb6b:#1f8f4d>〈 Page " + page + "</click></hover><reset> ";
|
String previousPageHover = escapeMiniMessageQuotedText("<green>Click to go back to page " + page);
|
||||||
|
s += "<hover:show_text:'" + previousPageHover + "'><click:run_command:" + v.getPath() + " help=" + page + "><gradient:#34eb6b:#1f8f4d>〈 Page " + page + "</click></hover><reset> ";
|
||||||
}
|
}
|
||||||
|
|
||||||
s += "<reset><font:minecraft:uniform><strikethrough><gradient:#32bfad:#34eb6b>" + Form.repeat(" ", l) + "<reset>";
|
s += "<reset><font:minecraft:uniform><strikethrough><gradient:#32bfad:#34eb6b>" + Form.repeat(" ", l) + "<reset>";
|
||||||
|
|
||||||
if (next.get()) {
|
if (next.get()) {
|
||||||
s += " <hover:show_text:'<green>Click to go to back to page " + (page + 2) + "'><click:run_command:" + v.getPath() + " help=" + (page + 2) + "><gradient:#1f8f4d:#34eb6b>Page " + (page + 2) + " ❭</click></hover>";
|
String nextPageHover = escapeMiniMessageQuotedText("<green>Click to go to back to page " + (page + 2));
|
||||||
|
s += " <hover:show_text:'" + nextPageHover + "'><click:run_command:" + v.getPath() + " help=" + (page + 2) + "><gradient:#1f8f4d:#34eb6b>Page " + (page + 2) + " ❭</click></hover>";
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessageRaw(s);
|
sendMessageRaw(s);
|
||||||
@@ -550,13 +557,11 @@ public class VolmitSender implements CommandSender {
|
|||||||
nUsage = "<#3fbe6f>✔ <#9de5b6><font:minecraft:uniform>This parameter is optional.";
|
nUsage = "<#3fbe6f>✔ <#9de5b6><font:minecraft:uniform>This parameter is optional.";
|
||||||
}
|
}
|
||||||
String type = "<#4fbf7f>✢ <#8ad9af><font:minecraft:uniform>This parameter is of type " + p.getType().getSimpleName() + ".";
|
String type = "<#4fbf7f>✢ <#8ad9af><font:minecraft:uniform>This parameter is of type " + p.getType().getSimpleName() + ".";
|
||||||
|
String parameterHover = escapeMiniMessageQuotedText(nHoverTitle + newline + nDescription + newline + nUsage + newline + type);
|
||||||
|
|
||||||
nodes
|
nodes
|
||||||
.append("<hover:show_text:'")
|
.append("<hover:show_text:'")
|
||||||
.append(nHoverTitle).append(newline)
|
.append(parameterHover)
|
||||||
.append(nDescription).append(newline)
|
|
||||||
.append(nUsage).append(newline)
|
|
||||||
.append(type)
|
|
||||||
.append("'>")
|
.append("'>")
|
||||||
.append(fullTitle)
|
.append(fullTitle)
|
||||||
.append("</hover>");
|
.append("</hover>");
|
||||||
@@ -565,12 +570,16 @@ public class VolmitSender implements CommandSender {
|
|||||||
nodes = new StringBuilder("<gradient:#b7eecb:#9de5b6> - Category of Commands");
|
nodes = new StringBuilder("<gradient:#b7eecb:#9de5b6> - Category of Commands");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "<hover:show_text:'" +
|
String entryHover = escapeMiniMessageQuotedText(
|
||||||
hoverTitle + newline +
|
hoverTitle + newline +
|
||||||
description + newline +
|
description + newline +
|
||||||
usage +
|
usage +
|
||||||
suggestion +
|
suggestion +
|
||||||
suggestions +
|
suggestions
|
||||||
|
);
|
||||||
|
|
||||||
|
return "<hover:show_text:'" +
|
||||||
|
entryHover +
|
||||||
"'>" +
|
"'>" +
|
||||||
"<click:" +
|
"<click:" +
|
||||||
onClick +
|
onClick +
|
||||||
|
|||||||
@@ -617,18 +617,38 @@ public class J {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean runAsyncImmediate(Runnable runnable) {
|
private static boolean runAsyncImmediate(Runnable runnable) {
|
||||||
if (!isFolia()) {
|
if (!isPluginEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isFolia()) {
|
||||||
|
try {
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(Iris.instance, runnable);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return FoliaScheduler.runAsync(Iris.instance, runnable);
|
return FoliaScheduler.runAsync(Iris.instance, runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean runAsyncDelayed(Runnable runnable, int delayTicks) {
|
private static boolean runAsyncDelayed(Runnable runnable, int delayTicks) {
|
||||||
if (!isFolia()) {
|
if (!isPluginEnabled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isFolia()) {
|
||||||
|
try {
|
||||||
|
Bukkit.getScheduler().runTaskLaterAsynchronously(Iris.instance, runnable, Math.max(0, delayTicks));
|
||||||
|
return true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Iris.reportError(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return FoliaScheduler.runAsync(Iris.instance, runnable, Math.max(0, delayTicks));
|
return FoliaScheduler.runAsync(Iris.instance, runnable, Math.max(0, delayTicks));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
public class ChunkContext {
|
public class ChunkContext {
|
||||||
private final int x;
|
private final int x;
|
||||||
private final int z;
|
private final int z;
|
||||||
|
private final IrisComplex complex;
|
||||||
|
private final long generationSessionId;
|
||||||
private final ChunkedDataCache<Double> height;
|
private final ChunkedDataCache<Double> height;
|
||||||
private final ChunkedDataCache<IrisBiome> biome;
|
private final ChunkedDataCache<IrisBiome> biome;
|
||||||
private final ChunkedDataCache<IrisBiome> cave;
|
private final ChunkedDataCache<IrisBiome> cave;
|
||||||
@@ -23,20 +25,26 @@ public class ChunkContext {
|
|||||||
private final ChunkedDataCache<IrisRegion> region;
|
private final ChunkedDataCache<IrisRegion> region;
|
||||||
|
|
||||||
public ChunkContext(int x, int z, IrisComplex complex) {
|
public ChunkContext(int x, int z, IrisComplex complex) {
|
||||||
this(x, z, complex, true, PrefillPlan.NO_CAVE, null);
|
this(x, z, complex, 0L, true, PrefillPlan.NO_CAVE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChunkContext(int x, int z, IrisComplex complex, boolean cache) {
|
public ChunkContext(int x, int z, IrisComplex complex, boolean cache) {
|
||||||
this(x, z, complex, cache, PrefillPlan.NO_CAVE, null);
|
this(x, z, complex, 0L, cache, PrefillPlan.NO_CAVE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChunkContext(int x, int z, IrisComplex complex, boolean cache, EngineMetrics metrics) {
|
public ChunkContext(int x, int z, IrisComplex complex, boolean cache, EngineMetrics metrics) {
|
||||||
this(x, z, complex, cache, PrefillPlan.NO_CAVE, metrics);
|
this(x, z, complex, 0L, cache, PrefillPlan.NO_CAVE, metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChunkContext(int x, int z, IrisComplex complex, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) {
|
public ChunkContext(int x, int z, IrisComplex complex, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) {
|
||||||
|
this(x, z, complex, 0L, cache, prefillPlan, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChunkContext(int x, int z, IrisComplex complex, long generationSessionId, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.z = z;
|
this.z = z;
|
||||||
|
this.complex = complex;
|
||||||
|
this.generationSessionId = generationSessionId;
|
||||||
this.height = new ChunkedDataCache<>(complex.getHeightStream(), x, z, cache);
|
this.height = new ChunkedDataCache<>(complex.getHeightStream(), x, z, cache);
|
||||||
this.biome = new ChunkedDataCache<>(complex.getTrueBiomeStream(), x, z, cache);
|
this.biome = new ChunkedDataCache<>(complex.getTrueBiomeStream(), x, z, cache);
|
||||||
this.cave = new ChunkedDataCache<>(complex.getCaveBiomeStream(), x, z, cache);
|
this.cave = new ChunkedDataCache<>(complex.getCaveBiomeStream(), x, z, cache);
|
||||||
@@ -68,7 +76,7 @@ public class ChunkContext {
|
|||||||
fillTasks.add(new PrefillFillTask(cave));
|
fillTasks.add(new PrefillFillTask(cave));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fillTasks.size() <= 1 || Iris.instance == null) {
|
if (!shouldPrefillAsync(fillTasks.size())) {
|
||||||
for (PrefillFillTask fillTask : fillTasks) {
|
for (PrefillFillTask fillTask : fillTasks) {
|
||||||
fillTask.run();
|
fillTask.run();
|
||||||
}
|
}
|
||||||
@@ -88,6 +96,15 @@ public class ChunkContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean shouldPrefillAsync(int fillTaskCount) {
|
||||||
|
if (fillTaskCount <= 1 || Iris.instance == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String threadName = Thread.currentThread().getName();
|
||||||
|
return threadName != null && threadName.startsWith("Iris ");
|
||||||
|
}
|
||||||
|
|
||||||
public int getX() {
|
public int getX() {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
@@ -96,6 +113,10 @@ public class ChunkContext {
|
|||||||
return z;
|
return z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IrisComplex getComplex() {
|
||||||
|
return complex;
|
||||||
|
}
|
||||||
|
|
||||||
public ChunkedDataCache<Double> getHeight() {
|
public ChunkedDataCache<Double> getHeight() {
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public class IrisContext {
|
|||||||
private static final ChronoLatch cl = new ChronoLatch(60000);
|
private static final ChronoLatch cl = new ChronoLatch(60000);
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private ChunkContext chunkContext;
|
private ChunkContext chunkContext;
|
||||||
|
private long generationSessionId;
|
||||||
|
|
||||||
public IrisContext(Engine engine) {
|
public IrisContext(Engine engine) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
@@ -100,6 +101,7 @@ public class IrisContext {
|
|||||||
return new KMap<String, Object>()
|
return new KMap<String, Object>()
|
||||||
.qput("studio", engine.isStudio())
|
.qput("studio", engine.isStudio())
|
||||||
.qput("closed", engine.isClosed())
|
.qput("closed", engine.isClosed())
|
||||||
|
.qput("generationSessionId", generationSessionId)
|
||||||
.qput("pack", new KMap<>()
|
.qput("pack", new KMap<>()
|
||||||
.qput("key", dimension == null ? "" : dimension.getLoadKey())
|
.qput("key", dimension == null ? "" : dimension.getLoadKey())
|
||||||
.qput("version", dimension == null ? "" : dimension.getVersion())
|
.qput("version", dimension == null ? "" : dimension.getVersion())
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package art.arcane.iris;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class IrisDiagnosticsTest {
|
||||||
|
@Test
|
||||||
|
public void reportErrorWithContextPrintsFullStacktrace() {
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
PrintStream originalErr = System.err;
|
||||||
|
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||||
|
try {
|
||||||
|
Iris.reportError("Runtime world creation failed.", new IllegalStateException("outer", new IllegalArgumentException("inner")));
|
||||||
|
} finally {
|
||||||
|
System.setErr(originalErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = output.toString(StandardCharsets.UTF_8);
|
||||||
|
assertTrue(text.contains("Runtime world creation failed."));
|
||||||
|
assertTrue(text.contains("IllegalStateException"));
|
||||||
|
assertTrue(text.contains("IllegalArgumentException"));
|
||||||
|
assertTrue(text.contains("inner"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collectSplashPacksSkipsInternalAndInvalidFolders() throws Exception {
|
||||||
|
Path root = Files.createTempDirectory("iris-splash");
|
||||||
|
try {
|
||||||
|
Path validPack = root.resolve("overworld");
|
||||||
|
Files.createDirectories(validPack.resolve("dimensions"));
|
||||||
|
Files.writeString(validPack.resolve("dimensions").resolve("overworld.json"), "{\"version\":\"4000\"}");
|
||||||
|
|
||||||
|
Files.createDirectories(root.resolve("datapack-imports"));
|
||||||
|
|
||||||
|
Path brokenPack = root.resolve("broken");
|
||||||
|
Files.createDirectories(brokenPack.resolve("dimensions"));
|
||||||
|
Files.writeString(brokenPack.resolve("dimensions").resolve("broken.json"), "{");
|
||||||
|
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
PrintStream originalErr = System.err;
|
||||||
|
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||||
|
List<Iris.SplashPackMetadata> packs;
|
||||||
|
try {
|
||||||
|
packs = Iris.collectSplashPacks(root.toFile());
|
||||||
|
} finally {
|
||||||
|
System.setErr(originalErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(1, packs.size());
|
||||||
|
assertEquals("overworld", packs.get(0).name());
|
||||||
|
assertEquals("4000", packs.get(0).version());
|
||||||
|
|
||||||
|
String text = output.toString(StandardCharsets.UTF_8);
|
||||||
|
assertTrue(text.contains("Failed to read splash metadata for dimension pack \"broken\"."));
|
||||||
|
assertTrue(text.contains("Json"));
|
||||||
|
} finally {
|
||||||
|
Files.walk(root)
|
||||||
|
.sorted(Comparator.reverseOrder())
|
||||||
|
.map(Path::toFile)
|
||||||
|
.forEach(File::delete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,16 @@ public class IrisRuntimeSchedulerModeRoutingTest {
|
|||||||
assertEquals(IrisRuntimeSchedulerMode.FOLIA, resolved);
|
assertEquals(IrisRuntimeSchedulerMode.FOLIA, resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void autoResolvesToPaperLikeOnCanvasBranding() {
|
||||||
|
installServer("Canvas", "git-Canvas-101 (MC: 1.21.11)");
|
||||||
|
IrisSettings.IrisSettingsPregen pregen = new IrisSettings.IrisSettingsPregen();
|
||||||
|
pregen.runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO;
|
||||||
|
|
||||||
|
IrisRuntimeSchedulerMode resolved = IrisRuntimeSchedulerMode.resolve(pregen);
|
||||||
|
assertEquals(IrisRuntimeSchedulerMode.PAPER_LIKE, resolved);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void explicitModeBypassesAutoDetection() {
|
public void explicitModeBypassesAutoDetection() {
|
||||||
installServer("Purpur", "git-Purpur-2562 (MC: 1.21.11)");
|
installServer("Purpur", "git-Purpur-2562 (MC: 1.21.11)");
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package art.arcane.iris.core;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ServerConfiguratorDatapackFolderTest {
|
||||||
|
@Test
|
||||||
|
public void resolvesDimensionWorldFolderBackToRootDatapacks() {
|
||||||
|
File folder = new File("/tmp/server/world/dimensions/minecraft/overworld");
|
||||||
|
File datapacks = ServerConfigurator.resolveDatapacksFolder(folder);
|
||||||
|
assertEquals(new File("/tmp/server/world/datapacks").getAbsolutePath(), datapacks.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void keepsStandaloneWorldFolderDatapacksUnchanged() {
|
||||||
|
File folder = new File("/tmp/server/custom_world");
|
||||||
|
File datapacks = ServerConfigurator.resolveDatapacksFolder(folder);
|
||||||
|
assertEquals(new File("/tmp/server/custom_world/datapacks").getAbsolutePath(), datapacks.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void installFoldersIncludeExtraStudioWorldDatapackTargets() {
|
||||||
|
File baseFolder = new File("/tmp/server/world/datapacks");
|
||||||
|
File extraFolder = new File("/tmp/server/iris-studio/datapacks");
|
||||||
|
KList<File> baseFolders = new KList<>();
|
||||||
|
baseFolders.add(baseFolder);
|
||||||
|
KList<File> extraFolders = new KList<>();
|
||||||
|
extraFolders.add(extraFolder);
|
||||||
|
KMap<String, KList<File>> extrasByPack = new KMap<>();
|
||||||
|
extrasByPack.put("overworld", extraFolders);
|
||||||
|
|
||||||
|
KList<File> folders = ServerConfigurator.collectInstallDatapackFolders(baseFolders, extrasByPack);
|
||||||
|
|
||||||
|
assertEquals(2, folders.size());
|
||||||
|
assertTrue(folders.contains(baseFolder));
|
||||||
|
assertTrue(folders.contains(extraFolder));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package art.arcane.iris.core;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.commands.CommandStudio;
|
||||||
|
import art.arcane.iris.core.tools.IrisCreator;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
|
||||||
|
public class StudioRuntimeCleanupTest {
|
||||||
|
@Test
|
||||||
|
public void pregenSettingsNoLongerExposeStartupNoisemapPrebake() {
|
||||||
|
boolean found = Arrays.stream(IrisSettings.IrisSettingsPregen.class.getDeclaredFields())
|
||||||
|
.anyMatch(field -> field.getName().equals("startupNoisemapPrebake"));
|
||||||
|
|
||||||
|
assertFalse(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void studioCommandNoLongerExposesProfilecache() {
|
||||||
|
boolean found = Arrays.stream(CommandStudio.class.getDeclaredMethods())
|
||||||
|
.anyMatch(method -> method.getName().equals("profilecache"));
|
||||||
|
|
||||||
|
assertFalse(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void studioCreatorNoLongerContainsPrewarmOrPrebakeHelpers() {
|
||||||
|
boolean found = Arrays.stream(IrisCreator.class.getDeclaredMethods())
|
||||||
|
.map(method -> method.getName().toLowerCase())
|
||||||
|
.anyMatch(name -> name.contains("prewarm") || name.contains("prebake"));
|
||||||
|
|
||||||
|
assertFalse(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noisemapPrebakePipelineClassIsRemoved() {
|
||||||
|
try {
|
||||||
|
Class.forName("art.arcane.iris.engine.IrisNoisemapPrebakePipeline");
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AssertionError("IrisNoisemapPrebakePipeline should not exist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class CapabilityResolutionTest {
|
||||||
|
@Test
|
||||||
|
public void resolvesExactCreateLevelMethod() throws Exception {
|
||||||
|
Method method = CapabilityResolution.resolveCreateLevelMethod(CurrentCreateLevelOwner.class);
|
||||||
|
|
||||||
|
assertEquals("createLevel", method.getName());
|
||||||
|
assertEquals(3, method.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesDeclaredLegacyCreateLevelMethod() throws Exception {
|
||||||
|
Method method = CapabilityResolution.resolveCreateLevelMethod(DeclaredLegacyCreateLevelOwner.class);
|
||||||
|
|
||||||
|
assertEquals("createLevel", method.getName());
|
||||||
|
assertEquals(4, method.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesTwoArgLevelStorageAccessMethod() throws Exception {
|
||||||
|
Method method = CapabilityResolution.resolveLevelStorageAccessMethod(TwoArgLevelStorageSource.class);
|
||||||
|
|
||||||
|
assertEquals("validateAndCreateAccess", method.getName());
|
||||||
|
assertEquals(2, method.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesOneArgLevelStorageAccessMethod() throws Exception {
|
||||||
|
Method method = CapabilityResolution.resolveLevelStorageAccessMethod(OneArgLevelStorageSource.class);
|
||||||
|
|
||||||
|
assertEquals("validateAndCreateAccess", method.getName());
|
||||||
|
assertEquals(1, method.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesPublicWorldDataHelper() throws Exception {
|
||||||
|
Method method = CapabilityResolution.resolvePaperWorldDataMethod(PublicWorldLoader.class);
|
||||||
|
|
||||||
|
assertEquals("loadWorldData", method.getName());
|
||||||
|
assertEquals(3, method.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesDeclaredWorldDataHelper() throws Exception {
|
||||||
|
Method method = CapabilityResolution.resolvePaperWorldDataMethod(DeclaredWorldLoader.class);
|
||||||
|
|
||||||
|
assertEquals("loadWorldData", method.getName());
|
||||||
|
assertEquals(3, method.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesDeclaredServerRegistryAccessMethod() throws Exception {
|
||||||
|
Method method = CapabilityResolution.resolveServerRegistryAccessMethod(DeclaredRegistryAccessOwner.class);
|
||||||
|
|
||||||
|
assertEquals("registryAccess", method.getName());
|
||||||
|
assertEquals(0, method.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesCurrentWorldLoadingInfoConstructor() throws Exception {
|
||||||
|
Constructor<?> constructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(CurrentWorldLoadingInfo.class);
|
||||||
|
|
||||||
|
assertEquals(4, constructor.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolvesLegacyWorldLoadingInfoConstructor() throws Exception {
|
||||||
|
Constructor<?> constructor = CapabilityResolution.resolveWorldLoadingInfoConstructor(LegacyWorldLoadingInfo.class);
|
||||||
|
|
||||||
|
assertEquals(5, constructor.getParameterCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class LevelStem {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class WorldLoadingInfo {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class WorldLoadingInfoAndData {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class WorldDataAndGenSettings {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class PrimaryLevelData {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class LevelStorageAccess {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ResourceKey {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class LoadedWorldData {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class MinecraftServer {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class RegistryAccess {
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Environment {
|
||||||
|
NORMAL
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class CurrentCreateLevelOwner {
|
||||||
|
public void createLevel(LevelStem levelStem, WorldLoadingInfoAndData worldLoadingInfoAndData, WorldDataAndGenSettings worldDataAndGenSettings) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class DeclaredLegacyCreateLevelOwner {
|
||||||
|
private void createLevel(LevelStem levelStem, WorldLoadingInfo worldLoadingInfo, LevelStorageAccess levelStorageAccess, PrimaryLevelData primaryLevelData) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class TwoArgLevelStorageSource {
|
||||||
|
public LevelStorageAccess validateAndCreateAccess(String worldName, ResourceKey resourceKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class OneArgLevelStorageSource {
|
||||||
|
public LevelStorageAccess validateAndCreateAccess(String worldName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class PublicWorldLoader {
|
||||||
|
public static LoadedWorldData loadWorldData(MinecraftServer minecraftServer, ResourceKey dimensionKey, String worldName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class DeclaredWorldLoader {
|
||||||
|
private static LoadedWorldData loadWorldData(MinecraftServer minecraftServer, ResourceKey dimensionKey, String worldName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class DeclaredRegistryAccessOwner {
|
||||||
|
private RegistryAccess registryAccess() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class CurrentWorldLoadingInfo {
|
||||||
|
public CurrentWorldLoadingInfo(Environment environment, ResourceKey stemKey, ResourceKey dimensionKey, boolean enabled) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class LegacyWorldLoadingInfo {
|
||||||
|
private LegacyWorldLoadingInfo(int index, String worldName, String environment, ResourceKey stemKey, boolean enabled) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
public class WorldLifecycleDiagnosticsTest {
|
||||||
|
@Test
|
||||||
|
public void studioCreateSelectionFailurePrintsFullStacktrace() {
|
||||||
|
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, false));
|
||||||
|
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
|
||||||
|
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
PrintStream originalErr = System.err;
|
||||||
|
System.setErr(new PrintStream(output, true, StandardCharsets.UTF_8));
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
service.create(request).join();
|
||||||
|
fail("Expected lifecycle create to fail when paper_like_runtime is unavailable.");
|
||||||
|
} catch (CompletionException | IllegalStateException ignored) {
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
System.setErr(originalErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = output.toString(StandardCharsets.UTF_8);
|
||||||
|
assertTrue(text.contains("WorldLifecycle create backend selection failed"));
|
||||||
|
assertTrue(text.contains("paper_like_runtime"));
|
||||||
|
assertTrue(text.contains("IllegalStateException"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.nms.INMSBinding;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
|
import art.arcane.iris.engine.platform.ChunkReplacementListener;
|
||||||
|
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
|
||||||
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
|
||||||
|
public class WorldLifecycleRuntimeLevelStemTest {
|
||||||
|
@Test
|
||||||
|
public void runtimeStemUsesFullServerRegistryAccessForPlatformGenerators() throws Exception {
|
||||||
|
Object datapackDimensions = new MissingDimensionTypeRegistry();
|
||||||
|
Object serverRegistryAccess = new Object();
|
||||||
|
CapabilitySnapshot capabilities = CapabilitySnapshot.forTestingRuntimeRegistries(ServerFamily.PURPUR, false, datapackDimensions, serverRegistryAccess);
|
||||||
|
WorldLifecycleRequest request = new WorldLifecycleRequest(
|
||||||
|
"studio",
|
||||||
|
World.Environment.NORMAL,
|
||||||
|
new TestingPlatformChunkGenerator(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
1337L,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
WorldLifecycleCaller.STUDIO
|
||||||
|
);
|
||||||
|
AtomicReference<Object> seenRegistryAccess = new AtomicReference<>();
|
||||||
|
INMSBinding binding = createBinding((registryAccess, generator) -> {
|
||||||
|
seenRegistryAccess.set(registryAccess);
|
||||||
|
return "runtime-stem";
|
||||||
|
});
|
||||||
|
|
||||||
|
Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request, binding);
|
||||||
|
|
||||||
|
assertEquals("runtime-stem", levelStem);
|
||||||
|
assertSame(serverRegistryAccess, seenRegistryAccess.get());
|
||||||
|
assertNotSame(datapackDimensions, seenRegistryAccess.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static INMSBinding createBinding(RuntimeStemFactory factory) {
|
||||||
|
InvocationHandler handler = (proxy, method, args) -> {
|
||||||
|
if ("createRuntimeLevelStem".equals(method.getName())) {
|
||||||
|
return factory.create(args[0], (ChunkGenerator) args[1]);
|
||||||
|
}
|
||||||
|
Class<?> returnType = method.getReturnType();
|
||||||
|
if (boolean.class.equals(returnType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (int.class.equals(returnType)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (long.class.equals(returnType)) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
if (float.class.equals(returnType)) {
|
||||||
|
return 0F;
|
||||||
|
}
|
||||||
|
if (double.class.equals(returnType)) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return (INMSBinding) Proxy.newProxyInstance(
|
||||||
|
INMSBinding.class.getClassLoader(),
|
||||||
|
new Class[]{INMSBinding.class},
|
||||||
|
handler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface RuntimeStemFactory {
|
||||||
|
Object create(Object registryAccess, ChunkGenerator generator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MissingDimensionTypeRegistry {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestingPlatformChunkGenerator extends ChunkGenerator implements PlatformChunkGenerator {
|
||||||
|
@Override
|
||||||
|
public Engine getEngine() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IrisData getData() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EngineTarget getTarget() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectChunkReplacement(World world, int x, int z, Executor syncExecutor, ChunkReplacementOptions options, ChunkReplacementListener listener) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStudio() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void touch(World world) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Integer> getSpawnChunks() {
|
||||||
|
return CompletableFuture.completedFuture(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Location getInitialSpawnLocation(World world) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void hotload() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class WorldLifecycleSelectionTest {
|
||||||
|
@Test
|
||||||
|
public void studioSelectsPaperLikeBackendOnPaper() {
|
||||||
|
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, true));
|
||||||
|
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
|
||||||
|
|
||||||
|
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void studioSelectsPaperLikeBackendOnPurpur() {
|
||||||
|
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true));
|
||||||
|
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
|
||||||
|
|
||||||
|
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void studioSelectsPaperLikeBackendOnCanvas() {
|
||||||
|
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.CANVAS, true, false, true));
|
||||||
|
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
|
||||||
|
|
||||||
|
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void studioSelectsPaperLikeBackendOnFolia() {
|
||||||
|
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, true));
|
||||||
|
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
|
||||||
|
|
||||||
|
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void studioSelectsBukkitBackendOnSpigot() {
|
||||||
|
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.SPIGOT, false, false, false));
|
||||||
|
WorldLifecycleRequest request = new WorldLifecycleRequest("studio", World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO);
|
||||||
|
|
||||||
|
assertEquals("bukkit_public", service.selectCreateBackend(request).backendName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void persistentCreatePrefersBukkitBackendOnPaperLikeServers() {
|
||||||
|
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true));
|
||||||
|
WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE);
|
||||||
|
|
||||||
|
assertEquals("bukkit_public", service.selectCreateBackend(request).backendName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unloadUsesRememberedBackendFamily() {
|
||||||
|
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true));
|
||||||
|
|
||||||
|
service.rememberBackend("studio", "paper_like_runtime");
|
||||||
|
assertEquals("paper_like_runtime", service.selectUnloadBackend("studio").backendName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import org.bukkit.generator.BiomeProvider;
|
||||||
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class WorldLifecycleStagingTest {
|
||||||
|
@Test
|
||||||
|
public void stagedGeneratorIsConsumedExactlyOnce() {
|
||||||
|
ChunkGenerator generator = mock(ChunkGenerator.class);
|
||||||
|
|
||||||
|
WorldLifecycleStaging.stageGenerator("world", generator, null);
|
||||||
|
|
||||||
|
assertSame(generator, WorldLifecycleStaging.consumeGenerator("world"));
|
||||||
|
assertNull(WorldLifecycleStaging.consumeGenerator("world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void stagedStemGeneratorIsIndependentFromGeneratorConsumption() {
|
||||||
|
ChunkGenerator generator = mock(ChunkGenerator.class);
|
||||||
|
|
||||||
|
WorldLifecycleStaging.stageGenerator("world", generator, null);
|
||||||
|
WorldLifecycleStaging.stageStemGenerator("world", generator);
|
||||||
|
|
||||||
|
assertSame(generator, WorldLifecycleStaging.consumeGenerator("world"));
|
||||||
|
assertSame(generator, WorldLifecycleStaging.consumeStemGenerator("world"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clearAllRemovesGeneratorBiomeAndStemState() {
|
||||||
|
ChunkGenerator generator = mock(ChunkGenerator.class);
|
||||||
|
BiomeProvider biomeProvider = mock(BiomeProvider.class);
|
||||||
|
|
||||||
|
WorldLifecycleStaging.stageGenerator("world", generator, biomeProvider);
|
||||||
|
WorldLifecycleStaging.stageStemGenerator("world", generator);
|
||||||
|
WorldLifecycleStaging.clearAll("world");
|
||||||
|
|
||||||
|
assertNull(WorldLifecycleStaging.consumeGenerator("world"));
|
||||||
|
assertNull(WorldLifecycleStaging.consumeBiomeProvider("world"));
|
||||||
|
assertNull(WorldLifecycleStaging.consumeStemGenerator("world"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package art.arcane.iris.core.nms;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class INMSBindingProbeCodesTest {
|
||||||
|
@Test
|
||||||
|
public void skipsSyntheticBukkitBindingWhenNmsIsEnabled() {
|
||||||
|
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("BUKKIT", false, List.of("v1_21_R7"));
|
||||||
|
|
||||||
|
assertEquals(List.of("v1_21_R7"), probeCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void leavesBukkitFallbackEmptyWhenNmsIsDisabled() {
|
||||||
|
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("BUKKIT", true, List.of("v1_21_R7"));
|
||||||
|
|
||||||
|
assertTrue(probeCodes.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void keepsConcreteBindingCodesAsPrimaryProbe() {
|
||||||
|
List<String> probeCodes = NmsBindingProbeSupport.getBindingProbeCodes("v1_21_R7", false, List.of("v1_21_R7"));
|
||||||
|
|
||||||
|
assertEquals(List.of("v1_21_R7"), probeCodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package art.arcane.iris.core.nms;
|
||||||
|
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class MinecraftVersionTest {
|
||||||
|
private interface PaperLikeServer extends Server {
|
||||||
|
String getMinecraftVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void detectsMinecraftVersionFromPurpurDecoratedVersion() {
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
doReturn("git-Purpur-2570 (MC: 1.21.11)").when(server).getVersion();
|
||||||
|
doReturn("26.1.2.build.2570-experimental").when(server).getBukkitVersion();
|
||||||
|
|
||||||
|
MinecraftVersion version = MinecraftVersion.detect(server);
|
||||||
|
assertEquals("1.21.11", version.value());
|
||||||
|
assertEquals(21, version.major());
|
||||||
|
assertEquals(11, version.minor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prefersRuntimeMinecraftVersionMethodWhenPresent() {
|
||||||
|
PaperLikeServer server = mock(PaperLikeServer.class);
|
||||||
|
doReturn("1.21.11").when(server).getMinecraftVersion();
|
||||||
|
doReturn("26.1.2-2570-e64b1b2 (MC: 26.1.2)").when(server).getVersion();
|
||||||
|
doReturn("26.1.2.build.2570-experimental").when(server).getBukkitVersion();
|
||||||
|
|
||||||
|
MinecraftVersion version = MinecraftVersion.detect(server);
|
||||||
|
assertEquals("1.21.11", version.value());
|
||||||
|
assertEquals(21, version.major());
|
||||||
|
assertEquals(11, version.minor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsPurpurApiBuildNumbersAsMinecraftVersion() {
|
||||||
|
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("26.1.2.build.2570-experimental");
|
||||||
|
assertNull(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parsesStandardBukkitSnapshotVersion() {
|
||||||
|
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("1.21.11-R0.1-SNAPSHOT");
|
||||||
|
assertEquals("1.21.11", version.value());
|
||||||
|
assertEquals(21, version.major());
|
||||||
|
assertEquals(11, version.minor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void comparesMajorBeforeMinor() {
|
||||||
|
MinecraftVersion version = MinecraftVersion.fromBukkitVersion("1.20.12-R0.1-SNAPSHOT");
|
||||||
|
assertFalse(version.isAtLeast(21, 11));
|
||||||
|
assertTrue(version.isNewerThan(20, 11));
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package art.arcane.iris.core.nms.datapack.v1217;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.nms.datapack.IDataFixer.Dimension;
|
||||||
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class DataFixerV1217DimensionTypeTest {
|
||||||
|
private final DataFixerV1217 fixer = new DataFixerV1217();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createsOverworldDimensionWithDragonFightDisabled() {
|
||||||
|
JSONObject json = fixer.createDimension(Dimension.OVERWORLD, -256, 768, 512, null);
|
||||||
|
|
||||||
|
assertTrue(json.has("has_ender_dragon_fight"));
|
||||||
|
assertEquals(false, json.getBoolean("has_ender_dragon_fight"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createsEndDimensionWithDragonFightEnabled() {
|
||||||
|
JSONObject json = fixer.createDimension(Dimension.END, 0, 256, 256, null);
|
||||||
|
|
||||||
|
assertTrue(json.has("has_ender_dragon_fight"));
|
||||||
|
assertEquals(true, json.getBoolean("has_ender_dragon_fight"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class DatapackReadinessResultTest {
|
||||||
|
@Test
|
||||||
|
public void verificationUsesDimensionTypeKeyPath() throws Exception {
|
||||||
|
Path root = Files.createTempDirectory("iris-datapack-readiness");
|
||||||
|
Path datapackRoot = root.resolve("iris");
|
||||||
|
Files.createDirectories(datapackRoot.resolve("data/iris/dimension_type"));
|
||||||
|
Files.writeString(datapackRoot.resolve("pack.mcmeta"), "{}");
|
||||||
|
Files.writeString(datapackRoot.resolve("data/iris/dimension_type/runtime-key.json"), "{}");
|
||||||
|
|
||||||
|
ArrayList<String> verifiedPaths = new ArrayList<>();
|
||||||
|
ArrayList<String> missingPaths = new ArrayList<>();
|
||||||
|
DatapackReadinessResult.collectVerificationPaths(root.toFile(), "runtime-key", verifiedPaths, missingPaths);
|
||||||
|
|
||||||
|
assertTrue(missingPaths.isEmpty());
|
||||||
|
assertEquals(2, verifiedPaths.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verificationMarksMissingDimensionTypePath() throws Exception {
|
||||||
|
Path root = Files.createTempDirectory("iris-datapack-readiness-missing");
|
||||||
|
Path datapackRoot = root.resolve("iris");
|
||||||
|
Files.createDirectories(datapackRoot);
|
||||||
|
Files.writeString(datapackRoot.resolve("pack.mcmeta"), "{}");
|
||||||
|
|
||||||
|
ArrayList<String> verifiedPaths = new ArrayList<>();
|
||||||
|
ArrayList<String> missingPaths = new ArrayList<>();
|
||||||
|
DatapackReadinessResult.collectVerificationPaths(root.toFile(), "runtime-key", verifiedPaths, missingPaths);
|
||||||
|
|
||||||
|
assertEquals(1, verifiedPaths.size());
|
||||||
|
assertEquals(1, missingPaths.size());
|
||||||
|
assertTrue(missingPaths.get(0).endsWith(File.separator + "iris" + File.separator + "data" + File.separator + "iris" + File.separator + "dimension_type" + File.separator + "runtime-key.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class SmokeDiagnosticsServiceCloseStateTest {
|
||||||
|
@Test
|
||||||
|
public void closeStateIsPersistedIntoRunSnapshot() {
|
||||||
|
SmokeDiagnosticsService service = SmokeDiagnosticsService.get();
|
||||||
|
SmokeDiagnosticsService.SmokeRunHandle handle = service.beginRun(
|
||||||
|
SmokeDiagnosticsService.SmokeRunMode.STUDIO_CLOSE,
|
||||||
|
"iris-test-world",
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
handle.setCloseState(true, false, true);
|
||||||
|
|
||||||
|
SmokeDiagnosticsService.SmokeRunReport report = handle.snapshot();
|
||||||
|
assertTrue(report.isCloseUnloadCompletedLive());
|
||||||
|
assertFalse(report.isCloseFolderDeletionCompletedLive());
|
||||||
|
assertTrue(report.isCloseStartupCleanupQueued());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class TransientWorldCleanupSupportTest {
|
||||||
|
@Test
|
||||||
|
public void identifiesTransientStudioBaseNamesAndSidecars() {
|
||||||
|
String baseName = "iris-123e4567-e89b-12d3-a456-426614174000";
|
||||||
|
|
||||||
|
assertTrue(TransientWorldCleanupSupport.isTransientStudioWorldName(baseName));
|
||||||
|
assertEquals(baseName, TransientWorldCleanupSupport.transientStudioBaseWorldName(baseName));
|
||||||
|
assertEquals(baseName, TransientWorldCleanupSupport.transientStudioBaseWorldName(baseName + "_nether"));
|
||||||
|
assertEquals(baseName, TransientWorldCleanupSupport.transientStudioBaseWorldName(baseName + "_the_end"));
|
||||||
|
assertFalse(TransientWorldCleanupSupport.isTransientStudioWorldName("iris-smoke-studio-deadbeef"));
|
||||||
|
assertNull(TransientWorldCleanupSupport.transientStudioBaseWorldName("overworld"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void expandsWorldFamilyNamesForDeletion() {
|
||||||
|
String baseName = "iris-123e4567-e89b-12d3-a456-426614174000";
|
||||||
|
|
||||||
|
List<String> names = TransientWorldCleanupSupport.worldFamilyNames(baseName);
|
||||||
|
|
||||||
|
assertEquals(List.of(baseName, baseName + "_nether", baseName + "_the_end"), names);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void collectsOnlyTransientStudioWorldFamiliesFromContainer() throws IOException {
|
||||||
|
File container = Files.createTempDirectory("transient-world-cleanup-test").toFile();
|
||||||
|
String baseName = "iris-123e4567-e89b-12d3-a456-426614174000";
|
||||||
|
File baseFolder = new File(container, baseName);
|
||||||
|
File netherFolder = new File(container, baseName + "_nether");
|
||||||
|
File smokeFolder = new File(container, "iris-smoke-studio-deadbeef");
|
||||||
|
File regularFolder = new File(container, "overworld");
|
||||||
|
baseFolder.mkdirs();
|
||||||
|
netherFolder.mkdirs();
|
||||||
|
smokeFolder.mkdirs();
|
||||||
|
regularFolder.mkdirs();
|
||||||
|
|
||||||
|
try {
|
||||||
|
LinkedHashSet<String> names = TransientWorldCleanupSupport.collectTransientStudioWorldNames(container);
|
||||||
|
|
||||||
|
assertEquals(new LinkedHashSet<>(List.of(baseName)), names);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(baseFolder.toPath());
|
||||||
|
Files.deleteIfExists(netherFolder.toPath());
|
||||||
|
Files.deleteIfExists(smokeFolder.toPath());
|
||||||
|
Files.deleteIfExists(regularFolder.toPath());
|
||||||
|
Files.deleteIfExists(container.toPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class WorldRuntimeControlServiceSafeEntryTest {
|
||||||
|
@Test
|
||||||
|
public void resolvesStudioEntryAnchorFromGeneratorInsteadOfMutableWorldSpawn() {
|
||||||
|
World world = mock(World.class);
|
||||||
|
PlatformChunkGenerator provider = mock(PlatformChunkGenerator.class);
|
||||||
|
Location initialSpawn = new Location(world, 0.5D, 96D, 0.5D);
|
||||||
|
Location mutableWorldSpawn = new Location(world, 128.5D, 80D, -64.5D);
|
||||||
|
|
||||||
|
doReturn(true).when(provider).isStudio();
|
||||||
|
doReturn(initialSpawn).when(provider).getInitialSpawnLocation(world);
|
||||||
|
doReturn(mutableWorldSpawn).when(world).getSpawnLocation();
|
||||||
|
|
||||||
|
Location resolved = WorldRuntimeControlService.resolveEntryAnchor(world, provider);
|
||||||
|
|
||||||
|
assertEquals(initialSpawn, resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fallsBackToWorldSpawnWhenGeneratorIsNotStudio() {
|
||||||
|
World world = mock(World.class);
|
||||||
|
PlatformChunkGenerator provider = mock(PlatformChunkGenerator.class);
|
||||||
|
Location mutableWorldSpawn = new Location(world, 128.5D, 80D, -64.5D);
|
||||||
|
|
||||||
|
doReturn(false).when(provider).isStudio();
|
||||||
|
doReturn(mutableWorldSpawn).when(world).getSpawnLocation();
|
||||||
|
|
||||||
|
Location resolved = WorldRuntimeControlService.resolveEntryAnchor(world, provider);
|
||||||
|
|
||||||
|
assertEquals(mutableWorldSpawn, resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void scansFromRuntimeSurfaceBeforeFallingThroughRemainingWorldHeight() {
|
||||||
|
World world = mock(World.class);
|
||||||
|
|
||||||
|
doReturn(0).when(world).getMinHeight();
|
||||||
|
doReturn(256).when(world).getMaxHeight();
|
||||||
|
doReturn(179).when(world).getHighestBlockYAt(0, 0);
|
||||||
|
|
||||||
|
int[] scanOrder = WorldRuntimeControlService.buildSafeLocationScanOrder(world, new Location(world, 0.5D, 96D, 0.5D));
|
||||||
|
|
||||||
|
assertEquals(180, scanOrder[0]);
|
||||||
|
assertEquals(179, scanOrder[1]);
|
||||||
|
assertEquals(1, scanOrder[179]);
|
||||||
|
assertEquals(181, scanOrder[180]);
|
||||||
|
assertEquals(254, scanOrder[scanOrder.length - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+239
@@ -0,0 +1,239 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
|
||||||
|
import art.arcane.iris.core.lifecycle.ServerFamily;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.plugin.PluginManager;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.OptionalLong;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class WorldRuntimeControlServiceTimeLockTest {
|
||||||
|
@Before
|
||||||
|
public void ensureBukkitServer() {
|
||||||
|
if (Bukkit.getServer() != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
PluginManager pluginManager = mock(PluginManager.class);
|
||||||
|
doReturn(pluginManager).when(server).getPluginManager();
|
||||||
|
doReturn(Logger.getLogger("WorldRuntimeControlServiceTimeLockTest")).when(server).getLogger();
|
||||||
|
Bukkit.setServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void skipsTimeLockWhenWorldDoesNotExposeMutableClock() throws Exception {
|
||||||
|
AtomicBoolean setTimeCalled = new AtomicBoolean(false);
|
||||||
|
AtomicLong dayTime = new AtomicLong(0L);
|
||||||
|
World world = createWorldProxy("fixed", true, setTimeCalled, dayTime, false);
|
||||||
|
|
||||||
|
boolean applied = createService().applyNoonTimeLock(world);
|
||||||
|
|
||||||
|
assertFalse(applied);
|
||||||
|
assertFalse(setTimeCalled.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void skipsTimeLockWhenRuntimeSetterRejectsClockMutation() throws Exception {
|
||||||
|
AtomicBoolean setTimeCalled = new AtomicBoolean(false);
|
||||||
|
AtomicLong dayTime = new AtomicLong(0L);
|
||||||
|
World world = createWorldProxy("no-clock", false, setTimeCalled, dayTime, true);
|
||||||
|
|
||||||
|
boolean applied = createService().applyNoonTimeLock(world);
|
||||||
|
|
||||||
|
assertFalse(applied);
|
||||||
|
assertTrue(setTimeCalled.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void appliesTimeLockWhenWorldHasMutableClock() throws Exception {
|
||||||
|
AtomicBoolean setTimeCalled = new AtomicBoolean(false);
|
||||||
|
AtomicLong dayTime = new AtomicLong(0L);
|
||||||
|
World world = createWorldProxy("mutable", false, setTimeCalled, dayTime, false);
|
||||||
|
|
||||||
|
boolean applied = createService().applyNoonTimeLock(world);
|
||||||
|
|
||||||
|
assertTrue(applied);
|
||||||
|
assertTrue(setTimeCalled.get());
|
||||||
|
assertTrue(dayTime.get() == 6000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorldRuntimeControlService createService() throws Exception {
|
||||||
|
Constructor<CapabilitySnapshot> snapshotConstructor = CapabilitySnapshot.class.getDeclaredConstructor(
|
||||||
|
ServerFamily.class,
|
||||||
|
boolean.class,
|
||||||
|
Object.class,
|
||||||
|
Class.class,
|
||||||
|
Class.class,
|
||||||
|
String.class,
|
||||||
|
Object.class,
|
||||||
|
Object.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
CapabilitySnapshot.PaperLikeFlavor.class,
|
||||||
|
Class.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
java.lang.reflect.Constructor.class,
|
||||||
|
java.lang.reflect.Constructor.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
java.lang.reflect.Field.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
java.lang.reflect.Field.class,
|
||||||
|
java.lang.reflect.Field.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
java.lang.reflect.Method.class,
|
||||||
|
String.class
|
||||||
|
);
|
||||||
|
snapshotConstructor.setAccessible(true);
|
||||||
|
CapabilitySnapshot snapshot = snapshotConstructor.newInstance(
|
||||||
|
ServerFamily.PAPER,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"test",
|
||||||
|
Bukkit.getServer(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
CapabilitySnapshot.PaperLikeFlavor.UNSUPPORTED,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"test"
|
||||||
|
);
|
||||||
|
|
||||||
|
Constructor<WorldRuntimeControlService> serviceConstructor = WorldRuntimeControlService.class.getDeclaredConstructor(CapabilitySnapshot.class);
|
||||||
|
serviceConstructor.setAccessible(true);
|
||||||
|
return serviceConstructor.newInstance(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private World createWorldProxy(String name, boolean fixedTime, AtomicBoolean setTimeCalled, AtomicLong dayTime, boolean throwOnSetTime) {
|
||||||
|
Object dimensionType = Proxy.newProxyInstance(
|
||||||
|
World.class.getClassLoader(),
|
||||||
|
new Class[]{DimensionTypeProbe.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("hasFixedTime".equals(method.getName())) {
|
||||||
|
return fixedTime;
|
||||||
|
}
|
||||||
|
if ("fixedTime".equals(method.getName())) {
|
||||||
|
return fixedTime ? OptionalLong.of(6000L) : OptionalLong.empty();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Object holder = Proxy.newProxyInstance(
|
||||||
|
World.class.getClassLoader(),
|
||||||
|
new Class[]{HolderProbe.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("value".equals(method.getName())) {
|
||||||
|
return dimensionType;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Object handle = Proxy.newProxyInstance(
|
||||||
|
World.class.getClassLoader(),
|
||||||
|
new Class[]{HandleProbe.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("dimensionTypeRegistration".equals(method.getName())) {
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
if ("getDayTime".equals(method.getName())) {
|
||||||
|
return dayTime.get();
|
||||||
|
}
|
||||||
|
if ("setDayTime".equals(method.getName())) {
|
||||||
|
setTimeCalled.set(true);
|
||||||
|
if (throwOnSetTime) {
|
||||||
|
throw new IllegalArgumentException("Cannot set time in world without world clock");
|
||||||
|
}
|
||||||
|
dayTime.set(((Long) args[0]).longValue());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (World) Proxy.newProxyInstance(
|
||||||
|
World.class.getClassLoader(),
|
||||||
|
new Class[]{World.class, WorldHandleProbe.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("getName".equals(method.getName())) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if ("getHandle".equals(method.getName())) {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
if ("getFullTime".equals(method.getName())) {
|
||||||
|
return dayTime.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<?> returnType = method.getReturnType();
|
||||||
|
if (boolean.class.equals(returnType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (int.class.equals(returnType)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (long.class.equals(returnType)) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
if (float.class.equals(returnType)) {
|
||||||
|
return 0F;
|
||||||
|
}
|
||||||
|
if (double.class.equals(returnType)) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface WorldHandleProbe {
|
||||||
|
Object getHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface HandleProbe {
|
||||||
|
Object dimensionTypeRegistration();
|
||||||
|
|
||||||
|
long getDayTime();
|
||||||
|
|
||||||
|
void setDayTime(long time);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface HolderProbe {
|
||||||
|
Object value();
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface DimensionTypeProbe {
|
||||||
|
boolean hasFixedTime();
|
||||||
|
|
||||||
|
OptionalLong fixedTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class GenerationSessionManagerTest {
|
||||||
|
@Test
|
||||||
|
public void teardownSealMarksRejectedWorkAsExpected() throws Exception {
|
||||||
|
GenerationSessionManager manager = new GenerationSessionManager();
|
||||||
|
|
||||||
|
manager.sealAndAwait("close", 1000L, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
manager.acquire("chunk_generate");
|
||||||
|
} catch (GenerationSessionException e) {
|
||||||
|
assertTrue(e.isExpectedTeardown());
|
||||||
|
assertTrue(e.getMessage().contains("during close"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AssertionError("Expected teardown rejection.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sealAndAwaitCompletesWhenOutstandingLeaseReleases() throws Exception {
|
||||||
|
GenerationSessionManager manager = new GenerationSessionManager();
|
||||||
|
GenerationSessionLease lease = manager.acquire("chunk_generate");
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
Thread releaser = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
latch.await(200L, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
lease.close();
|
||||||
|
});
|
||||||
|
releaser.start();
|
||||||
|
latch.countDown();
|
||||||
|
|
||||||
|
manager.sealAndAwait("close", 1000L, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class IrisDimensionTypeKeyTest {
|
||||||
|
@Test
|
||||||
|
public void dimensionTypeKeyUsesSanitizedSemanticPackKey() {
|
||||||
|
IrisDimension dimension = new IrisDimension();
|
||||||
|
dimension.setLoadKey("Overworld");
|
||||||
|
|
||||||
|
assertEquals("overworld", dimension.getDimensionTypeKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void dimensionTypeKeySanitizesUnsafePackCharacters() {
|
||||||
|
IrisDimension dimension = new IrisDimension();
|
||||||
|
dimension.setLoadKey("Worlds/My Pack");
|
||||||
|
|
||||||
|
assertEquals("worlds_my_pack", dimension.getDimensionTypeKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package art.arcane.iris.engine.object;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
public class IrisObjectBlockDataMergeTest {
|
||||||
|
@Test
|
||||||
|
public void reparsesBlockDataBeforeRetryingMerge() {
|
||||||
|
BlockData base = mock(BlockData.class);
|
||||||
|
BlockData update = mock(BlockData.class);
|
||||||
|
BlockData parsedBase = mock(BlockData.class);
|
||||||
|
BlockData parsedUpdate = mock(BlockData.class);
|
||||||
|
BlockData merged = mock(BlockData.class);
|
||||||
|
Function<String, BlockData> resolver = createResolver(
|
||||||
|
"minecraft:oak_log[axis=x]", parsedBase,
|
||||||
|
"minecraft:oak_log[axis=y]", parsedUpdate
|
||||||
|
);
|
||||||
|
|
||||||
|
doThrow(new IllegalArgumentException("Block data not created via string parsing")).when(base).merge(update);
|
||||||
|
doReturn("minecraft:oak_log[axis=x]").when(base).getAsString(false);
|
||||||
|
doReturn("minecraft:oak_log[axis=y]").when(update).getAsString(false);
|
||||||
|
doReturn(merged).when(parsedBase).merge(parsedUpdate);
|
||||||
|
|
||||||
|
BlockData result = BlockDataMergeSupport.merge(base, update, resolver);
|
||||||
|
|
||||||
|
assertSame(merged, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fallsBackToNormalizedUpdateWhenRetryMergeStillFails() {
|
||||||
|
BlockData base = mock(BlockData.class);
|
||||||
|
BlockData update = mock(BlockData.class);
|
||||||
|
BlockData parsedBase = mock(BlockData.class);
|
||||||
|
BlockData parsedUpdate = mock(BlockData.class);
|
||||||
|
Function<String, BlockData> resolver = createResolver(
|
||||||
|
"minecraft:stone", parsedBase,
|
||||||
|
"minecraft:stone[waterlogged=true]", parsedUpdate
|
||||||
|
);
|
||||||
|
|
||||||
|
doThrow(new IllegalArgumentException("Block data not created via string parsing")).when(base).merge(update);
|
||||||
|
doReturn("minecraft:stone").when(base).getAsString(false);
|
||||||
|
doReturn("minecraft:stone[waterlogged=true]").when(update).getAsString(false);
|
||||||
|
doThrow(new IllegalArgumentException("normalized merge failed")).when(parsedBase).merge(parsedUpdate);
|
||||||
|
|
||||||
|
BlockData result = BlockDataMergeSupport.merge(base, update, resolver);
|
||||||
|
|
||||||
|
assertSame(parsedUpdate, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void keepsDirectMergeWhenBukkitAcceptsIt() {
|
||||||
|
BlockData base = mock(BlockData.class);
|
||||||
|
BlockData update = mock(BlockData.class);
|
||||||
|
BlockData merged = mock(BlockData.class);
|
||||||
|
|
||||||
|
doReturn(Material.STONE).when(base).getMaterial();
|
||||||
|
doReturn(Material.STONE).when(update).getMaterial();
|
||||||
|
doReturn(merged).when(base).merge(update);
|
||||||
|
|
||||||
|
BlockData result = BlockDataMergeSupport.merge(base, update, key -> null);
|
||||||
|
|
||||||
|
assertSame(merged, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<String, BlockData> createResolver(String firstKey, BlockData firstValue, String secondKey, BlockData secondValue) {
|
||||||
|
Map<String, BlockData> resolved = new HashMap<>();
|
||||||
|
resolved.put(firstKey, firstValue);
|
||||||
|
resolved.put(secondKey, secondValue);
|
||||||
|
return resolved::get;
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package art.arcane.iris.util.common.parallel;
|
||||||
|
|
||||||
|
import art.arcane.volmlib.util.parallel.BurstExecutorSupport;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.ForkJoinPool;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class BurstExecutorSupportReentrantTest {
|
||||||
|
@Test
|
||||||
|
public void runsNestedBurstInlineOnSameForkJoinPoolWorker() throws Exception {
|
||||||
|
ForkJoinPool pool = new ForkJoinPool(1);
|
||||||
|
AtomicBoolean nestedExecuted = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Future<?> future = pool.submit(() -> {
|
||||||
|
BurstExecutorSupport burst = new BurstExecutorSupport(pool, 1);
|
||||||
|
burst.queue(() -> {
|
||||||
|
BurstExecutorSupport nested = new BurstExecutorSupport(pool, 1);
|
||||||
|
nested.queue(() -> nestedExecuted.set(true));
|
||||||
|
nested.complete();
|
||||||
|
});
|
||||||
|
burst.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
future.get(5, TimeUnit.SECONDS);
|
||||||
|
assertTrue(nestedExecuted.get());
|
||||||
|
} finally {
|
||||||
|
pool.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package art.arcane.iris.util.common.plugin;
|
||||||
|
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class VolmitSenderMiniMessageEscapeTest {
|
||||||
|
@Test
|
||||||
|
public void escapesApostrophesForQuotedHoverText() {
|
||||||
|
String escaped = VolmitSender.escapeMiniMessageQuotedText("This world's dimension config");
|
||||||
|
|
||||||
|
assertEquals("This world\\'s dimension config", escaped);
|
||||||
|
MiniMessage.miniMessage().deserialize("<hover:show_text:'" + escaped + "'>ok</hover>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void escapesBackslashesBeforeQuotedHoverText() {
|
||||||
|
String escaped = VolmitSender.escapeMiniMessageQuotedText("Path \\\\ data");
|
||||||
|
|
||||||
|
assertEquals("Path \\\\\\\\ data", escaped);
|
||||||
|
MiniMessage.miniMessage().deserialize("<hover:show_text:'" + escaped + "'>ok</hover>");
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.util.project.context;
|
package art.arcane.iris.util.project.context;
|
||||||
|
|
||||||
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
@@ -7,9 +8,16 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
|
|||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
@@ -76,6 +84,16 @@ public class ChunkContextPrefillPlanTest {
|
|||||||
assertEquals(256, caveCalls.get());
|
assertEquals(256, caveCalls.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void paperCommonWorkerThreadsDisableAsyncPrefillWhenPluginLoaded() throws Exception {
|
||||||
|
assertPrefillAsyncDecision("Paper Common Worker #0", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void irisWorkerThreadsKeepAsyncPrefillWhenPluginLoaded() throws Exception {
|
||||||
|
assertPrefillAsyncDecision("Iris 42", true);
|
||||||
|
}
|
||||||
|
|
||||||
private ChunkContext createContext(
|
private ChunkContext createContext(
|
||||||
ChunkContext.PrefillPlan prefillPlan,
|
ChunkContext.PrefillPlan prefillPlan,
|
||||||
AtomicInteger caveCalls,
|
AtomicInteger caveCalls,
|
||||||
@@ -145,4 +163,26 @@ public class ChunkContextPrefillPlanTest {
|
|||||||
|
|
||||||
return new ChunkContext(32, 48, complex, true, prefillPlan, null);
|
return new ChunkContext(32, 48, complex, true, prefillPlan, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void assertPrefillAsyncDecision(String threadName, boolean expected) throws InterruptedException, ExecutionException, java.util.concurrent.TimeoutException {
|
||||||
|
Iris previous = Iris.instance;
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
|
Thread thread = new Thread(runnable);
|
||||||
|
thread.setName(threadName);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
Iris.instance = mock(Iris.class);
|
||||||
|
Future<Boolean> future = executor.submit(() -> ChunkContext.shouldPrefillAsync(2));
|
||||||
|
boolean actual = future.get(10, TimeUnit.SECONDS);
|
||||||
|
if (expected) {
|
||||||
|
assertTrue(actual);
|
||||||
|
} else {
|
||||||
|
assertFalse(actual);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Iris.instance = previous;
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
Paper 1.21.11
|
||||||
|
/iris create matrix-paper overworld
|
||||||
|
/iris std o overworld
|
||||||
|
close Studio world
|
||||||
|
benchmark create/unload
|
||||||
|
|
||||||
|
Purpur 1.21.11
|
||||||
|
/iris create matrix-purpur overworld
|
||||||
|
/iris std o overworld
|
||||||
|
close Studio world
|
||||||
|
benchmark create/unload
|
||||||
|
|
||||||
|
Canvas 1.21.11
|
||||||
|
/iris create matrix-canvas overworld
|
||||||
|
/iris std o overworld
|
||||||
|
close Studio world
|
||||||
|
benchmark create/unload
|
||||||
|
|
||||||
|
Folia 1.21.11
|
||||||
|
/iris create matrix-folia overworld
|
||||||
|
/iris std o overworld
|
||||||
|
close Studio world
|
||||||
|
benchmark create/unload
|
||||||
|
|
||||||
|
Spigot 1.21.11
|
||||||
|
/iris create matrix-spigot overworld
|
||||||
|
/iris std o overworld
|
||||||
|
close Studio world
|
||||||
|
unload/remove
|
||||||
@@ -54,6 +54,7 @@ mythic = "5.9.5"
|
|||||||
mythic-chrucible = "2.1.0"
|
mythic-chrucible = "2.1.0"
|
||||||
kgenerators = "7.3" # https://repo.codemc.io/repository/maven-public/me/kryniowesegryderiusz/kgenerators-core/maven-metadata.xml
|
kgenerators = "7.3" # https://repo.codemc.io/repository/maven-public/me/kryniowesegryderiusz/kgenerators-core/maven-metadata.xml
|
||||||
multiverseCore = "5.1.0"
|
multiverseCore = "5.1.0"
|
||||||
|
craftengine = "0.0.67" # https://github.com/Xiao-MoMi/craft-engine/releases
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# Core Libraries
|
# Core Libraries
|
||||||
@@ -104,6 +105,8 @@ mythic = { module = "io.lumine:Mythic-Dist", version.ref = "mythic" }
|
|||||||
mythicChrucible = { module = "io.lumine:MythicCrucible-Dist", version.ref = "mythic-chrucible" }
|
mythicChrucible = { module = "io.lumine:MythicCrucible-Dist", version.ref = "mythic-chrucible" }
|
||||||
kgenerators = { module = "me.kryniowesegryderiusz:kgenerators-core", version.ref = "kgenerators" }
|
kgenerators = { module = "me.kryniowesegryderiusz:kgenerators-core", version.ref = "kgenerators" }
|
||||||
multiverseCore = { module = "org.mvplugins.multiverse.core:multiverse-core", version.ref = "multiverseCore" }
|
multiverseCore = { module = "org.mvplugins.multiverse.core:multiverse-core", version.ref = "multiverseCore" }
|
||||||
|
craftengine-core = { module = "net.momirealms:craft-engine-core", version.ref = "craftengine" }
|
||||||
|
craftengine-bukkit = { module = "net.momirealms:craft-engine-bukkit", version.ref = "craftengine" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
|
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import net.minecraft.world.level.biome.BiomeSource;
|
|||||||
import net.minecraft.world.level.biome.Climate;
|
import net.minecraft.world.level.biome.Climate;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.CraftServer;
|
import org.bukkit.craftbukkit.CraftServer;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.CraftWorld;
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|||||||
+2
-2
@@ -36,8 +36,8 @@ import net.minecraft.world.level.levelgen.structure.StructureSet;
|
|||||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.CraftWorld;
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.generator.CustomChunkGenerator;
|
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||||
import org.spigotmc.SpigotWorldConfig;
|
import org.spigotmc.SpigotWorldConfig;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import net.minecraft.server.MinecraftServer;
|
|||||||
import net.minecraft.server.commands.data.BlockDataAccessor;
|
import net.minecraft.server.commands.data.BlockDataAccessor;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.tags.TagKey;
|
import net.minecraft.tags.TagKey;
|
||||||
import net.minecraft.world.RandomSequences;
|
|
||||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||||
import net.minecraft.world.entity.EntityType;
|
import net.minecraft.world.entity.EntityType;
|
||||||
import net.minecraft.world.item.component.CustomData;
|
import net.minecraft.world.item.component.CustomData;
|
||||||
@@ -65,19 +64,19 @@ import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
|||||||
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||||
import net.minecraft.world.level.storage.PrimaryLevelData;
|
import net.minecraft.world.level.storage.ServerLevelData;
|
||||||
import org.bukkit.*;
|
import org.bukkit.*;
|
||||||
import org.bukkit.block.Biome;
|
import org.bukkit.block.Biome;
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.CraftChunk;
|
import org.bukkit.craftbukkit.CraftChunk;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.CraftServer;
|
import org.bukkit.craftbukkit.CraftServer;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.CraftWorld;
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.block.CraftBlockState;
|
import org.bukkit.craftbukkit.block.CraftBlockState;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.block.CraftBlockStates;
|
import org.bukkit.craftbukkit.block.CraftBlockStates;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.block.data.CraftBlockData;
|
import org.bukkit.craftbukkit.block.data.CraftBlockData;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack;
|
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.util.CraftMagicNumbers;
|
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
||||||
import org.bukkit.craftbukkit.v1_21_R7.util.CraftNamespacedKey;
|
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
|
||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||||
import org.bukkit.generator.BiomeProvider;
|
import org.bukkit.generator.BiomeProvider;
|
||||||
@@ -562,8 +561,17 @@ public class NMSBinding implements INMSBinding {
|
|||||||
worldGenContextField.setAccessible(true);
|
worldGenContextField.setAccessible(true);
|
||||||
var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
|
var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
|
||||||
var dimensionType = chunkMap.level.dimensionTypeRegistration().unwrapKey().orElse(null);
|
var dimensionType = chunkMap.level.dimensionTypeRegistration().unwrapKey().orElse(null);
|
||||||
if (dimensionType != null && !dimensionType.identifier().getNamespace().equals("iris"))
|
String expectedDimensionType = "iris:" + engine.getDimension().getDimensionTypeKey();
|
||||||
Iris.error("Loaded world %s with invalid dimension type! (%s)", world.getName(), dimensionType.identifier().toString());
|
if (dimensionType != null) {
|
||||||
|
String actualDimensionType = dimensionType.identifier().toString();
|
||||||
|
if (!dimensionType.identifier().getNamespace().equals("iris")) {
|
||||||
|
Iris.error("Loaded world %s with invalid dimension type! expected=%s actual=%s", world.getName(), expectedDimensionType, actualDimensionType);
|
||||||
|
} else {
|
||||||
|
Iris.info("Loaded world %s with Iris dimension type %s", world.getName(), actualDimensionType);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Iris.error("Loaded world %s with unknown dimension type! expected=%s", world.getName(), expectedDimensionType);
|
||||||
|
}
|
||||||
|
|
||||||
var newContext = new WorldGenContext(
|
var newContext = new WorldGenContext(
|
||||||
worldGenContext.level(), new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world),
|
worldGenContext.level(), new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world),
|
||||||
@@ -786,9 +794,8 @@ public class NMSBinding implements INMSBinding {
|
|||||||
var buddy = new ByteBuddy();
|
var buddy = new ByteBuddy();
|
||||||
buddy.redefine(ServerLevel.class)
|
buddy.redefine(ServerLevel.class)
|
||||||
.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor().and(ElementMatchers.takesArguments(
|
.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor().and(ElementMatchers.takesArguments(
|
||||||
MinecraftServer.class, Executor.class, LevelStorageSource.LevelStorageAccess.class, PrimaryLevelData.class,
|
MinecraftServer.class, Executor.class, LevelStorageSource.LevelStorageAccess.class, ServerLevelData.class,
|
||||||
ResourceKey.class, LevelStem.class, boolean.class, long.class, List.class,
|
ResourceKey.class, LevelStem.class, boolean.class, long.class, List.class, boolean.class))))
|
||||||
boolean.class, RandomSequences.class, World.Environment.class, ChunkGenerator.class, BiomeProvider.class))))
|
|
||||||
.make()
|
.make()
|
||||||
.load(ServerLevel.class.getClassLoader(), Agent.installed());
|
.load(ServerLevel.class.getClassLoader(), Agent.installed());
|
||||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||||
@@ -896,12 +903,18 @@ public class NMSBinding implements INMSBinding {
|
|||||||
.collect(Collectors.toMap(Pair::getA, Pair::getB, (a, b) -> a, KMap::new));
|
.collect(Collectors.toMap(Pair::getA, Pair::getB, (a, b) -> a, KMap::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
public LevelStem levelStem(RegistryAccess access, ChunkGenerator raw) {
|
@Override
|
||||||
if (!(raw instanceof PlatformChunkGenerator gen))
|
public Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) {
|
||||||
|
if (!(registryAccess instanceof RegistryAccess access)) {
|
||||||
|
throw new IllegalStateException("Runtime LevelStem creation requires a RegistryAccess instance.");
|
||||||
|
}
|
||||||
|
if (!(raw instanceof PlatformChunkGenerator generator)) {
|
||||||
throw new IllegalStateException("Generator is not platform chunk generator!");
|
throw new IllegalStateException("Generator is not platform chunk generator!");
|
||||||
|
}
|
||||||
|
|
||||||
var dimensionKey = Identifier.fromNamespaceAndPath("iris", gen.getTarget().getDimension().getDimensionTypeKey());
|
Identifier dimensionKey = Identifier.fromNamespaceAndPath("iris", generator.getTarget().getDimension().getDimensionTypeKey());
|
||||||
var dimensionType = access.lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, dimensionKey));
|
Holder.Reference<net.minecraft.world.level.dimension.DimensionType> dimensionType = access.lookupOrThrow(Registries.DIMENSION_TYPE)
|
||||||
|
.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, dimensionKey));
|
||||||
return new LevelStem(dimensionType, chunkGenerator(access));
|
return new LevelStem(dimensionType, chunkGenerator(access));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,25 +936,42 @@ public class NMSBinding implements INMSBinding {
|
|||||||
@Advice.OnMethodEnter
|
@Advice.OnMethodEnter
|
||||||
static void enter(
|
static void enter(
|
||||||
@Advice.Argument(0) MinecraftServer server,
|
@Advice.Argument(0) MinecraftServer server,
|
||||||
@Advice.Argument(3) PrimaryLevelData levelData,
|
@Advice.Argument(2) LevelStorageSource.LevelStorageAccess levelStorageAccess,
|
||||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||||
@Advice.Argument(11) World.Environment env,
|
@Advice.Argument(3) ServerLevelData levelData
|
||||||
@Advice.Argument(12) ChunkGenerator gen
|
|
||||||
) {
|
) {
|
||||||
if (gen == null || !gen.getClass().getPackageName().startsWith("art.arcane.iris"))
|
if (levelStorageAccess == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
String levelId = levelStorageAccess.getLevelId();
|
||||||
|
if (levelId == null || levelId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, Bukkit.getPluginManager().getPlugin("Iris")
|
||||||
|
.getClass()
|
||||||
|
.getClassLoader())
|
||||||
|
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||||
|
.invoke(null, levelId);
|
||||||
|
if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris")
|
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris")
|
||||||
.getClass()
|
.getClass()
|
||||||
.getClassLoader())
|
.getClassLoader())
|
||||||
.getDeclaredMethod("get")
|
.getDeclaredMethod("get")
|
||||||
.invoke(null);
|
.invoke(null);
|
||||||
levelStem = (LevelStem) bindings.getClass()
|
if (!(bindings instanceof INMSBinding binding)) {
|
||||||
.getDeclaredMethod("levelStem", RegistryAccess.class, ChunkGenerator.class)
|
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||||
.invoke(bindings, server.registryAccess(), gen);
|
}
|
||||||
|
|
||||||
levelData.customDimensions = null;
|
Object resolvedStem = binding.createRuntimeLevelStem(server.registryAccess(), gen);
|
||||||
|
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||||
|
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||||
|
}
|
||||||
|
levelStem = runtimeStem;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user