nms patches

This commit is contained in:
Brian Neumann-Fopiano
2026-06-10 09:00:23 -04:00
parent a82b8f4a9c
commit a530a03459
20 changed files with 708 additions and 229 deletions
+1 -1
View File
@@ -1 +1 @@
-2003345818
203609563
+9 -3
View File
@@ -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);