mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Preserved terrain across updates
- Saved chunks retain their natural geometry and generation ownership - Future chunks blend against frozen terrain boundaries - Build revisions capture generation source and dependencies automatically - Native terrain queries use contained transition geometry and final height caches - Historical searches retain recorded pack identities and placement facts - World creation and Studio updates preserve native lifecycle ordering - Hydrology containment and scoped content workers retain generation context - Pregeneration validates saved completion before trusting cached progress
This commit is contained in:
1 parent
20ad49b9d2
commit
b1e1fbc15e
214 files changed
+11985
-4082
No files matched your search
+86
-11
@@ -1,5 +1,10 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.nativegen.NativeTransitionColumn;
|
||||
import art.arcane.iris.engine.history.TerrainBoundarySignature;
|
||||
import java.util.Optional;
|
||||
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
|
||||
import java.util.function.LongPredicate;
|
||||
import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
@@ -12,6 +17,10 @@ import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.engine.history.SavedTerrainChunk;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
@@ -112,7 +121,6 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -122,7 +130,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator implements LongPredicate {
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private static final Runnable NO_OP = () -> {
|
||||
@@ -413,6 +421,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
));
|
||||
adjustGeneratedStructures(
|
||||
registryAccess, access, previousStarts, configuredStarts, templateManager);
|
||||
if (route != null) {
|
||||
access.persistentDataContainer.set(new NamespacedKey("iris", "structure_activation"),
|
||||
PersistentDataType.LONG, route.activation().activationId());
|
||||
access.markUnsaved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,6 +729,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) {
|
||||
return;
|
||||
}
|
||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||
}
|
||||
}
|
||||
@@ -729,6 +745,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) {
|
||||
return;
|
||||
}
|
||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||
}
|
||||
}
|
||||
@@ -766,6 +785,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope =
|
||||
openHistoryRuntimeScope(route);
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
claimGeneratedSemantics(route, filled, engine.getMinHeight());
|
||||
persistNaturalTerrain(filled, route);
|
||||
primeWorldgenHeightmaps(filled);
|
||||
return filled;
|
||||
}
|
||||
@@ -783,6 +804,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
completion.completeExceptionally(completionFailure);
|
||||
}
|
||||
});
|
||||
lease.detachThread();
|
||||
if (route != null) {
|
||||
route.detachThread();
|
||||
}
|
||||
return completion;
|
||||
} catch (RuntimeException | Error failure) {
|
||||
lease.close();
|
||||
@@ -792,6 +817,19 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private static void persistNaturalTerrain(ChunkAccess chunk, GenerationHistoryRuntimeRouter.RuntimeRoute route) {
|
||||
if (route == null || route.naturalTerrain().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
SavedTerrainChunk terrain = route.naturalTerrain().orElseThrow();
|
||||
chunk.persistentDataContainer.set(new NamespacedKey("iris", "natural_terrain"), PersistentDataType.BYTE_ARRAY,
|
||||
NativeTerrainReceipt.encode(terrain, route.activation().activationId(), route.epoch().epochId()));
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to persist natural terrain for chunk " + chunk.getPos(), failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable closeNoisePipelineResources(
|
||||
Throwable failure,
|
||||
GenerationSessionLease lease,
|
||||
@@ -933,19 +971,22 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)) {
|
||||
if (!ichunkaccess.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) {
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
}
|
||||
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)
|
||||
&& NativeGenerationWriteGuard.allowsDecoration(engine, generatoraccessseed, chunkPos)) {
|
||||
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
|
||||
// feature-order cycle is reported once and degraded to features-off.
|
||||
importedFeatures.prepare(generatoraccessseed);
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
|
||||
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
|
||||
// dimension set importedFeatures.enabled.
|
||||
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
}
|
||||
claimGeneratedSemantics(route, "bukkit_nms_biome_decoration");
|
||||
claimGeneratedSemantics(route, ichunkaccess, engine.getMinHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,7 +1039,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||
for (StructureStart start : starts) {
|
||||
BoundingBox footprint = start.getBoundingBox();
|
||||
if (!engine.getComplex().allowsNewGenerationFootprint(
|
||||
if (!isHistoricalStructureStart(world, start)
|
||||
&& !engine.getComplex().allowsNewGenerationFootprint(
|
||||
footprint.minX(),
|
||||
footprint.minZ(),
|
||||
footprint.maxX(),
|
||||
@@ -1098,6 +1140,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHistoricalStructureStart(WorldGenLevel world, StructureStart start) {
|
||||
ChunkPos origin = start.getChunkPos();
|
||||
if (!engine.getComplex().allowsMantleChunkWrite(origin.x(), origin.z())) {
|
||||
return true;
|
||||
}
|
||||
ChunkAccess source = world.getChunk(origin.x(), origin.z(), ChunkStatus.EMPTY, false);
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
long activation = source.persistentDataContainer.getOrDefault(
|
||||
new NamespacedKey("iris", "structure_activation"), PersistentDataType.LONG, 0L);
|
||||
return NativeGenerationWriteGuard.isHistoricalStructure(engine, activation);
|
||||
}
|
||||
|
||||
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
|
||||
if (placementGroups.isEmpty()) {
|
||||
return "<no resolved native structures>";
|
||||
@@ -1272,12 +1328,19 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(long packedChunk) {
|
||||
return !engine.isClosing() && !engine.isClosed()
|
||||
&& engine.getComplex().allowsMantleChunkWrite(ChunkPos.getX(packedChunk), ChunkPos.getZ(packedChunk));
|
||||
}
|
||||
|
||||
private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) {
|
||||
if (chunk.getPersistedStatus().isOrAfter(stage)) {
|
||||
return false;
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
return engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
|
||||
return NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, stage)
|
||||
|| engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
|
||||
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
||||
@@ -1314,6 +1377,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
i, j, "bukkit_nms_base_height");
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(i, j);
|
||||
if (resolved.isPresent()) {
|
||||
return NativeTransitionColumn.height(resolved.get(), heightmap_type, levelheightaccessor);
|
||||
}
|
||||
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||
int height = engine.getDimensionStackContext() == null
|
||||
? engine.getHeight(i, j, ignoreFluid)
|
||||
@@ -1331,6 +1398,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
i, j, "bukkit_nms_base_column");
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
Optional<TerrainBoundarySignature> resolved = engine.getComplex().resolvedTerrainColumn(i, j);
|
||||
if (resolved.isPresent()) {
|
||||
return NativeTransitionColumn.column(resolved.get(), levelheightaccessor);
|
||||
}
|
||||
boolean dimensionStack = engine.getDimensionStackContext() != null;
|
||||
int block = dimensionStack
|
||||
? Engine.hostHeight(engine, i, j, true)
|
||||
@@ -1412,16 +1483,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
private static void claimGeneratedSemantics(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
String operation
|
||||
ChunkAccess chunk,
|
||||
int minimumY
|
||||
) {
|
||||
if (route == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
route.claimGeneratedSemantics();
|
||||
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
|
||||
route.claimGeneratedSemantics((x, y, z) -> {
|
||||
BlockState state = chunk.getBlockState(position.set(x, minimumY + y, z));
|
||||
return state.isAir() || state.liquid();
|
||||
});
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " could not durably claim generated chunk semantics.", failure);
|
||||
throw new IllegalStateException("Iris could not durably claim generated chunk semantics.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-378
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.history.SavedTerrainChunk;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||
@@ -26,10 +27,8 @@ import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.nativegen.NativeStructureFactory;
|
||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -54,15 +53,6 @@ import art.arcane.volmlib.util.nbt.mca.palette.MCAWrappedPalettedContainer;
|
||||
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 net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.dynamic.DynamicType;
|
||||
import net.bytebuddy.utility.JavaModule;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.IdMapper;
|
||||
@@ -95,7 +85,6 @@ import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
@@ -108,7 +97,6 @@ import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
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;
|
||||
@@ -127,7 +115,6 @@ import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.TreeFeature;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -159,10 +146,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.awt.Color;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
@@ -173,7 +158,6 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class NMSBinding implements INMSBinding {
|
||||
@@ -181,11 +165,7 @@ public class NMSBinding implements INMSBinding {
|
||||
private volatile DataVersion dataVersion;
|
||||
private final BlockData AIR = Material.AIR.createBlockData();
|
||||
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
|
||||
private final AtomicBoolean injected = new AtomicBoolean();
|
||||
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
||||
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
|
||||
private boolean pluginClassLoaderCloseDeferred;
|
||||
private final NmsWorldLifecycle worldLifecycle = new NmsWorldLifecycle();
|
||||
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
||||
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
@@ -1131,6 +1111,17 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(World world) {
|
||||
return NmsSavedTerrainCapture.flush(world);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(World world, int chunkX, int chunkZ,
|
||||
int minimumY, int height) {
|
||||
return NmsSavedTerrainCapture.capture(world, chunkX, chunkZ, minimumY, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveAndUnloadChunk(World world, int x, int z) {
|
||||
try {
|
||||
@@ -1552,170 +1543,37 @@ public class NMSBinding implements INMSBinding {
|
||||
|
||||
@Override
|
||||
public boolean injectBukkit() {
|
||||
synchronized (injected) {
|
||||
if (injected.get()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
Agent.requireClassLoaderCloseDeferral();
|
||||
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
|
||||
.getMethod("close").getDeclaringClass();
|
||||
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
|
||||
pluginClassLoaderTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
|
||||
.with(loaderListener)
|
||||
.type(ElementMatchers.is(loaderCloseType))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
|
||||
.on(ElementMatchers.named("close")
|
||||
.and(ElementMatchers.takesArguments(0))
|
||||
.and(ElementMatchers.returns(void.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
loaderListener.requireInstalled();
|
||||
levelStorageAccessTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArguments(4))
|
||||
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
|
||||
.and(ElementMatchers.takesArgument(1, String.class))
|
||||
.and(ElementMatchers.takesArgument(2, Path.class))
|
||||
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
serverLevelTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
}
|
||||
|
||||
injected.set(true);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
||||
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
||||
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
pluginClassLoaderTransformer = null;
|
||||
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
||||
partialServerLevel,
|
||||
partialStorageAccess,
|
||||
partialPluginClassLoader
|
||||
}) {
|
||||
if (partial == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable cleanupFailure) {
|
||||
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return worldLifecycle.injectBukkit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureServerLevelInjection() {
|
||||
if (!injected.get())
|
||||
return;
|
||||
try {
|
||||
Agent.getInstrumentation().retransformClasses(
|
||||
LevelStorageSource.LevelStorageAccess.class,
|
||||
ServerLevel.class
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error(C.RED + "Failed to re-apply Bukkit world lifecycle injection");
|
||||
}
|
||||
worldLifecycle.ensureServerLevelInjection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninjectBukkit() {
|
||||
synchronized (injected) {
|
||||
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
injected.set(false);
|
||||
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
|
||||
activeServerLevel,
|
||||
activeStorageAccess
|
||||
}) {
|
||||
if (transformer == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
worldLifecycle.uninjectBukkit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isServerStopping() {
|
||||
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
|
||||
return worldLifecycle.isServerStopping();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
||||
return new ServerShutdownBoundary(
|
||||
() -> server.hasFullyShutdown,
|
||||
server.getRunningThread()
|
||||
);
|
||||
return worldLifecycle.createServerShutdownBoundary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deferPluginClassLoaderClose() {
|
||||
synchronized (injected) {
|
||||
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
|
||||
return;
|
||||
}
|
||||
Agent.retainClassLoader(getClass().getClassLoader());
|
||||
pluginClassLoaderCloseDeferred = true;
|
||||
}
|
||||
worldLifecycle.deferPluginClassLoaderClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePluginClassLoaderClose() {
|
||||
ResettableClassFileTransformer transformer;
|
||||
boolean releaseLoader;
|
||||
synchronized (injected) {
|
||||
transformer = pluginClassLoaderTransformer;
|
||||
pluginClassLoaderTransformer = null;
|
||||
releaseLoader = pluginClassLoaderCloseDeferred;
|
||||
pluginClassLoaderCloseDeferred = false;
|
||||
}
|
||||
try {
|
||||
if (transformer != null
|
||||
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
|
||||
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
|
||||
}
|
||||
} finally {
|
||||
if (releaseLoader) {
|
||||
Agent.releaseClassLoader(getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
worldLifecycle.releasePluginClassLoaderClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1761,220 +1619,4 @@ public class NMSBinding implements INMSBinding {
|
||||
settings.updateLayers();
|
||||
return new FlatLevelSource(settings);
|
||||
}
|
||||
|
||||
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
|
||||
private volatile boolean transformed;
|
||||
private volatile Throwable failure;
|
||||
|
||||
@Override
|
||||
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, DynamicType dynamicType) {
|
||||
transformed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String typeName, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, Throwable throwable) {
|
||||
failure = throwable;
|
||||
}
|
||||
|
||||
private void requireInstalled() {
|
||||
if (!transformed || failure != null) {
|
||||
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PluginClassLoaderCloseAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
|
||||
Class<?> installer = Class.forName(
|
||||
"art.arcane.iris.util.project.agent.Installer", true, ClassLoader.getSystemClassLoader());
|
||||
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
|
||||
.invoke(null, loader));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChunkAccessAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||
return index >= access.getPostProcessing().length;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LevelStorageAccessAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(1) String levelId,
|
||||
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
|
||||
) {
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object generator;
|
||||
try {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("peekStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to inspect the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (generator == null || !generatorType.isInstance(generator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object target = generatorType.getMethod("getTarget").invoke(generator);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to bind the staged world storage identity",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerLevelAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||
@Advice.AllArguments Object[] constructorArguments
|
||||
) {
|
||||
if (dimensionKey == null)
|
||||
return;
|
||||
|
||||
// This advice is inlined into every ServerLevel construction on the server. Until a
|
||||
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
|
||||
// Iris being unloaded or half-loaded must never break other plugins' world creation.
|
||||
String levelId;
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
levelId = dimensionKey.identifier().getPath();
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
|
||||
return;
|
||||
}
|
||||
|
||||
// From here Iris is present and its classes resolve; a failure resolving the
|
||||
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
|
||||
// the vanilla stem corrupts generation.
|
||||
ChunkGenerator gen = null;
|
||||
try {
|
||||
ChunkGenerator constructorGenerator = null;
|
||||
for (Object argument : constructorArguments) {
|
||||
if (argument instanceof ChunkGenerator candidate) {
|
||||
constructorGenerator = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||
if (generator == null) {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
}
|
||||
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||
gen = owned;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to resolve the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (gen == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Past the ownership gate the world is Iris-owned; silently handing back the
|
||||
// vanilla stem would corrupt generation, so failures from here rethrow.
|
||||
try {
|
||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (bindings == null) {
|
||||
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||
}
|
||||
|
||||
java.lang.reflect.Method stemMethod = null;
|
||||
for (java.lang.reflect.Method candidate : bindings.getClass().getMethods()) {
|
||||
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
|
||||
stemMethod = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stemMethod == null) {
|
||||
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
|
||||
}
|
||||
Object target = generatorType.getMethod("getTarget").invoke(gen);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
|
||||
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||
}
|
||||
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
|
||||
levelStem = runtimeStem;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import java.util.function.LongPredicate;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class NmsGenerationHooks {
|
||||
private NmsGenerationHooks() {
|
||||
}
|
||||
|
||||
static void install() {
|
||||
ByteBuddy buddy = new ByteBuddy();
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
}
|
||||
buddy.redefine(WorldGenRegion.class)
|
||||
.visit(Advice.to(WorldGenerationWriteAdvice.class).on(ElementMatchers.named("ensureCanWrite")
|
||||
.and(ElementMatchers.takesArguments(BlockPos.class))))
|
||||
.make()
|
||||
.load(WorldGenRegion.class.getClassLoader(), Agent.installed());
|
||||
}
|
||||
|
||||
private static class WorldGenerationWriteAdvice {
|
||||
@Advice.OnMethodExit
|
||||
static void exit(@Advice.This WorldGenRegion region, @Advice.Argument(0) BlockPos position,
|
||||
@Advice.Return(readOnly = false) boolean allowed) {
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
ChunkGenerator generator = region.getLevel().getChunkSource().getGenerator();
|
||||
if (generator instanceof LongPredicate predicate
|
||||
&& generator.getClass().getName().equals("art.arcane.iris.core.nms.v26_2_R1.IrisChunkGenerator")
|
||||
&& region.getChunk(position.getX() >> 4, position.getZ() >> 4)
|
||||
.getPersistedStatus().isOrAfter(ChunkStatus.FULL)) {
|
||||
allowed = predicate.test(ChunkPos.pack(position.getX() >> 4, position.getZ() >> 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChunkAccessAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||
return index >= access.getPostProcessing().length;
|
||||
}
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.engine.history.NativeTerrainReceipt;
|
||||
import art.arcane.iris.engine.history.SavedTerrainChunk;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import ca.spottedleaf.moonrise.common.PlatformHooks;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
final class NmsSavedTerrainCapture {
|
||||
private NmsSavedTerrainCapture() {
|
||||
}
|
||||
|
||||
static CompletableFuture<SavedTerrainChunk> capture(World world, int chunkX, int chunkZ, int minimumY, int height) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
CompletableFuture<CompoundTag> snapshot = new CompletableFuture<>();
|
||||
J.runRegionFuture(world, chunkX, chunkZ, () -> {
|
||||
try {
|
||||
NewChunkHolder holder = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkX, chunkZ);
|
||||
ChunkAccess chunk = holder == null ? null : holder.getCurrentChunk();
|
||||
if (chunk == null) {
|
||||
snapshot.complete(null);
|
||||
return;
|
||||
}
|
||||
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
|
||||
if (!SavedTerrainChunk.hasTerrain(status)) {
|
||||
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no completed terrain at " + status);
|
||||
}
|
||||
snapshot.complete(saveSnapshot(level, chunk));
|
||||
} catch (Throwable failure) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
}).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
snapshot.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
return snapshot.thenApplyAsync(data -> {
|
||||
try {
|
||||
if (data == null) {
|
||||
return SavedTerrainChunk.read(world.getWorldFolder().toPath(), chunkX, chunkZ, minimumY, height);
|
||||
}
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
try (DataOutputStream output = new DataOutputStream(bytes)) {
|
||||
NbtIo.write(data, output);
|
||||
}
|
||||
return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, minimumY, height);
|
||||
} catch (IOException failure) {
|
||||
throw new CompletionException(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static CompletableFuture<Void> flush(World world) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
List<NewChunkHolder> holders = level.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolders();
|
||||
List<CompletableFuture<Checkpoint>> saves = new ArrayList<>(holders.size());
|
||||
for (NewChunkHolder holder : holders) {
|
||||
CompletableFuture<Checkpoint> saved = new CompletableFuture<>();
|
||||
saves.add(saved);
|
||||
J.runRegionFuture(world, holder.chunkX, holder.chunkZ, () -> {
|
||||
try {
|
||||
ChunkAccess chunk = holder.getCurrentChunk();
|
||||
if (chunk == null || !chunk.isUnsaved()) {
|
||||
saved.complete(null);
|
||||
return;
|
||||
}
|
||||
CompoundTag data = saveSnapshot(level, chunk);
|
||||
byte[] receipt = data.getCompound("ChunkBukkitValues")
|
||||
.flatMap(values -> values.getByteArray(NativeTerrainReceipt.NBT_KEY)).orElse(null);
|
||||
saved.complete(new Checkpoint(holder.chunkX, holder.chunkZ,
|
||||
BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString(), receipt,
|
||||
data.getCompound("ChunkBukkitValues").map(values -> values.getLongOr(
|
||||
NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0)).orElse(0L)));
|
||||
} catch (Throwable failure) {
|
||||
saved.completeExceptionally(failure);
|
||||
}
|
||||
}).whenComplete((ignored, failure) -> {
|
||||
if (failure != null) {
|
||||
saved.completeExceptionally(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
return CompletableFuture.allOf(saves.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
|
||||
try {
|
||||
MoonriseRegionFileIO.flush(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||
MoonriseRegionFileIO.flushRegionStorages(level, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||
for (CompletableFuture<Checkpoint> future : saves) {
|
||||
Checkpoint saved = future.join();
|
||||
if (saved == null) {
|
||||
continue;
|
||||
}
|
||||
SavedTerrainChunk.verifyCheckpoint(world.getWorldFolder().toPath(), saved.chunkX(), saved.chunkZ(),
|
||||
saved.status(), saved.receipt(), saved.structureActivation());
|
||||
}
|
||||
} catch (IOException failure) {
|
||||
throw new CompletionException(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static CompoundTag saveSnapshot(ServerLevel level, ChunkAccess chunk) {
|
||||
SerializableChunkData snapshot = SerializableChunkData.copyOf(level, chunk);
|
||||
PlatformHooks.get().chunkSyncSave(level, chunk, snapshot);
|
||||
CompoundTag data = snapshot.write();
|
||||
MoonriseRegionFileIO.scheduleSave(level, chunk.getPos().x(), chunk.getPos().z(), data,
|
||||
MoonriseRegionFileIO.RegionFileType.CHUNK_DATA);
|
||||
return data;
|
||||
}
|
||||
|
||||
private record Checkpoint(int chunkX, int chunkZ, String status, byte[] receipt, long structureActivation) {
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
package art.arcane.iris.core.nms.v26_2_R1;
|
||||
|
||||
import art.arcane.iris.core.nms.ServerShutdownBoundary;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.dynamic.DynamicType;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.bytebuddy.utility.JavaModule;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
final class NmsWorldLifecycle {
|
||||
private final AtomicBoolean injected = new AtomicBoolean();
|
||||
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
||||
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
|
||||
private boolean pluginClassLoaderCloseDeferred;
|
||||
|
||||
public boolean injectBukkit() {
|
||||
synchronized (injected) {
|
||||
if (injected.get()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
Agent.requireClassLoaderCloseDeferral();
|
||||
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
|
||||
.getMethod("close").getDeclaringClass();
|
||||
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
|
||||
pluginClassLoaderTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
|
||||
.with(loaderListener)
|
||||
.type(ElementMatchers.is(loaderCloseType))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
|
||||
.on(ElementMatchers.named("close")
|
||||
.and(ElementMatchers.takesArguments(0))
|
||||
.and(ElementMatchers.returns(void.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
loaderListener.requireInstalled();
|
||||
levelStorageAccessTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArguments(4))
|
||||
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
|
||||
.and(ElementMatchers.takesArgument(1, String.class))
|
||||
.and(ElementMatchers.takesArgument(2, Path.class))
|
||||
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
serverLevelTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.type(ElementMatchers.is(ServerLevel.class))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
NmsGenerationHooks.install();
|
||||
|
||||
injected.set(true);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
||||
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
||||
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
pluginClassLoaderTransformer = null;
|
||||
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
||||
partialServerLevel,
|
||||
partialStorageAccess,
|
||||
partialPluginClassLoader
|
||||
}) {
|
||||
if (partial == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable cleanupFailure) {
|
||||
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureServerLevelInjection() {
|
||||
if (!injected.get()) {
|
||||
throw new IllegalStateException("Iris world lifecycle injection is unavailable. Fix the Java Agent or Code Injection startup failure and restart before creating worlds.");
|
||||
}
|
||||
try {
|
||||
Agent.getInstrumentation().retransformClasses(
|
||||
LevelStorageSource.LevelStorageAccess.class,
|
||||
ServerLevel.class
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException("Failed to re-apply Bukkit world lifecycle injection. World creation was stopped before loading the world.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void uninjectBukkit() {
|
||||
synchronized (injected) {
|
||||
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
injected.set(false);
|
||||
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
|
||||
activeServerLevel,
|
||||
activeStorageAccess
|
||||
}) {
|
||||
if (transformer == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(C.RED + "Failed to remove Bukkit world lifecycle injection", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isServerStopping() {
|
||||
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
|
||||
}
|
||||
|
||||
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
||||
return new ServerShutdownBoundary(
|
||||
() -> server.hasFullyShutdown,
|
||||
server.getRunningThread()
|
||||
);
|
||||
}
|
||||
|
||||
public void deferPluginClassLoaderClose() {
|
||||
synchronized (injected) {
|
||||
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
|
||||
return;
|
||||
}
|
||||
Agent.retainClassLoader(getClass().getClassLoader());
|
||||
pluginClassLoaderCloseDeferred = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void releasePluginClassLoaderClose() {
|
||||
ResettableClassFileTransformer transformer;
|
||||
boolean releaseLoader;
|
||||
synchronized (injected) {
|
||||
transformer = pluginClassLoaderTransformer;
|
||||
pluginClassLoaderTransformer = null;
|
||||
releaseLoader = pluginClassLoaderCloseDeferred;
|
||||
pluginClassLoaderCloseDeferred = false;
|
||||
}
|
||||
try {
|
||||
if (transformer != null
|
||||
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
|
||||
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
|
||||
}
|
||||
} finally {
|
||||
if (releaseLoader) {
|
||||
Agent.releaseClassLoader(getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
|
||||
private volatile boolean transformed;
|
||||
private volatile Throwable failure;
|
||||
|
||||
@Override
|
||||
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, DynamicType dynamicType) {
|
||||
transformed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String typeName, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, Throwable throwable) {
|
||||
failure = throwable;
|
||||
}
|
||||
|
||||
private void requireInstalled() {
|
||||
if (!transformed || failure != null) {
|
||||
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PluginClassLoaderCloseAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
|
||||
Class<?> installer = Class.forName(
|
||||
"art.arcane.iris.util.project.agent.Installer", true, ClassLoader.getSystemClassLoader());
|
||||
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
|
||||
.invoke(null, loader));
|
||||
}
|
||||
}
|
||||
|
||||
private static class LevelStorageAccessAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(1) String levelId,
|
||||
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
|
||||
) {
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object generator;
|
||||
try {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("peekStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to inspect the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (generator == null || !generatorType.isInstance(generator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Object target = generatorType.getMethod("getTarget").invoke(generator);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to bind the staged world storage identity",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerLevelAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||
@Advice.AllArguments Object[] constructorArguments
|
||||
) {
|
||||
if (dimensionKey == null)
|
||||
return;
|
||||
|
||||
// This advice is inlined into every ServerLevel construction on the server. Until a
|
||||
// world is proven Iris-owned, every failure must fail OPEN (keep the vanilla stem):
|
||||
// Iris being unloaded or half-loaded must never break other plugins' world creation.
|
||||
String levelId;
|
||||
ClassLoader pluginClassLoader;
|
||||
Class<?> generatorType;
|
||||
Class<?> stagingType;
|
||||
try {
|
||||
levelId = dimensionKey.identifier().getPath();
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
|
||||
if (irisPlugin == null) {
|
||||
return;
|
||||
}
|
||||
pluginClassLoader = irisPlugin.getClass().getClassLoader();
|
||||
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
|
||||
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
|
||||
} catch (Throwable ignored) {
|
||||
// Iris absent or half-loaded: fail OPEN for a world we cannot prove ours.
|
||||
return;
|
||||
}
|
||||
|
||||
// From here Iris is present and its classes resolve; a failure resolving the
|
||||
// staged generator must fail LOUD — silently handing a possibly-staged Iris world
|
||||
// the vanilla stem corrupts generation.
|
||||
ChunkGenerator gen = null;
|
||||
try {
|
||||
ChunkGenerator constructorGenerator = null;
|
||||
for (Object argument : constructorArguments) {
|
||||
if (argument instanceof ChunkGenerator candidate) {
|
||||
constructorGenerator = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
|
||||
if (generator == null) {
|
||||
generator = stagingType
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
}
|
||||
if (generator instanceof ChunkGenerator owned && owned.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||
gen = owned;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to resolve the staged world generator",
|
||||
e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
if (gen == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Past the ownership gate the world is Iris-owned; silently handing back the
|
||||
// vanilla stem would corrupt generation, so failures from here rethrow.
|
||||
try {
|
||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, pluginClassLoader)
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (bindings == null) {
|
||||
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||
}
|
||||
|
||||
Method stemMethod = null;
|
||||
for (Method candidate : bindings.getClass().getMethods()) {
|
||||
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
|
||||
stemMethod = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stemMethod == null) {
|
||||
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
|
||||
}
|
||||
Object target = generatorType.getMethod("getTarget").invoke(gen);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Iris generator has no engine target.");
|
||||
}
|
||||
Object world = target.getClass().getMethod("getWorld").invoke(target);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris generator target has no world identity.");
|
||||
}
|
||||
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
|
||||
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
|
||||
Identifier worldIdentifier = Identifier.parse(worldIdentity);
|
||||
if (!"iris".equals(worldIdentifier.getNamespace())
|
||||
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
|
||||
throw new IllegalStateException(
|
||||
"Iris generator has an unmanaged world identity: " + worldIdentity);
|
||||
}
|
||||
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
|
||||
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||
}
|
||||
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
|
||||
levelStem = runtimeStem;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -43,6 +43,9 @@ public final class GenerationHistoryRoutingContractTest {
|
||||
assertBefore(noise, "openHistoryRoute(", "requireGenerationLease(");
|
||||
assertTrue(count(noise, "openHistoryRuntimeScope(route)") >= 2);
|
||||
assertBefore(noise, ".thenApply(filled ->", "pipeline.whenComplete(");
|
||||
assertBefore(noise, "pipeline.whenComplete(", "lease.detachThread()");
|
||||
assertBefore(noise, "lease.detachThread()", "route.detachThread()");
|
||||
assertBefore(noise, "route.detachThread()", "return completion;");
|
||||
assertTrue(noise.contains("closeNoisePipelineResources(\n failure, lease, route, stage)"));
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -8,11 +8,11 @@ import java.nio.file.Path;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NMSBindingServerLevelAdviceContractTest {
|
||||
public class NmsWorldLifecycleServerLevelAdviceContractTest {
|
||||
@Test
|
||||
public void serverLevelAdviceSupportsCurrentSpigotAndPaperConstructorLayouts() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n");
|
||||
String transformer = section(
|
||||
source,
|
||||
"public boolean injectBukkit()",
|
||||
@@ -35,7 +35,7 @@ public class NMSBindingServerLevelAdviceContractTest {
|
||||
@Test
|
||||
public void currentSpigotStorageAccessReceivesManagedIdentityBeforeConstruction() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n");
|
||||
String transformer = section(
|
||||
source,
|
||||
"public boolean injectBukkit()",
|
||||
@@ -68,7 +68,7 @@ public class NMSBindingServerLevelAdviceContractTest {
|
||||
@Test
|
||||
public void ownedWorldPublishesItsManagedIdentityAndRuntimeStemTogether() throws Exception {
|
||||
String source = Files.readString(
|
||||
Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n");
|
||||
String advice = section(
|
||||
source,
|
||||
"private static class ServerLevelAdvice",
|
||||
+7
-7
@@ -8,10 +8,10 @@ import java.nio.file.Path;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class NMSBindingShutdownBoundaryContractTest {
|
||||
public class NmsWorldLifecycleShutdownBoundaryContractTest {
|
||||
@Test
|
||||
public void shutdownClassificationUsesTheServerStopFlagInsteadOfPluginEnablement() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java"));
|
||||
String stopping = section(source, "public boolean isServerStopping()", "public ServerShutdownBoundary createServerShutdownBoundary");
|
||||
|
||||
assertTrue(stopping.contains("((CraftServer) Bukkit.getServer()).getServer().hasStopped()"));
|
||||
@@ -21,10 +21,10 @@ public class NMSBindingShutdownBoundaryContractTest {
|
||||
|
||||
@Test
|
||||
public void pluginLoaderGuardUsesSystemAgentIdentityAndIsRemovedAtRelease() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java"));
|
||||
String injection = section(source, "public boolean injectBukkit()", "public void ensureServerLevelInjection()");
|
||||
String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class ChunkAccessAdvice");
|
||||
String release = section(source, "public void releasePluginClassLoaderClose()", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
||||
String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class LevelStorageAccessAdvice");
|
||||
String release = section(source, "public void releasePluginClassLoaderClose()", "private static final class PluginClassLoaderInjectionListener");
|
||||
|
||||
assertTrue(injection.contains("Agent.requireClassLoaderCloseDeferral()"));
|
||||
assertTrue(injection.contains("getMethod(\"close\").getDeclaringClass()"));
|
||||
@@ -40,8 +40,8 @@ public class NMSBindingShutdownBoundaryContractTest {
|
||||
|
||||
@Test
|
||||
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n");
|
||||
String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "private static final class PluginClassLoaderInjectionListener");
|
||||
|
||||
assertTrue(boundary.contains("new ServerShutdownBoundary("));
|
||||
assertTrue(boundary.contains("server.hasFullyShutdown"));
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.history.GenerationActivation;
|
||||
import art.arcane.iris.engine.history.GenerationHistory;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import java.util.Optional;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class NativeGenerationWriteGuardTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preTerrainSavedStampDistinguishesEarlierNativeStarts() {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
GenerationHistory history = mock(GenerationHistory.class);
|
||||
GenerationActivation active = mock(GenerationActivation.class);
|
||||
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
|
||||
when(router.history()).thenReturn(history);
|
||||
when(history.activeActivation()).thenReturn(active);
|
||||
when(active.activationId()).thenReturn(4L);
|
||||
assertTrue(NativeGenerationWriteGuard.isHistoricalStructure(engine, 3));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 0));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 4));
|
||||
assertFalse(NativeGenerationWriteGuard.isHistoricalStructure(engine, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialNeighborsMayFinishButCompletedHistoricalNeighborRejectsPass() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
WorldGenLevel region = mock(WorldGenLevel.class);
|
||||
ChunkAccess partial = mock(ChunkAccess.class);
|
||||
when(partial.getPersistedStatus()).thenReturn(ChunkStatus.NOISE);
|
||||
when(region.getChunk(anyInt(), anyInt())).thenReturn(partial);
|
||||
assertTrue(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
|
||||
ChunkAccess complete = mock(ChunkAccess.class);
|
||||
when(complete.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
|
||||
when(region.getChunk(1, -1)).thenReturn(complete);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsDecoration(engine, region, new ChunkPos(0, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void historicalNoiseMayFinishFeaturesButNeverReplaysNoiseOrCompletedStage() {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
ChunkAccess chunk = mock(ChunkAccess.class);
|
||||
when(chunk.getPos()).thenReturn(new ChunkPos(-2, 3));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.NOISE);
|
||||
assertTrue(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.NOISE));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FEATURES);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.FEATURES));
|
||||
when(chunk.getPersistedStatus()).thenReturn(ChunkStatus.FULL);
|
||||
assertFalse(NativeGenerationWriteGuard.allowsPendingStage(engine, chunk, ChunkStatus.SPAWN));
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package art.arcane.iris.nativegen;
|
||||
|
||||
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
|
||||
import art.arcane.iris.engine.history.TerrainBoundarySignature;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.RotatedPillarBlock;
|
||||
import net.minecraft.world.level.block.LiquidBlock;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class NativeTransitionColumnTest {
|
||||
@BeforeClass
|
||||
public static void bootstrap() {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preservesNativePropertiesCavesAndFluids() {
|
||||
NoiseColumn column = NativeTransitionColumn.column(signature("minecraft:oak_log[axis=x]"),
|
||||
LevelHeightAccessor.create(-16, 32));
|
||||
assertEquals(Direction.Axis.X, column.getBlock(-15).getValue(RotatedPillarBlock.AXIS));
|
||||
assertTrue(column.getBlock(-14).is(Blocks.CAVE_AIR));
|
||||
assertEquals(Integer.valueOf(3), column.getBlock(-13).getValue(LiquidBlock.LEVEL));
|
||||
assertTrue(column.getBlock(0).isAir());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void usesNativeHeightmapPredicatesForWaterAndOpenSpace() {
|
||||
TerrainBoundarySignature signature = signature("minecraft:oak_log[axis=x]");
|
||||
LevelHeightAccessor height = LevelHeightAccessor.create(-16, 32);
|
||||
assertEquals(-12, NativeTransitionColumn.height(signature, Heightmap.Types.WORLD_SURFACE_WG, height));
|
||||
assertEquals(-14, NativeTransitionColumn.height(signature, Heightmap.Types.OCEAN_FLOOR_WG, height));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsUnknownSavedStatesInsteadOfReplacingTerrain() {
|
||||
assertThrows(IllegalArgumentException.class, () -> NativeTransitionColumn.column(
|
||||
signature("missing:removed_block"), LevelHeightAccessor.create(-16, 32)));
|
||||
}
|
||||
|
||||
private static TerrainBoundarySignature signature(String solidState) {
|
||||
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-16, List.of(
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel(solidState, BoundaryColumnGeometry.Phase.SOLID, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:cave_air", BoundaryColumnGeometry.Phase.AIR, "", false),
|
||||
new BoundaryColumnGeometry.Voxel("minecraft:water[level=3]", BoundaryColumnGeometry.Phase.FLUID,
|
||||
"minecraft:water[level=3]", false)));
|
||||
return new TerrainBoundarySignature(
|
||||
new TerrainBoundarySignature.Column(0, 0, 3, 1, OptionalInt.of(3), OptionalInt.empty()),
|
||||
new TerrainBoundarySignature.Samples(new TerrainBoundarySignature.VerticalLayout(-16, 4, 1),
|
||||
new TerrainBoundarySignature.BiomeEncoding(List.of("minecraft:plains"), new short[]{0})),
|
||||
geometry);
|
||||
}
|
||||
}
|
||||
+21
-2
@@ -23,6 +23,7 @@ import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
||||
@@ -30,6 +31,9 @@ import art.arcane.iris.core.service.IrisApiEventSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.SavedTerrainChunk;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import art.arcane.iris.engine.framework.EngineMode;
|
||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||
@@ -47,6 +51,16 @@ import org.bukkit.World;
|
||||
import org.bukkit.WorldBorder;
|
||||
|
||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
@Override
|
||||
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
|
||||
return INMS.get().flushSavedTerrainCapture(BukkitWorldBinding.world(engine.getWorld()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) {
|
||||
return BukkitSavedTerrainCapture.capture(engine, chunkX, chunkZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||
return INMS.get().nativeStructureVolumes(engine, minX, minZ, maxX, maxZ);
|
||||
@@ -54,12 +68,17 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
|
||||
@Override
|
||||
public void refreshWorkspace(Engine engine) {
|
||||
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
|
||||
new IrisCodeWorkspace(new IrisProject(engine.getPackSource().toFile())).updateWorkspace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshDatapackWorkspace(Engine engine) {
|
||||
DatapackIngestService.refreshWorkspace(engine.getData());
|
||||
IrisData data = IrisData.openRuntime(engine.getPackSource().toFile());
|
||||
try {
|
||||
DatapackIngestService.refreshWorkspace(data);
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package art.arcane.iris.core.runtime;
|
||||
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.SavedTerrainChunk;
|
||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
final class BukkitSavedTerrainCapture {
|
||||
private BukkitSavedTerrainCapture() {
|
||||
}
|
||||
|
||||
static CompletableFuture<SavedTerrainChunk> capture(Engine engine, int chunkX, int chunkZ) {
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (world == null) {
|
||||
return CompletableFuture.failedFuture(new IOException("The world is unavailable for terrain capture."));
|
||||
}
|
||||
return INMS.get().captureSavedTerrainChunk(world, chunkX, chunkZ, engine.getMinHeight(), engine.getHeight());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -192,7 +192,7 @@ public class IrisProtocolService implements IrisService, PluginMessageListener,
|
||||
World world = player.getWorld();
|
||||
Engine engine = engineFor(world);
|
||||
if (engine != null) {
|
||||
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getData().getDataFolder().getName(),
|
||||
protocol.sendDimensionStatus(sessionId, engine.getDimension().getLoadKey(), engine.getPackSource().getFileName().toString(),
|
||||
world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user