mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-13 02:15:46 +00:00
nms patches
This commit is contained in:
Vendored
+1
-1
@@ -1 +1 @@
|
||||
-2003345818
|
||||
203609563
|
||||
@@ -25,6 +25,7 @@ import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||
@@ -545,6 +546,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
|
||||
private void enable() {
|
||||
PaperLibBootstrap.install();
|
||||
services = new KMap<>();
|
||||
setupAudience();
|
||||
Bindings.setupSentry();
|
||||
@@ -575,7 +577,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
WorldRuntimeControlService.get();
|
||||
|
||||
if (J.isFolia()) {
|
||||
checkForBukkitWorlds(s -> true);
|
||||
J.s(() -> checkForBukkitWorlds(s -> true), 1);
|
||||
}
|
||||
|
||||
J.s(() -> {
|
||||
@@ -641,6 +643,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
WorldCreator c = new WorldCreator(s)
|
||||
.generator(gen)
|
||||
.environment(dim.getEnvironment());
|
||||
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
|
||||
if (stagedSeed != null) {
|
||||
c.seed(stagedSeed);
|
||||
}
|
||||
INMS.get().createWorld(c);
|
||||
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
|
||||
} catch (Throwable e) {
|
||||
@@ -661,8 +667,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
});
|
||||
if (!deferredStartupWorlds.isEmpty()) {
|
||||
Iris.warn("World init delayed on Folia until server world-init phase for staged Iris worlds: %s", String.join(", ", deferredStartupWorlds));
|
||||
Iris.warn("Bukkit.createWorld is intentionally unavailable in this startup phase. Worlds remain staged in bukkit.yml.");
|
||||
Iris.warn("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds));
|
||||
Iris.warn("Bukkit.createWorld is unsupported on this server and the Iris runtime world backend is unavailable (%s).", WorldLifecycleService.get().capabilities().paperLikeResolution());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
reportError("Failed while loading startup Iris worlds.", e);
|
||||
|
||||
@@ -11,6 +11,7 @@ import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
@@ -101,6 +102,16 @@ public class IrisWorlds {
|
||||
}
|
||||
}
|
||||
|
||||
public static Long readBukkitWorldSeed(String world) {
|
||||
YamlConfiguration bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||
ConfigurationSection worlds = bukkit.getConfigurationSection("worlds");
|
||||
if (worlds == null || !worlds.contains(world + ".seed")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return worlds.getLong(world + ".seed");
|
||||
}
|
||||
|
||||
public static KMap<String, String> readBukkitWorlds() {
|
||||
var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||
var worlds = bukkit.getConfigurationSection("worlds");
|
||||
|
||||
@@ -219,7 +219,12 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
}
|
||||
|
||||
@Director(description = "Test")
|
||||
public void mantle(@Param(defaultValue = "false") boolean plate, @Param(defaultValue = "21474836474") String name) throws Throwable {
|
||||
public void mantle(
|
||||
@Param(name = "plate", description = "Dump the whole tectonic plate instead of a single section", defaultValue = "false")
|
||||
boolean plate,
|
||||
@Param(name = "name", description = "The dump file id under plugins/Iris/dump (pv.<id>.*)", defaultValue = "21474836474")
|
||||
String name
|
||||
) throws Throwable {
|
||||
var base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
|
||||
var section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
|
||||
|
||||
@@ -256,7 +261,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
|
||||
@Director(description = "test")
|
||||
public void mca (
|
||||
@Param(description = "String") String world) {
|
||||
@Param(description = "The world folder to scan for .mca region files") String world) {
|
||||
try {
|
||||
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
|
||||
for (File mca : McaFiles) {
|
||||
|
||||
@@ -115,11 +115,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
@Param(description = "The seed to generate the world with", defaultValue = "1337")
|
||||
long seed,
|
||||
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
|
||||
boolean main,
|
||||
@Param(aliases = {"remove-others", "removeothers"}, description = "When main-world is true, remove other Iris worlds from bukkit.yml and queue deletion on startup", defaultValue = "false")
|
||||
boolean removeOthers,
|
||||
@Param(aliases = {"remove-worlds", "removeworlds"}, description = "Comma-separated world names to remove from Iris control and delete on next startup (main-world only)", defaultValue = "none")
|
||||
String removeWorlds
|
||||
boolean main
|
||||
) {
|
||||
if (name.equalsIgnoreCase("iris")) {
|
||||
sender().sendMessage(C.RED + "You cannot use the world name \"iris\" for creating worlds as Iris uses this directory for studio worlds.");
|
||||
@@ -146,18 +142,12 @@ public class CommandIris implements DirectorExecutor {
|
||||
if (dimension == null) {
|
||||
sender().sendMessage(C.RED + "Could not find or download dimension \"" + resolvedType + "\".");
|
||||
sender().sendMessage(C.YELLOW + "Try one of: overworld, vanilla, flat, theend");
|
||||
sender().sendMessage(C.YELLOW + "Or download manually: /iris download IrisDimensions/" + resolvedType);
|
||||
sender().sendMessage(C.YELLOW + "Or download manually: /iris download " + resolvedType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!main && (removeOthers || hasExplicitCleanupWorlds(removeWorlds))) {
|
||||
sender().sendMessage(C.YELLOW + "remove-others/remove-worlds only apply when main-world=true. Ignoring cleanup options.");
|
||||
removeOthers = false;
|
||||
removeWorlds = "none";
|
||||
}
|
||||
|
||||
if (J.isFolia()) {
|
||||
if (stageFoliaWorldCreation(name, dimension, seed, main, removeOthers, removeWorlds)) {
|
||||
if (stageFoliaWorldCreation(name, dimension, seed, main)) {
|
||||
sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\".");
|
||||
}
|
||||
return;
|
||||
@@ -185,11 +175,6 @@ public class CommandIris implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (main && !applyMainWorldCleanup(name, removeOthers, removeWorlds)) {
|
||||
worldCreation = false;
|
||||
return;
|
||||
}
|
||||
|
||||
worldCreation = false;
|
||||
sender().sendMessage(C.GREEN + "Successfully created your world!");
|
||||
if (main) sender().sendMessage(C.GREEN + "Your world will automatically be set as the main world when the server restarts.");
|
||||
@@ -230,7 +215,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main, boolean removeOthers, String removeWorlds) {
|
||||
private boolean stageFoliaWorldCreation(String name, IrisDimension dimension, long seed, boolean main) {
|
||||
sender().sendMessage(C.YELLOW + "Runtime world creation is disabled on Folia.");
|
||||
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
|
||||
|
||||
@@ -252,11 +237,6 @@ public class CommandIris implements DirectorExecutor {
|
||||
sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!applyMainWorldCleanup(name, removeOthers, removeWorlds)) {
|
||||
sender().sendMessage(C.RED + "World was staged, but failed to apply main-world cleanup options.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Staged Iris world \"" + name + "\" with generator Iris:" + dimension.getLoadKey() + " and seed " + seed + ".");
|
||||
@@ -295,126 +275,6 @@ public class CommandIris implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean applyMainWorldCleanup(String mainWorld, boolean removeOthers, String removeWorlds) {
|
||||
Set<String> targets = resolveCleanupTargets(mainWorld, removeOthers, removeWorlds);
|
||||
if (targets.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
sender().sendMessage(C.YELLOW + "Applying main-world cleanup for " + targets.size() + " world(s).");
|
||||
|
||||
YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML);
|
||||
ConfigurationSection worlds = yml.getConfigurationSection("worlds");
|
||||
|
||||
Set<String> removedFromBukkit = new LinkedHashSet<>();
|
||||
Set<String> notRemoved = new LinkedHashSet<>();
|
||||
for (String target : targets) {
|
||||
String key = findWorldKeyIgnoreCase(worlds, target);
|
||||
if (key == null) {
|
||||
notRemoved.add(target);
|
||||
continue;
|
||||
}
|
||||
|
||||
String generator = worlds.getString(key + ".generator");
|
||||
if (generator == null || !(generator.equalsIgnoreCase("iris") || generator.startsWith("Iris:"))) {
|
||||
notRemoved.add(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
worlds.set(key, null);
|
||||
removedFromBukkit.add(key);
|
||||
}
|
||||
|
||||
try {
|
||||
if (worlds != null && worlds.getKeys(false).isEmpty()) {
|
||||
yml.set("worlds", null);
|
||||
}
|
||||
|
||||
if (!removedFromBukkit.isEmpty()) {
|
||||
yml.save(BUKKIT_YML);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to update bukkit.yml while applying cleanup: " + e.getMessage());
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
int queued = Iris.queueWorldDeletionOnStartup(targets);
|
||||
if (queued > 0) {
|
||||
sender().sendMessage(C.GREEN + "Queued " + queued + " world folder(s) for deletion on next startup.");
|
||||
} else {
|
||||
sender().sendMessage(C.YELLOW + "Cleanup queue already contained the requested world folder(s).");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
sender().sendMessage(C.RED + "Failed to queue startup world deletions: " + e.getMessage());
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!removedFromBukkit.isEmpty()) {
|
||||
sender().sendMessage(C.GREEN + "Removed from Iris control in bukkit.yml: " + String.join(", ", removedFromBukkit));
|
||||
}
|
||||
|
||||
if (!notRemoved.isEmpty()) {
|
||||
sender().sendMessage(C.YELLOW + "Skipped from bukkit.yml removal (not found or non-Iris generator): " + String.join(", ", notRemoved));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Set<String> resolveCleanupTargets(String mainWorld, boolean removeOthers, String removeWorlds) {
|
||||
Set<String> targets = new LinkedHashSet<>();
|
||||
if (removeOthers) {
|
||||
IrisWorlds.readBukkitWorlds().keySet().stream()
|
||||
.filter(world -> !world.equalsIgnoreCase(mainWorld))
|
||||
.forEach(targets::add);
|
||||
}
|
||||
|
||||
if (hasExplicitCleanupWorlds(removeWorlds)) {
|
||||
for (String raw : removeWorlds.split("[,;\\s]+")) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw.equalsIgnoreCase(mainWorld)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
targets.add(raw.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static boolean hasExplicitCleanupWorlds(String removeWorlds) {
|
||||
if (removeWorlds == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trimmed = removeWorlds.trim();
|
||||
return !trimmed.isEmpty() && !trimmed.equalsIgnoreCase("none");
|
||||
}
|
||||
|
||||
private static String findWorldKeyIgnoreCase(ConfigurationSection worlds, String requested) {
|
||||
if (worlds == null || requested == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (worlds.contains(requested)) {
|
||||
return requested;
|
||||
}
|
||||
|
||||
for (String key : worlds.getKeys(false)) {
|
||||
if (key.equalsIgnoreCase(requested)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Director(description = "Teleport to another world", aliases = {"tp"}, sync = true)
|
||||
public void teleport(
|
||||
@Param(description = "World to teleport to")
|
||||
@@ -595,7 +455,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
|
||||
@Director(description = "Download a project.", aliases = "dl")
|
||||
public void download(
|
||||
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
|
||||
@Param(name = "pack", description = "The pack to download", aliases = "project")
|
||||
String pack,
|
||||
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
|
||||
String branch,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import io.papermc.lib.environments.PaperEnvironment;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
public final class PaperLibBootstrap {
|
||||
private PaperLibBootstrap() {
|
||||
}
|
||||
|
||||
public static void install() {
|
||||
if (PaperLib.getEnvironment().getMinecraftVersion() > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String bukkitVersion = Bukkit.getBukkitVersion();
|
||||
if (!isModernVersionScheme(bukkitVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasAsyncTeleport = hasMethod(Entity.class, "teleportAsync", Location.class, PlayerTeleportEvent.TeleportCause.class);
|
||||
boolean hasAsyncChunks = hasMethod(World.class, "getChunkAtAsync", int.class, int.class, boolean.class);
|
||||
if (!hasAsyncTeleport || !hasAsyncChunks) {
|
||||
return;
|
||||
}
|
||||
|
||||
PaperLib.setCustomEnvironment(new ModernPaperEnvironment());
|
||||
Iris.info("PaperLib version detection failed for MC " + bukkitVersion + "; forced modern Paper environment");
|
||||
}
|
||||
|
||||
static boolean isModernVersionScheme(String bukkitVersion) {
|
||||
if (bukkitVersion == null || bukkitVersion.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int end = 0;
|
||||
while (end < bukkitVersion.length() && Character.isDigit(bukkitVersion.charAt(end))) {
|
||||
end++;
|
||||
}
|
||||
|
||||
if (end == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return Integer.parseInt(bukkitVersion.substring(0, end)) > 1;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasMethod(Class<?> owner, String name, Class<?>... parameterTypes) {
|
||||
try {
|
||||
owner.getMethod(name, parameterTypes);
|
||||
return true;
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ModernPaperEnvironment extends PaperEnvironment {
|
||||
@Override
|
||||
public boolean isVersion(int minor, int patch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,15 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
|
||||
|
||||
@Override
|
||||
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
||||
return request.studio()
|
||||
&& capabilities.serverFamily().isPaperLike()
|
||||
&& capabilities.hasPaperLikeRuntime();
|
||||
if (!capabilities.serverFamily().isPaperLike() || !capabilities.hasPaperLikeRuntime()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.studio()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return capabilities.serverFamily() == ServerFamily.FOLIA || capabilities.regionizedRuntime();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,6 +56,9 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
|
||||
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, !request.studio());
|
||||
Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData);
|
||||
Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName());
|
||||
if (!WorldLifecycleSupport.hasExistingWorldData(request.worldName())) {
|
||||
worldDataAndGenSettings = WorldLifecycleSupport.applySeedToWorldDataAndGenSettings(worldDataAndGenSettings, request.seed());
|
||||
}
|
||||
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings);
|
||||
} else {
|
||||
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName());
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.bukkit.configuration.file.YamlConfiguration;
|
||||
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;
|
||||
@@ -21,6 +22,7 @@ import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -252,6 +254,63 @@ final class WorldLifecycleSupport {
|
||||
setModdedInfoMethod.invoke(worldData, modName, modified);
|
||||
}
|
||||
|
||||
static boolean hasExistingWorldData(String worldName) {
|
||||
File worldFolder = new File(Bukkit.getWorldContainer(), worldName);
|
||||
return new File(worldFolder, "level.dat").exists()
|
||||
|| new File(worldFolder, "region").exists()
|
||||
|| new File(worldFolder, "data").exists();
|
||||
}
|
||||
|
||||
static Object applySeedToWorldDataAndGenSettings(Object worldDataAndGenSettings, long seed) throws ReflectiveOperationException {
|
||||
Method genSettingsMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "genSettings", method -> method.getParameterCount() == 0);
|
||||
Method dataMethod = CapabilityResolution.resolveMethod(worldDataAndGenSettings.getClass(), "data", method -> method.getParameterCount() == 0);
|
||||
if (genSettingsMethod == null || dataMethod == null) {
|
||||
throw new IllegalStateException("WorldDataAndGenSettings does not expose data()/genSettings().");
|
||||
}
|
||||
|
||||
Object genSettings = genSettingsMethod.invoke(worldDataAndGenSettings);
|
||||
Method optionsMethod = CapabilityResolution.resolveMethod(genSettings.getClass(), "options", method -> method.getParameterCount() == 0);
|
||||
Method dimensionsMethod = CapabilityResolution.resolveMethod(genSettings.getClass(), "dimensions", method -> method.getParameterCount() == 0);
|
||||
if (optionsMethod == null || dimensionsMethod == null) {
|
||||
throw new IllegalStateException("WorldGenSettings does not expose options()/dimensions().");
|
||||
}
|
||||
|
||||
Object options = optionsMethod.invoke(genSettings);
|
||||
Method seedMethod = CapabilityResolution.resolveMethod(options.getClass(), "seed", method -> method.getParameterCount() == 0 && long.class.equals(method.getReturnType()));
|
||||
Method withSeedMethod = CapabilityResolution.resolveMethod(options.getClass(), "withSeed", method -> {
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
return params.length == 1 && OptionalLong.class.equals(params[0]);
|
||||
});
|
||||
if (seedMethod == null || withSeedMethod == null) {
|
||||
throw new IllegalStateException("WorldOptions does not expose seed()/withSeed(OptionalLong).");
|
||||
}
|
||||
|
||||
long currentSeed = (long) seedMethod.invoke(options);
|
||||
if (currentSeed == seed) {
|
||||
return worldDataAndGenSettings;
|
||||
}
|
||||
|
||||
Object newOptions = withSeedMethod.invoke(options, OptionalLong.of(seed));
|
||||
Object newGenSettings = construct(genSettings.getClass(), newOptions, dimensionsMethod.invoke(genSettings));
|
||||
return construct(worldDataAndGenSettings.getClass(), dataMethod.invoke(worldDataAndGenSettings), newGenSettings);
|
||||
}
|
||||
|
||||
private static Object construct(Class<?> type, Object first, Object second) throws ReflectiveOperationException {
|
||||
for (Constructor<?> constructor : type.getDeclaredConstructors()) {
|
||||
Class<?>[] params = constructor.getParameterTypes();
|
||||
if (params.length != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((first == null || params[0].isInstance(first)) && (second == null || params[1].isInstance(second))) {
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(first, second);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No compatible two-argument constructor on " + type.getName());
|
||||
}
|
||||
|
||||
static Object createCurrentWorldDataAndSettings(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
|
||||
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
|
||||
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), capabilities.minecraftServer());
|
||||
|
||||
@@ -26,6 +26,7 @@ import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -167,6 +168,14 @@ public interface INMSBinding {
|
||||
|
||||
MCAPaletteAccess createPalette();
|
||||
|
||||
default boolean applyChunkBlocks(Chunk chunk, TerrainChunk data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean clearChunkBlocks(Chunk chunk) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
|
||||
|
||||
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
|
||||
|
||||
@@ -19,14 +19,15 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@@ -118,20 +119,24 @@ public final class ChunkClearer {
|
||||
}
|
||||
}
|
||||
|
||||
int minHeight = world.getMinHeight();
|
||||
int maxHeight = world.getMaxHeight();
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = minHeight; y < maxHeight; y++) {
|
||||
Block live = chunk.getBlock(x, y, z);
|
||||
if (live.getType() != Material.AIR) {
|
||||
live.setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!INMS.get().clearChunkBlocks(chunk)) {
|
||||
clearBlocks(chunk, chunk.getChunkSnapshot(true, false, false), world.getMinHeight(), world.getMaxHeight());
|
||||
}
|
||||
|
||||
mantle.deleteChunk(chunkX, chunkZ);
|
||||
world.refreshChunk(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
static void clearBlocks(Chunk chunk, ChunkSnapshot snapshot, int minHeight, int maxHeight) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int top = Math.min(maxHeight - 1, snapshot.getHighestBlockYAt(x, z) + 1);
|
||||
for (int y = minHeight; y <= top; y++) {
|
||||
if (snapshot.getBlockType(x, y, z) != Material.AIR) {
|
||||
chunk.getBlock(x, y, z).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,20 +19,23 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@@ -102,37 +105,40 @@ public final class InPlaceChunkRegenerator {
|
||||
int chunkX = target[0];
|
||||
int chunkZ = target[1];
|
||||
|
||||
TerrainChunk buffer = TerrainChunk.create(world);
|
||||
try {
|
||||
engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
reporter.countApplied(false);
|
||||
allApplied.countDown();
|
||||
continue;
|
||||
}
|
||||
|
||||
inFlight.acquire();
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||
boolean ok = false;
|
||||
MultiBurst.burst.lazy(() -> {
|
||||
TerrainChunk buffer = TerrainChunk.create(world);
|
||||
try {
|
||||
applyToLiveChunk(chunkX, chunkZ, buffer);
|
||||
ok = true;
|
||||
engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
} finally {
|
||||
reporter.countApplied(ok);
|
||||
reporter.countApplied(false);
|
||||
inFlight.release();
|
||||
allApplied.countDown();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
||||
boolean ok = false;
|
||||
try {
|
||||
applyToLiveChunk(chunkX, chunkZ, buffer);
|
||||
ok = true;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
} finally {
|
||||
reporter.countApplied(ok);
|
||||
inFlight.release();
|
||||
allApplied.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
if (!scheduled) {
|
||||
Iris.warn("Regen could not schedule chunk apply at " + chunkX + "," + chunkZ + " in " + world.getName() + ".");
|
||||
reporter.countApplied(false);
|
||||
inFlight.release();
|
||||
allApplied.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
if (!scheduled) {
|
||||
Iris.warn("Regen could not schedule chunk apply at " + chunkX + "," + chunkZ + " in " + world.getName() + ".");
|
||||
reporter.countApplied(false);
|
||||
inFlight.release();
|
||||
allApplied.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
allApplied.await();
|
||||
@@ -148,20 +154,8 @@ public final class InPlaceChunkRegenerator {
|
||||
|
||||
int minHeight = world.getMinHeight();
|
||||
int maxHeight = world.getMaxHeight();
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = minHeight; y < maxHeight; y++) {
|
||||
BlockData data = buffer.getBlockData(x, y, z);
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
Block live = chunk.getBlock(x, y, z);
|
||||
if (data.getMaterial() == Material.AIR && live.getType() == Material.AIR) {
|
||||
continue;
|
||||
}
|
||||
live.setBlockData(data, false);
|
||||
}
|
||||
}
|
||||
if (!INMS.get().applyChunkBlocks(chunk, buffer)) {
|
||||
applyBlockDiffs(chunk, chunk.getChunkSnapshot(false, false, false), buffer.getChunkData(), minHeight, maxHeight);
|
||||
}
|
||||
|
||||
int baseX = chunkX << 4;
|
||||
@@ -170,7 +164,7 @@ public final class InPlaceChunkRegenerator {
|
||||
for (int z = 0; z < 16; z += BIOME_STEP) {
|
||||
for (int y = minHeight; y < maxHeight; y += BIOME_STEP) {
|
||||
Biome biome = buffer.getBiome(x, y, z);
|
||||
if (biome != null) {
|
||||
if (biome != null && world.getBiome(baseX + x, y, baseZ + z) != biome) {
|
||||
world.setBiome(baseX + x, y, baseZ + z, biome);
|
||||
}
|
||||
}
|
||||
@@ -180,4 +174,27 @@ public final class InPlaceChunkRegenerator {
|
||||
engine.updateChunk(chunk);
|
||||
world.refreshChunk(chunkX, chunkZ);
|
||||
}
|
||||
|
||||
static void applyBlockDiffs(Chunk chunk, ChunkSnapshot snapshot, ChunkData generated, int minHeight, int maxHeight) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int y = minHeight; y < maxHeight; y++) {
|
||||
Material target = generated.getType(x, y, z);
|
||||
if (snapshot.getBlockType(x, y, z) != target) {
|
||||
chunk.getBlock(x, y, z).setBlockData(generated.getBlockData(x, y, z), false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target == Material.AIR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockData data = generated.getBlockData(x, y, z);
|
||||
if (!data.equals(snapshot.getBlockData(x, y, z))) {
|
||||
chunk.getBlock(x, y, z).setBlockData(data, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,12 @@ public final class StudioOpenCoordinator {
|
||||
}
|
||||
|
||||
updateStage(request, "apply_world_rules", 0.72D);
|
||||
WorldRuntimeControlService.get().applyStudioWorldRules(world);
|
||||
final World rulesWorld = world;
|
||||
CompletableFuture<Boolean> rulesApplied =
|
||||
J.sfut(() -> WorldRuntimeControlService.get().applyStudioWorldRules(rulesWorld));
|
||||
if (rulesApplied != null) {
|
||||
rulesApplied.get(15L, TimeUnit.SECONDS);
|
||||
}
|
||||
t = logStudioPhase("applyStudioWorldRules", t, openStart);
|
||||
|
||||
updateStage(request, "prepare_generator", 0.78D);
|
||||
@@ -157,7 +162,12 @@ public final class StudioOpenCoordinator {
|
||||
throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online.");
|
||||
}
|
||||
|
||||
Boolean teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(10L, TimeUnit.SECONDS);
|
||||
Boolean teleported;
|
||||
try {
|
||||
teleported = WorldRuntimeControlService.get().teleport(player, safeEntry).get(60L, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
throw new IllegalStateException("Studio teleport timed out — destination region may still be generating.");
|
||||
}
|
||||
if (!Boolean.TRUE.equals(teleported)) {
|
||||
throw new IllegalStateException("Studio teleport did not complete successfully.");
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ public final class WorldRuntimeControlService {
|
||||
}
|
||||
|
||||
Iris.linkMultiverseCore.removeFromConfig(world);
|
||||
setIntGameRule(world, 0, "SPAWN_CHUNK_RADIUS", "spawnChunkRadius");
|
||||
if (!IrisSettings.get().getStudio().isDisableTimeAndWeather()) {
|
||||
return true;
|
||||
}
|
||||
@@ -247,26 +248,30 @@ public final class WorldRuntimeControlService {
|
||||
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
boolean scheduled = J.runEntity(player, () -> {
|
||||
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(player, location);
|
||||
if (teleportFuture == null) {
|
||||
future.complete(false);
|
||||
return;
|
||||
try {
|
||||
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);
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
future.completeExceptionally(t);
|
||||
}
|
||||
|
||||
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() + "."));
|
||||
|
||||
@@ -74,7 +74,7 @@ public class StudioSVC implements IrisService {
|
||||
Iris.info("Downloading Default Pack " + pack + " (latest on master)");
|
||||
Iris.service(StudioSVC.class).downloadBranch(Iris.getSender(), "IrisDimensions/overworld", "master", false);
|
||||
} else {
|
||||
Iris.warn("Default pack '" + pack + "' is not installed. Please download it manually with /iris download");
|
||||
Iris.warn("Default pack '" + pack + "' is not installed. Please download it manually with /iris download " + pack);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -216,7 +216,7 @@ public class StudioSVC implements IrisService {
|
||||
|
||||
if (url == null) {
|
||||
sender.sendMessage("Pack '" + key + "' was not found in the pack listing.");
|
||||
sender.sendMessage("Use /iris download <user/repo> <branch> to download manually.");
|
||||
sender.sendMessage("Use /iris download <pack> branch=<branch> to download manually.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ public class StudioSVC implements IrisService {
|
||||
if (zip == null || !zip.exists()) {
|
||||
sender.sendMessage("Failed to find pack at " + url);
|
||||
sender.sendMessage("Make sure you specified the correct repo and branch!");
|
||||
sender.sendMessage("For example: /iris download IrisDimensions/overworld branch=stable");
|
||||
sender.sendMessage("For example: /iris download overworld branch=stable");
|
||||
return;
|
||||
}
|
||||
sender.sendMessage("Unpacking " + repo);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class PaperLibBootstrapTest {
|
||||
@Test
|
||||
public void isModernVersionSchemeAcceptsMajorVersionAboveOne() {
|
||||
assertTrue(PaperLibBootstrap.isModernVersionScheme("26.1.2-R0.1-SNAPSHOT"));
|
||||
assertTrue(PaperLibBootstrap.isModernVersionScheme("26.1.2"));
|
||||
assertTrue(PaperLibBootstrap.isModernVersionScheme("27.0.0-R0.1-SNAPSHOT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isModernVersionSchemeRejectsLegacyScheme() {
|
||||
assertFalse(PaperLibBootstrap.isModernVersionScheme("1.21.4-R0.1-SNAPSHOT"));
|
||||
assertFalse(PaperLibBootstrap.isModernVersionScheme("1.13.2-R0.1-SNAPSHOT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isModernVersionSchemeRejectsUnparsableInput() {
|
||||
assertFalse(PaperLibBootstrap.isModernVersionScheme(null));
|
||||
assertFalse(PaperLibBootstrap.isModernVersionScheme(""));
|
||||
assertFalse(PaperLibBootstrap.isModernVersionScheme("unknown"));
|
||||
assertFalse(PaperLibBootstrap.isModernVersionScheme("-R0.1-SNAPSHOT"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package art.arcane.iris.core.lifecycle;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class WorldLifecycleSeedTest {
|
||||
@Test
|
||||
public void appliesRequestedSeedToFreshWorldData() throws Exception {
|
||||
TestWorldOptions options = new TestWorldOptions(111L, true, false);
|
||||
Object dimensions = new Object();
|
||||
TestWorldGenSettings genSettings = new TestWorldGenSettings(options, dimensions);
|
||||
Object data = new Object();
|
||||
TestWorldDataAndGenSettings input = new TestWorldDataAndGenSettings(data, genSettings);
|
||||
|
||||
Object result = WorldLifecycleSupport.applySeedToWorldDataAndGenSettings(input, 69420L);
|
||||
|
||||
assertTrue(result instanceof TestWorldDataAndGenSettings);
|
||||
TestWorldDataAndGenSettings typed = (TestWorldDataAndGenSettings) result;
|
||||
assertSame(data, typed.data());
|
||||
assertSame(dimensions, typed.genSettings().dimensions());
|
||||
assertEquals(69420L, typed.genSettings().options().seed());
|
||||
assertTrue(typed.genSettings().options().generateStructures());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void returnsSameInstanceWhenSeedAlreadyMatches() throws Exception {
|
||||
TestWorldOptions options = new TestWorldOptions(69420L, true, false);
|
||||
TestWorldGenSettings genSettings = new TestWorldGenSettings(options, new Object());
|
||||
TestWorldDataAndGenSettings input = new TestWorldDataAndGenSettings(new Object(), genSettings);
|
||||
|
||||
Object result = WorldLifecycleSupport.applySeedToWorldDataAndGenSettings(input, 69420L);
|
||||
|
||||
assertSame(input, result);
|
||||
}
|
||||
|
||||
public static final class TestWorldDataAndGenSettings {
|
||||
private final Object data;
|
||||
private final TestWorldGenSettings genSettings;
|
||||
|
||||
public TestWorldDataAndGenSettings(Object data, TestWorldGenSettings genSettings) {
|
||||
this.data = data;
|
||||
this.genSettings = genSettings;
|
||||
}
|
||||
|
||||
public Object data() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public TestWorldGenSettings genSettings() {
|
||||
return genSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TestWorldGenSettings {
|
||||
private final TestWorldOptions options;
|
||||
private final Object dimensions;
|
||||
|
||||
public TestWorldGenSettings(TestWorldOptions options, Object dimensions) {
|
||||
this.options = options;
|
||||
this.dimensions = dimensions;
|
||||
}
|
||||
|
||||
public TestWorldOptions options() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public Object dimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class TestWorldOptions {
|
||||
private final long seed;
|
||||
private final boolean generateStructures;
|
||||
private final boolean generateBonusChest;
|
||||
|
||||
public TestWorldOptions(long seed, boolean generateStructures, boolean generateBonusChest) {
|
||||
this.seed = seed;
|
||||
this.generateStructures = generateStructures;
|
||||
this.generateBonusChest = generateBonusChest;
|
||||
}
|
||||
|
||||
public long seed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public boolean generateStructures() {
|
||||
return generateStructures;
|
||||
}
|
||||
|
||||
public boolean generateBonusChest() {
|
||||
return generateBonusChest;
|
||||
}
|
||||
|
||||
public TestWorldOptions withSeed(OptionalLong newSeed) {
|
||||
return new TestWorldOptions(newSeed.orElse(seed), generateStructures, generateBonusChest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,30 @@ public class WorldLifecycleSelectionTest {
|
||||
assertEquals("bukkit_public", service.selectCreateBackend(request).backendName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistentCreateSelectsPaperLikeBackendOnFolia() {
|
||||
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, true));
|
||||
WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE);
|
||||
|
||||
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistentCreateSelectsPaperLikeBackendOnCanvas() {
|
||||
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.CANVAS, true, false, true));
|
||||
WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE);
|
||||
|
||||
assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistentCreateFallsBackToBukkitBackendOnFoliaWithoutRuntime() {
|
||||
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, false));
|
||||
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));
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ChunkClearerBlocksTest {
|
||||
private static final int MIN_HEIGHT = 0;
|
||||
private static final int MAX_HEIGHT = 16;
|
||||
|
||||
@Test
|
||||
public void clearBlocksOnlyIteratesUpToColumnHeightmap() {
|
||||
ChunkSnapshot snapshot = mock(ChunkSnapshot.class);
|
||||
when(snapshot.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(3);
|
||||
when(snapshot.getBlockType(anyInt(), anyInt(), anyInt())).thenReturn(Material.STONE);
|
||||
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
Block block = mock(Block.class);
|
||||
when(chunk.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(block);
|
||||
|
||||
ChunkClearer.clearBlocks(chunk, snapshot, MIN_HEIGHT, MAX_HEIGHT);
|
||||
|
||||
verify(block, times(16 * 16 * 5)).setType(Material.AIR, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearBlocksSkipsAirPositions() {
|
||||
ChunkSnapshot snapshot = mock(ChunkSnapshot.class);
|
||||
when(snapshot.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(2);
|
||||
when(snapshot.getBlockType(anyInt(), anyInt(), anyInt())).thenReturn(Material.AIR);
|
||||
when(snapshot.getBlockType(4, 1, 9)).thenReturn(Material.STONE);
|
||||
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
Block block = mock(Block.class);
|
||||
when(chunk.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(block);
|
||||
|
||||
ChunkClearer.clearBlocks(chunk, snapshot, MIN_HEIGHT, MAX_HEIGHT);
|
||||
|
||||
verify(chunk, times(1)).getBlock(4, 1, 9);
|
||||
verify(block, times(1)).setType(Material.AIR, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearBlocksHandlesEmptyColumns() {
|
||||
ChunkSnapshot snapshot = mock(ChunkSnapshot.class);
|
||||
when(snapshot.getHighestBlockYAt(anyInt(), anyInt())).thenReturn(MIN_HEIGHT - 1);
|
||||
when(snapshot.getBlockType(anyInt(), anyInt(), anyInt())).thenReturn(Material.AIR);
|
||||
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
Block block = mock(Block.class);
|
||||
when(chunk.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(block);
|
||||
|
||||
ChunkClearer.clearBlocks(chunk, snapshot, MIN_HEIGHT, MAX_HEIGHT);
|
||||
|
||||
verify(block, never()).setType(Material.AIR, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class InPlaceChunkRegeneratorDiffTest {
|
||||
private static final int MIN_HEIGHT = 0;
|
||||
private static final int MAX_HEIGHT = 8;
|
||||
|
||||
@Test
|
||||
public void applyBlockDiffsSkipsAllWritesWhenChunkMatchesBuffer() {
|
||||
BlockData stone = mock(BlockData.class);
|
||||
ChunkSnapshot snapshot = mock(ChunkSnapshot.class);
|
||||
when(snapshot.getBlockType(anyInt(), anyInt(), anyInt())).thenReturn(Material.STONE);
|
||||
when(snapshot.getBlockData(anyInt(), anyInt(), anyInt())).thenReturn(stone);
|
||||
|
||||
ChunkData generated = mock(ChunkData.class);
|
||||
when(generated.getType(anyInt(), anyInt(), anyInt())).thenReturn(Material.STONE);
|
||||
when(generated.getBlockData(anyInt(), anyInt(), anyInt())).thenReturn(stone);
|
||||
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
Block block = mock(Block.class);
|
||||
when(chunk.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(block);
|
||||
|
||||
InPlaceChunkRegenerator.applyBlockDiffs(chunk, snapshot, generated, MIN_HEIGHT, MAX_HEIGHT);
|
||||
|
||||
verify(block, never()).setBlockData(eq(stone), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyBlockDiffsWritesOnlyMaterialChanges() {
|
||||
BlockData stone = mock(BlockData.class);
|
||||
BlockData dirt = mock(BlockData.class);
|
||||
ChunkSnapshot snapshot = mock(ChunkSnapshot.class);
|
||||
when(snapshot.getBlockType(anyInt(), anyInt(), anyInt())).thenReturn(Material.STONE);
|
||||
when(snapshot.getBlockData(anyInt(), anyInt(), anyInt())).thenReturn(stone);
|
||||
|
||||
ChunkData generated = mock(ChunkData.class);
|
||||
when(generated.getType(anyInt(), anyInt(), anyInt())).thenReturn(Material.STONE);
|
||||
when(generated.getBlockData(anyInt(), anyInt(), anyInt())).thenReturn(stone);
|
||||
when(generated.getType(3, 5, 7)).thenReturn(Material.DIRT);
|
||||
when(generated.getBlockData(3, 5, 7)).thenReturn(dirt);
|
||||
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
Block block = mock(Block.class);
|
||||
when(chunk.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(block);
|
||||
|
||||
InPlaceChunkRegenerator.applyBlockDiffs(chunk, snapshot, generated, MIN_HEIGHT, MAX_HEIGHT);
|
||||
|
||||
verify(chunk, times(1)).getBlock(3, 5, 7);
|
||||
verify(block, times(1)).setBlockData(dirt, false);
|
||||
verify(block, never()).setBlockData(eq(stone), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyBlockDiffsWritesWhenMaterialMatchesButStateDiffers() {
|
||||
BlockData liveStairs = mock(BlockData.class);
|
||||
BlockData rotatedStairs = mock(BlockData.class);
|
||||
ChunkSnapshot snapshot = mock(ChunkSnapshot.class);
|
||||
when(snapshot.getBlockType(anyInt(), anyInt(), anyInt())).thenReturn(Material.OAK_STAIRS);
|
||||
when(snapshot.getBlockData(anyInt(), anyInt(), anyInt())).thenReturn(liveStairs);
|
||||
|
||||
ChunkData generated = mock(ChunkData.class);
|
||||
when(generated.getType(anyInt(), anyInt(), anyInt())).thenReturn(Material.OAK_STAIRS);
|
||||
when(generated.getBlockData(anyInt(), anyInt(), anyInt())).thenReturn(liveStairs);
|
||||
when(generated.getBlockData(1, 2, 3)).thenReturn(rotatedStairs);
|
||||
|
||||
Chunk chunk = mock(Chunk.class);
|
||||
Block block = mock(Block.class);
|
||||
when(chunk.getBlock(anyInt(), anyInt(), anyInt())).thenReturn(block);
|
||||
|
||||
InPlaceChunkRegenerator.applyBlockDiffs(chunk, snapshot, generated, MIN_HEIGHT, MAX_HEIGHT);
|
||||
|
||||
verify(block, times(1)).setBlockData(rotatedStairs, false);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.data.chunk.TerrainChunk;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
@@ -40,6 +41,7 @@ import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.commands.data.BlockDataAccessor;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ThreadedLevelLightEngine;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
@@ -54,11 +56,13 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
||||
@@ -78,6 +82,7 @@ import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.block.CraftBlockState;
|
||||
import org.bukkit.craftbukkit.block.CraftBlockStates;
|
||||
import org.bukkit.craftbukkit.block.data.CraftBlockData;
|
||||
import org.bukkit.craftbukkit.generator.CraftChunkData;
|
||||
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
||||
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
|
||||
@@ -718,6 +723,100 @@ public class NMSBinding implements INMSBinding {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applyChunkBlocks(Chunk bukkitChunk, TerrainChunk data) {
|
||||
if (!(data.getChunkData() instanceof CraftChunkData chunkData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
ServerLevel level = ((CraftWorld) bukkitChunk.getWorld()).getHandle();
|
||||
LevelChunk chunk = level.getChunk(bukkitChunk.getX(), bukkitChunk.getZ());
|
||||
ChunkAccess source = chunkData.getHandle();
|
||||
removeBlockEntities(chunk);
|
||||
|
||||
int minY = level.getMinY();
|
||||
int baseX = chunk.getPos().getMinBlockX();
|
||||
int baseZ = chunk.getPos().getMinBlockZ();
|
||||
for (int i = 0; i < chunk.getSectionsCount(); i++) {
|
||||
LevelChunkSection target = chunk.getSection(i);
|
||||
LevelChunkSection from = source.getSection(i);
|
||||
if (from.hasOnlyAir() && target.hasOnlyAir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int sectionBaseY = minY + (i << 4);
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
BlockState state = from.getBlockState(x, y, z);
|
||||
target.setBlockState(x, y, z, state, false);
|
||||
if (state.hasBlockEntity() && state.getBlock() instanceof EntityBlock entityBlock) {
|
||||
BlockPos pos = new BlockPos(baseX + x, sectionBaseY + y, baseZ + z);
|
||||
BlockEntity entity = entityBlock.newBlockEntity(pos, state);
|
||||
if (entity != null) {
|
||||
chunk.setBlockEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finishChunkRewrite(level, chunk);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearChunkBlocks(Chunk bukkitChunk) {
|
||||
try {
|
||||
ServerLevel level = ((CraftWorld) bukkitChunk.getWorld()).getHandle();
|
||||
LevelChunk chunk = level.getChunk(bukkitChunk.getX(), bukkitChunk.getZ());
|
||||
removeBlockEntities(chunk);
|
||||
|
||||
BlockState air = ((CraftBlockData) AIR).getState();
|
||||
for (int i = 0; i < chunk.getSectionsCount(); i++) {
|
||||
LevelChunkSection section = chunk.getSection(i);
|
||||
if (section.hasOnlyAir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
section.setBlockState(x, y, z, air, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finishChunkRewrite(level, chunk);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void removeBlockEntities(LevelChunk chunk) {
|
||||
for (BlockPos pos : new ArrayList<>(chunk.getBlockEntities().keySet())) {
|
||||
chunk.removeBlockEntity(pos);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishChunkRewrite(ServerLevel level, LevelChunk chunk) {
|
||||
Heightmap.primeHeightmaps(chunk, ChunkStatus.FULL.heightmapsAfter());
|
||||
chunk.markUnsaved();
|
||||
ThreadedLevelLightEngine lightEngine = (ThreadedLevelLightEngine) level.getChunkSource().getLightEngine();
|
||||
lightEngine.starlight$serverRelightChunks(List.of(chunk.getPos()), p -> {
|
||||
}, c -> {
|
||||
});
|
||||
}
|
||||
|
||||
public ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException {
|
||||
if (customNbt != null && !customNbt.isEmpty()) {
|
||||
net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack);
|
||||
|
||||
Reference in New Issue
Block a user