mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
nms patches
This commit is contained in:
1 parent
a82b8f4a9c
commit
a530a03459
20 files changed
+708
-229
No files matched your search
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.IrisWorlds;
|
||||||
import art.arcane.iris.core.ServerConfigurator;
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
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.lifecycle.WorldLifecycleService;
|
||||||
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
import art.arcane.iris.core.runtime.TransientWorldCleanupSupport;
|
||||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||||
@@ -545,6 +546,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void enable() {
|
private void enable() {
|
||||||
|
PaperLibBootstrap.install();
|
||||||
services = new KMap<>();
|
services = new KMap<>();
|
||||||
setupAudience();
|
setupAudience();
|
||||||
Bindings.setupSentry();
|
Bindings.setupSentry();
|
||||||
@@ -575,7 +577,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
WorldRuntimeControlService.get();
|
WorldRuntimeControlService.get();
|
||||||
|
|
||||||
if (J.isFolia()) {
|
if (J.isFolia()) {
|
||||||
checkForBukkitWorlds(s -> true);
|
J.s(() -> checkForBukkitWorlds(s -> true), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
J.s(() -> {
|
J.s(() -> {
|
||||||
@@ -641,6 +643,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
WorldCreator c = new WorldCreator(s)
|
WorldCreator c = new WorldCreator(s)
|
||||||
.generator(gen)
|
.generator(gen)
|
||||||
.environment(dim.getEnvironment());
|
.environment(dim.getEnvironment());
|
||||||
|
Long stagedSeed = IrisWorlds.readBukkitWorldSeed(s);
|
||||||
|
if (stagedSeed != null) {
|
||||||
|
c.seed(stagedSeed);
|
||||||
|
}
|
||||||
INMS.get().createWorld(c);
|
INMS.get().createWorld(c);
|
||||||
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
|
Iris.info(C.LIGHT_PURPLE + "Loaded " + s + "!");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
@@ -661,8 +667,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!deferredStartupWorlds.isEmpty()) {
|
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("Staged Iris worlds could not load on Folia: %s", String.join(", ", deferredStartupWorlds));
|
||||||
Iris.warn("Bukkit.createWorld is intentionally unavailable in this startup phase. Worlds remain staged in bukkit.yml.");
|
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) {
|
} catch (Throwable e) {
|
||||||
reportError("Failed while loading startup Iris worlds.", 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.volmlib.util.io.IO;
|
||||||
import art.arcane.iris.util.common.misc.ServerProperties;
|
import art.arcane.iris.util.common.misc.ServerProperties;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
import java.io.File;
|
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() {
|
public static KMap<String, String> readBukkitWorlds() {
|
||||||
var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
var bukkit = YamlConfiguration.loadConfiguration(ServerProperties.BUKKIT_YML);
|
||||||
var worlds = bukkit.getConfigurationSection("worlds");
|
var worlds = bukkit.getConfigurationSection("worlds");
|
||||||
|
|||||||
@@ -219,7 +219,12 @@ public class CommandDeveloper implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Test")
|
@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 base = Iris.instance.getDataFile("dump", "pv." + name + ".ttp.lz4b.bin");
|
||||||
var section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
|
var section = Iris.instance.getDataFile("dump", "pv." + name + ".section.bin");
|
||||||
|
|
||||||
@@ -256,7 +261,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
|||||||
|
|
||||||
@Director(description = "test")
|
@Director(description = "test")
|
||||||
public void mca (
|
public void mca (
|
||||||
@Param(description = "String") String world) {
|
@Param(description = "The world folder to scan for .mca region files") String world) {
|
||||||
try {
|
try {
|
||||||
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
|
File[] McaFiles = new File(world, "region").listFiles((dir, name) -> name.endsWith(".mca"));
|
||||||
for (File mca : McaFiles) {
|
for (File mca : McaFiles) {
|
||||||
|
|||||||
@@ -115,11 +115,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
@Param(description = "The seed to generate the world with", defaultValue = "1337")
|
@Param(description = "The seed to generate the world with", defaultValue = "1337")
|
||||||
long seed,
|
long seed,
|
||||||
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
|
@Param(aliases = "main-world", description = "Whether or not to automatically use this world as the main world", defaultValue = "false")
|
||||||
boolean main,
|
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
|
|
||||||
) {
|
) {
|
||||||
if (name.equalsIgnoreCase("iris")) {
|
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.");
|
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) {
|
if (dimension == null) {
|
||||||
sender().sendMessage(C.RED + "Could not find or download dimension \"" + resolvedType + "\".");
|
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 + "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;
|
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 (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 + "\".");
|
sender().sendMessage(C.GREEN + "World staging completed. Restart the server to generate/load \"" + name + "\".");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -185,11 +175,6 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (main && !applyMainWorldCleanup(name, removeOthers, removeWorlds)) {
|
|
||||||
worldCreation = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
worldCreation = false;
|
worldCreation = false;
|
||||||
sender().sendMessage(C.GREEN + "Successfully created your world!");
|
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.");
|
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 + "Runtime world creation is disabled on Folia.");
|
||||||
sender().sendMessage(C.YELLOW + "Preparing world files and bukkit.yml for next startup...");
|
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.");
|
sender().sendMessage(C.RED + "World was staged, but failed to update server.properties main world.");
|
||||||
return false;
|
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 + ".");
|
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)
|
@Director(description = "Teleport to another world", aliases = {"tp"}, sync = true)
|
||||||
public void teleport(
|
public void teleport(
|
||||||
@Param(description = "World to teleport to")
|
@Param(description = "World to teleport to")
|
||||||
@@ -595,7 +455,7 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
|
|
||||||
@Director(description = "Download a project.", aliases = "dl")
|
@Director(description = "Download a project.", aliases = "dl")
|
||||||
public void download(
|
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,
|
String pack,
|
||||||
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
|
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
|
||||||
String branch,
|
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
|
@Override
|
||||||
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
public boolean supports(WorldLifecycleRequest request, CapabilitySnapshot capabilities) {
|
||||||
return request.studio()
|
if (!capabilities.serverFamily().isPaperLike() || !capabilities.hasPaperLikeRuntime()) {
|
||||||
&& capabilities.serverFamily().isPaperLike()
|
return false;
|
||||||
&& capabilities.hasPaperLikeRuntime();
|
}
|
||||||
|
|
||||||
|
if (request.studio()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return capabilities.serverFamily() == ServerFamily.FOLIA || capabilities.regionizedRuntime();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -50,6 +56,9 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend {
|
|||||||
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, !request.studio());
|
Object worldLoadingInfo = capabilities.worldLoadingInfoConstructor().newInstance(request.environment(), stemKey, dimensionKey, !request.studio());
|
||||||
Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData);
|
Object worldLoadingInfoAndData = capabilities.worldLoadingInfoAndDataConstructor().newInstance(worldLoadingInfo, loadedWorldData);
|
||||||
Object worldDataAndGenSettings = WorldLifecycleSupport.createCurrentWorldDataAndSettings(capabilities, request.worldName());
|
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);
|
capabilities.createLevelMethod().invoke(capabilities.minecraftServer(), levelStem, worldLoadingInfoAndData, worldDataAndGenSettings);
|
||||||
} else {
|
} else {
|
||||||
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName());
|
legacyStorageAccess = WorldLifecycleSupport.createLegacyStorageAccess(capabilities, request.worldName());
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.bukkit.configuration.file.YamlConfiguration;
|
|||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@@ -21,6 +22,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalLong;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -252,6 +254,63 @@ final class WorldLifecycleSupport {
|
|||||||
setModdedInfoMethod.invoke(worldData, modName, modified);
|
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 {
|
static Object createCurrentWorldDataAndSettings(CapabilitySnapshot capabilities, String worldName) throws ReflectiveOperationException {
|
||||||
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
|
Object settings = read(capabilities.settingsField(), capabilities.minecraftServer());
|
||||||
Object worldLoaderContext = read(capabilities.worldLoaderContextField(), 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.container.BlockProperty;
|
||||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
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.framework.Engine;
|
||||||
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;
|
||||||
@@ -167,6 +168,14 @@ public interface INMSBinding {
|
|||||||
|
|
||||||
MCAPaletteAccess createPalette();
|
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);
|
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
|
||||||
|
|
||||||
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
|
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
|
||||||
|
|||||||
@@ -19,14 +19,15 @@
|
|||||||
package art.arcane.iris.core.runtime;
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
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.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
import org.bukkit.Chunk;
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.ChunkSnapshot;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Block;
|
|
||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
@@ -118,20 +119,24 @@ public final class ChunkClearer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int minHeight = world.getMinHeight();
|
if (!INMS.get().clearChunkBlocks(chunk)) {
|
||||||
int maxHeight = world.getMaxHeight();
|
clearBlocks(chunk, chunk.getChunkSnapshot(true, false, false), world.getMinHeight(), 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mantle.deleteChunk(chunkX, chunkZ);
|
mantle.deleteChunk(chunkX, chunkZ);
|
||||||
world.refreshChunk(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;
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
import art.arcane.iris.Iris;
|
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.data.chunk.TerrainChunk;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
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.plugin.VolmitSender;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
import org.bukkit.Chunk;
|
import org.bukkit.Chunk;
|
||||||
|
import org.bukkit.ChunkSnapshot;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Biome;
|
import org.bukkit.block.Biome;
|
||||||
import org.bukkit.block.Block;
|
|
||||||
import org.bukkit.block.data.BlockData;
|
import org.bukkit.block.data.BlockData;
|
||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
@@ -102,37 +105,40 @@ public final class InPlaceChunkRegenerator {
|
|||||||
int chunkX = target[0];
|
int chunkX = target[0];
|
||||||
int chunkZ = target[1];
|
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();
|
inFlight.acquire();
|
||||||
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
|
MultiBurst.burst.lazy(() -> {
|
||||||
boolean ok = false;
|
TerrainChunk buffer = TerrainChunk.create(world);
|
||||||
try {
|
try {
|
||||||
applyToLiveChunk(chunkX, chunkZ, buffer);
|
engine.generate(chunkX << 4, chunkZ << 4, buffer, false);
|
||||||
ok = true;
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError(e);
|
Iris.reportError(e);
|
||||||
} finally {
|
reporter.countApplied(false);
|
||||||
reporter.countApplied(ok);
|
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();
|
inFlight.release();
|
||||||
allApplied.countDown();
|
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();
|
allApplied.await();
|
||||||
@@ -148,20 +154,8 @@ public final class InPlaceChunkRegenerator {
|
|||||||
|
|
||||||
int minHeight = world.getMinHeight();
|
int minHeight = world.getMinHeight();
|
||||||
int maxHeight = world.getMaxHeight();
|
int maxHeight = world.getMaxHeight();
|
||||||
for (int x = 0; x < 16; x++) {
|
if (!INMS.get().applyChunkBlocks(chunk, buffer)) {
|
||||||
for (int z = 0; z < 16; z++) {
|
applyBlockDiffs(chunk, chunk.getChunkSnapshot(false, false, false), buffer.getChunkData(), minHeight, maxHeight);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int baseX = chunkX << 4;
|
int baseX = chunkX << 4;
|
||||||
@@ -170,7 +164,7 @@ public final class InPlaceChunkRegenerator {
|
|||||||
for (int z = 0; z < 16; z += BIOME_STEP) {
|
for (int z = 0; z < 16; z += BIOME_STEP) {
|
||||||
for (int y = minHeight; y < maxHeight; y += BIOME_STEP) {
|
for (int y = minHeight; y < maxHeight; y += BIOME_STEP) {
|
||||||
Biome biome = buffer.getBiome(x, y, z);
|
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);
|
world.setBiome(baseX + x, y, baseZ + z, biome);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,4 +174,27 @@ public final class InPlaceChunkRegenerator {
|
|||||||
engine.updateChunk(chunk);
|
engine.updateChunk(chunk);
|
||||||
world.refreshChunk(chunkX, chunkZ);
|
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);
|
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);
|
t = logStudioPhase("applyStudioWorldRules", t, openStart);
|
||||||
|
|
||||||
updateStage(request, "prepare_generator", 0.78D);
|
updateStage(request, "prepare_generator", 0.78D);
|
||||||
@@ -157,7 +162,12 @@ public final class StudioOpenCoordinator {
|
|||||||
throw new IllegalStateException("Player \"" + request.playerName() + "\" is not online.");
|
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)) {
|
if (!Boolean.TRUE.equals(teleported)) {
|
||||||
throw new IllegalStateException("Studio teleport did not complete successfully.");
|
throw new IllegalStateException("Studio teleport did not complete successfully.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ public final class WorldRuntimeControlService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Iris.linkMultiverseCore.removeFromConfig(world);
|
Iris.linkMultiverseCore.removeFromConfig(world);
|
||||||
|
setIntGameRule(world, 0, "SPAWN_CHUNK_RADIUS", "spawnChunkRadius");
|
||||||
if (!IrisSettings.get().getStudio().isDisableTimeAndWeather()) {
|
if (!IrisSettings.get().getStudio().isDisableTimeAndWeather()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -247,26 +248,30 @@ public final class WorldRuntimeControlService {
|
|||||||
|
|
||||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
boolean scheduled = J.runEntity(player, () -> {
|
boolean scheduled = J.runEntity(player, () -> {
|
||||||
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(player, location);
|
try {
|
||||||
if (teleportFuture == null) {
|
CompletableFuture<Boolean> teleportFuture = PaperLib.teleportAsync(player, location);
|
||||||
future.complete(false);
|
if (teleportFuture == null) {
|
||||||
return;
|
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) {
|
if (!scheduled) {
|
||||||
return CompletableFuture.failedFuture(new IllegalStateException("Failed to schedule teleport for " + player.getName() + "."));
|
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.info("Downloading Default Pack " + pack + " (latest on master)");
|
||||||
Iris.service(StudioSVC.class).downloadBranch(Iris.getSender(), "IrisDimensions/overworld", "master", false);
|
Iris.service(StudioSVC.class).downloadBranch(Iris.getSender(), "IrisDimensions/overworld", "master", false);
|
||||||
} else {
|
} 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) {
|
if (url == null) {
|
||||||
sender.sendMessage("Pack '" + key + "' was not found in the pack listing.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ public class StudioSVC implements IrisService {
|
|||||||
if (zip == null || !zip.exists()) {
|
if (zip == null || !zip.exists()) {
|
||||||
sender.sendMessage("Failed to find pack at " + url);
|
sender.sendMessage("Failed to find pack at " + url);
|
||||||
sender.sendMessage("Make sure you specified the correct repo and branch!");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
sender.sendMessage("Unpacking " + repo);
|
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());
|
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
|
@Test
|
||||||
public void unloadUsesRememberedBackendFamily() {
|
public void unloadUsesRememberedBackendFamily() {
|
||||||
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true));
|
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.container.BlockProperty;
|
||||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
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.framework.Engine;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.util.project.agent.Agent;
|
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.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.server.level.ThreadedLevelLightEngine;
|
||||||
import net.minecraft.tags.TagKey;
|
import net.minecraft.tags.TagKey;
|
||||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||||
import net.minecraft.world.entity.EntityType;
|
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.block.state.properties.Property;
|
||||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
import net.minecraft.world.level.chunk.LevelChunk;
|
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.ProtoChunk;
|
||||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||||
import net.minecraft.world.level.dimension.LevelStem;
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
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.WorldgenRandom;
|
||||||
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
|
||||||
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
|
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.CraftBlockState;
|
||||||
import org.bukkit.craftbukkit.block.CraftBlockStates;
|
import org.bukkit.craftbukkit.block.CraftBlockStates;
|
||||||
import org.bukkit.craftbukkit.block.data.CraftBlockData;
|
import org.bukkit.craftbukkit.block.data.CraftBlockData;
|
||||||
|
import org.bukkit.craftbukkit.generator.CraftChunkData;
|
||||||
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
||||||
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
||||||
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
|
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 {
|
public ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException {
|
||||||
if (customNbt != null && !customNbt.isEmpty()) {
|
if (customNbt != null && !customNbt.isEmpty()) {
|
||||||
net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack);
|
net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack);
|
||||||
|
|||||||
Reference in new issue
Block a user