Correct Java 26.2 Iris world lifecycle

This commit is contained in:
Brian Neumann-Fopiano
2026-08-15 11:27:46 -04:00
parent 32beca236c
commit 57072d5bcd
61 changed files with 2180 additions and 695 deletions
@@ -14,6 +14,7 @@ import art.arcane.iris.core.nms.container.BiomeColor;
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.core.lifecycle.WorldReplacementSeed;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
@@ -53,6 +54,8 @@ import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.iris.util.common.scheduling.J;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.shorts.ShortList;
import io.papermc.paper.world.saveddata.PaperLevelOverrides;
import io.papermc.paper.world.saveddata.PaperWorldMetadata;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
@@ -116,6 +119,8 @@ import net.minecraft.world.level.levelgen.structure.StructureCheck;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.storage.PrimaryLevelData;
import net.minecraft.world.level.storage.SavedDataStorage;
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
@@ -151,10 +156,14 @@ import org.jetbrains.annotations.NotNull;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@@ -163,11 +172,18 @@ import java.util.Map;
import java.util.Optional;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class NMSBinding implements INMSBinding {
private static final long CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS = 30L;
private final KMap<Biome, Object> baseBiomeCache = new KMap<>();
private volatile DataVersion dataVersion;
private final BlockData AIR = Material.AIR.createBlockData();
@@ -1593,6 +1609,105 @@ public class NMSBinding implements INMSBinding {
}
}
@Override
public void writeCurrentPaperWorldData(
Path sourceWorldDirectory,
Path targetWorldDirectory,
long seed
) throws IOException {
CraftServer craftServer = (CraftServer) Bukkit.getServer();
MinecraftServer server = craftServer.getHandle().getServer();
PaperLevelOverrides levelOverrides = captureCurrentPaperLevelOverrides(craftServer, server);
Path targetWorld = targetWorldDirectory.toAbsolutePath().normalize();
UUID metadataUuid = UUID.randomUUID();
WorldReplacementSeed.copyWithAuthoritativeSeed(sourceWorldDirectory, targetWorld, seed);
try (SavedDataStorage savedDataStorage = new SavedDataStorage(
targetWorld.resolve("data"),
server.getFixerUpper(),
server.registryAccess()
)) {
savedDataStorage.set(PaperWorldMetadata.TYPE, new PaperWorldMetadata(metadataUuid));
savedDataStorage.set(
PaperLevelOverrides.TYPE,
levelOverrides
);
}
List<Path> requiredDataFiles = List.of(
targetWorld.resolve("data/minecraft/world_gen_settings.dat"),
targetWorld.resolve("data/paper/metadata.dat"),
targetWorld.resolve("data/paper/level_overrides.dat")
);
for (Path requiredDataFile : requiredDataFiles) {
if (!Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Current Paper world data was not written: " + requiredDataFile);
}
}
long writtenSeed = WorldReplacementSeed.readAuthoritativeSeed(targetWorld);
if (writtenSeed != seed) {
throw new IOException("Current Paper world data did not retain the requested seed.");
}
try (SavedDataStorage verificationStorage = new SavedDataStorage(
targetWorld.resolve("data"),
server.getFixerUpper(),
server.registryAccess()
)) {
PaperWorldMetadata metadata = verificationStorage.get(PaperWorldMetadata.TYPE);
if (metadata == null || !metadataUuid.equals(metadata.uuid())) {
throw new IOException("Current Paper world metadata could not be verified.");
}
PaperLevelOverrides overrides = verificationStorage.get(PaperLevelOverrides.TYPE);
if (overrides == null || overrides.isInitialized()) {
throw new IOException("Current Paper level overrides could not be verified.");
}
}
}
private PaperLevelOverrides captureCurrentPaperLevelOverrides(
CraftServer craftServer,
MinecraftServer server
) throws IOException {
if (craftServer.isGlobalTickThread()) {
return createCurrentPaperLevelOverrides(server);
}
if (J.isFolia() && J.isPrimaryThread()) {
throw new IOException("Current Paper world data cannot be staged from a Folia region tick thread.");
}
CompletableFuture<PaperLevelOverrides> captured = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
captured.complete(createCurrentPaperLevelOverrides(server));
} catch (Throwable failure) {
captured.completeExceptionally(failure);
}
});
if (!scheduled) {
throw new IOException("Could not schedule the current Paper level-data snapshot on the global thread.");
}
try {
return captured.get(CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while capturing current Paper level data.", failure);
} catch (ExecutionException failure) {
throw new IOException("Could not capture current Paper level data.", failure.getCause());
} catch (TimeoutException failure) {
throw new IOException("Timed out while capturing current Paper level data.", failure);
}
}
private PaperLevelOverrides createCurrentPaperLevelOverrides(MinecraftServer server) throws IOException {
if (!(server.getWorldData().overworldData() instanceof PrimaryLevelData primaryLevelData)) {
throw new IOException("Paper primary level data is unavailable for current world data staging.");
}
return PaperLevelOverrides.createFromLiveLevelData(primaryLevelData);
}
@Override
public KMap<Material, List<BlockProperty>> getBlockProperties() {
KMap<Material, List<BlockProperty>> states = new KMap<>();
@@ -0,0 +1,92 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.core.nms.INMSBinding;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NMSBindingCurrentPaperWorldDataContractTest {
@Test
public void stagesAllCurrentPaperWorldDataFromLiveServerState() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
String writer = section(
source,
"public void writeCurrentPaperWorldData(",
"public KMap<Material, List<BlockProperty>> getBlockProperties()"
);
assertTrue(writer.contains("WorldReplacementSeed.copyWithAuthoritativeSeed("));
assertTrue(writer.contains("UUID metadataUuid = UUID.randomUUID()"));
assertTrue(writer.contains("new PaperWorldMetadata(metadataUuid)"));
assertTrue(writer.contains("captureCurrentPaperLevelOverrides(craftServer, server)"));
assertTrue(writer.contains("new SavedDataStorage("));
assertTrue(writer.contains("server.getFixerUpper()"));
assertTrue(writer.contains("server.registryAccess()"));
assertTrue(writer.contains("data/minecraft/world_gen_settings.dat"));
assertTrue(writer.contains("data/paper/metadata.dat"));
assertTrue(writer.contains("data/paper/level_overrides.dat"));
assertTrue(writer.contains("WorldReplacementSeed.readAuthoritativeSeed(targetWorld)"));
assertTrue(writer.contains("verificationStorage.get(PaperWorldMetadata.TYPE)"));
assertTrue(writer.contains("metadataUuid.equals(metadata.uuid())"));
assertTrue(writer.contains("verificationStorage.get(PaperLevelOverrides.TYPE)"));
assertTrue(writer.contains("overrides == null || overrides.isInitialized()"));
assertTrue(writer.contains("Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)"));
assertFalse(writer.toLowerCase().contains("migrat"));
assertFalse(writer.toLowerCase().contains("fallback"));
}
@Test
public void capturesOnlyLiveLevelOverridesOnTheGlobalThread() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
String capture = section(
source,
"private PaperLevelOverrides captureCurrentPaperLevelOverrides(",
"private PaperLevelOverrides createCurrentPaperLevelOverrides("
);
String create = section(
source,
"private PaperLevelOverrides createCurrentPaperLevelOverrides(",
"public KMap<Material, List<BlockProperty>> getBlockProperties()"
);
assertTrue(capture.contains("craftServer.isGlobalTickThread()"));
assertTrue(capture.contains("J.isFolia() && J.isPrimaryThread()"));
assertTrue(capture.contains("J.runGlobal("));
assertTrue(capture.contains("captured.get(CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS"));
assertTrue(capture.contains("Thread.currentThread().interrupt()"));
assertTrue(create.contains("PaperLevelOverrides.createFromLiveLevelData(primaryLevelData)"));
assertFalse(capture.contains("WorldReplacementSeed"));
assertFalse(capture.contains("SavedDataStorage"));
assertFalse(capture.contains("Files."));
}
@Test
public void unsupportedBindingsRejectCurrentPaperWorldDataStaging() {
INMSBinding binding = (INMSBinding) Proxy.newProxyInstance(
INMSBinding.class.getClassLoader(),
new Class<?>[]{INMSBinding.class},
(proxy, method, arguments) -> InvocationHandler.invokeDefault(proxy, method, arguments)
);
UnsupportedOperationException error = assertThrows(
UnsupportedOperationException.class,
() -> binding.writeCurrentPaperWorldData(Path.of("source"), Path.of("target"), 1L)
);
assertTrue(error.getMessage().contains("does not support current Paper world data staging"));
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue("Missing source section starting with " + startMarker, start >= 0);
assertTrue("Missing source section ending with " + endMarker, end > start);
return source.substring(start, end);
}
}