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:
Brian Neumann-Fopiano
2026-09-05 11:57:01 -04:00
parent 20ad49b9d2
commit b1e1fbc15e
214 changed files with 11985 additions and 4082 deletions
@@ -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);
}
}
@@ -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);
}
}
}
}
@@ -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;
}
}
}
@@ -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) {
}
}
@@ -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);
}
}
}
}
@@ -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)"));
}
@@ -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",
@@ -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"));
@@ -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));
}
}
@@ -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);
}
}
@@ -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
@@ -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());
}
}
@@ -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;
}
+1 -1
View File
@@ -26,7 +26,7 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
apply from: file('../../gradle/modded-generation-build-revision.gradle')
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
+1 -1
View File
@@ -26,7 +26,7 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
apply from: file('../../gradle/modded-generation-build-revision.gradle')
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
@@ -0,0 +1,46 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
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.ChunkPyramid;
import net.minecraft.world.level.chunk.status.ChunkStatus;
public final class NativeGenerationWriteGuard {
private NativeGenerationWriteGuard() {
}
public static boolean allowsDecoration(Engine engine, WorldGenLevel region, ChunkPos center) {
int radius = ChunkPyramid.GENERATION_PYRAMID.getStepTo(ChunkStatus.FEATURES).blockStateWriteRadius();
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int chunkX = Math.addExact(center.x(), offsetX);
int chunkZ = Math.addExact(center.z(), offsetZ);
ChunkAccess target = region.getChunk(chunkX, chunkZ);
if (target.getPersistedStatus().isOrAfter(ChunkStatus.FULL)
&& !engine.getComplex().allowsMantleChunkWrite(chunkX, chunkZ)) {
return false;
}
}
}
return true;
}
public static boolean isHistoricalStructure(Engine engine, long activation) {
if (activation <= 0 || !(engine instanceof IrisEngine irisEngine)) {
return false;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
return router != null && activation < router.history().activeActivation().activationId();
}
public static boolean allowsPendingStage(Engine engine, ChunkAccess chunk, ChunkStatus stage) {
return stage.isOrAfter(ChunkStatus.FEATURES)
&& chunk.getPersistedStatus().isOrAfter(ChunkStatus.NOISE)
&& !chunk.getPersistedStatus().isOrAfter(stage)
&& !engine.getComplex().allowsMantleChunkWrite(chunk.getPos().x(), chunk.getPos().z());
}
}
@@ -0,0 +1,53 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.commands.arguments.blocks.BlockStateParser;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap;
import java.util.HashMap;
import java.util.Map;
public final class NativeTransitionColumn {
private NativeTransitionColumn() {
}
public static NoiseColumn column(TerrainBoundarySignature signature, LevelHeightAccessor height) {
BlockState[] states = new BlockState[height.getHeight()];
Map<String, BlockState> palette = new HashMap<>();
for (int offset = 0; offset < states.length; offset++) {
String state = signature.geometry().voxelAt(height.getMinY() + offset).stateKey();
states[offset] = palette.computeIfAbsent(state, NativeTransitionColumn::parse);
}
return new NoiseColumn(height.getMinY(), states);
}
public static int height(TerrainBoundarySignature signature, Heightmap.Types type, LevelHeightAccessor height) {
Map<String, BlockState> palette = new HashMap<>();
for (int offset = height.getHeight() - 1; offset >= 0; offset--) {
String state = signature.geometry().voxelAt(height.getMinY() + offset).stateKey();
if (type.isOpaque().test(palette.computeIfAbsent(state, NativeTransitionColumn::parse))) {
return height.getMinY() + offset + 1;
}
}
return height.getMinY();
}
private static BlockState parse(String key) {
StringReader reader = new StringReader(key);
try {
BlockState state = BlockStateParser.parseForBlock(BuiltInRegistries.BLOCK, reader, false).blockState();
if (reader.canRead()) {
throw new IllegalArgumentException("Unexpected terrain block state suffix: " + reader.getRemaining());
}
return state;
} catch (CommandSyntaxException failure) {
throw new IllegalArgumentException("Saved terrain block state cannot be resolved: " + key, failure);
}
}
}
@@ -18,6 +18,10 @@
package art.arcane.iris.modded;
import art.arcane.iris.nativegen.NativeTransitionColumn;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import java.util.Optional;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.IrisEngine;
@@ -1019,14 +1023,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
writeBlocks(chunk, blocks, dimMinY, height);
if (route != null) {
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
route.claimGeneratedSemantics((x, y, z) -> {
BlockState state = chunk.getBlockState(position.set(x, dimMinY + y, z));
return state.isAir() || state.liquid();
});
}
ModdedNativeTerrainReceipts.persist(chunk, route);
writeTerrainHeightmaps(chunk, generationEngine, pos, height);
Heightmap.primeHeightmaps(chunk, EnumSet.of(
Heightmap.Types.MOTION_BLOCKING,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
if (route != null) {
route.claimGeneratedSemantics();
}
return chunk;
} catch (GenerationSessionException e) {
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
@@ -1066,6 +1075,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
completion.completeExceptionally(completionFailure);
}
});
route.detachThread();
return completion;
}
@@ -1205,6 +1215,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
if (chunk.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) {
return;
}
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
if (!allowsRoutedDiscreteGeneration(
current,
route,
@@ -1214,8 +1228,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
)) {
return;
}
if (!NativeGenerationWriteGuard.allowsDecoration(current, level, chunkPos)) {
return;
}
importedFeatures.prepare(current);
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
// Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk
// step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the
// dimension set importedFeatures.enabled.
@@ -1257,6 +1273,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
));
nativeStructures.adjustGeneratedStructures(
registryAccess, chunk, previousStarts, configuredStarts, current, templateManager);
ModdedNativeTerrainReceipts.persistStructureActivation(chunk, route);
}
}
@@ -1355,6 +1372,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
current, x, z, "modded_base_height");
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Optional<TerrainBoundarySignature> resolved = current.getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return NativeTransitionColumn.height(resolved.get(), type, heightAccessor);
}
int height = current.getDimensionStackContext() == null
? current.getHeight(x, z, ignoreFluid)
: Engine.hostHeight(current, x, z, ignoreFluid);
@@ -1367,12 +1388,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
int minY = heightAccessor.getMinY();
BlockState[] states = new BlockState[heightAccessor.getHeight()];
Engine current = requireDataQueryEngine("base column");
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
current, x, z, "modded_base_column");
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Optional<TerrainBoundarySignature> resolved = current.getComplex().resolvedTerrainColumn(x, z);
if (resolved.isPresent()) {
return NativeTransitionColumn.column(resolved.get(), heightAccessor);
}
BlockState[] states = new BlockState[heightAccessor.getHeight()];
BlockState airState = Blocks.AIR.defaultBlockState();
boolean dimensionStack = current.getDimensionStackContext() != null;
int surface = dimensionStack
@@ -1394,6 +1419,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
public boolean allowsNativeChunkWrite(int chunkX, int chunkZ) {
Engine current = engine;
return current == null || !current.isClosing() && !current.isClosed()
&& current.getComplex().allowsMantleChunkWrite(chunkX, chunkZ);
}
private boolean allowsRoutedDiscreteGeneration(
Engine current,
GenerationHistoryRuntimeRouter.RuntimeRoute route,
@@ -1407,7 +1438,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (route == null) {
return allowsGenerationHistoryBypass(current);
}
return current.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
return NativeGenerationWriteGuard.allowsPendingStage(current, chunk, stage)
|| current.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
}
private void bindGenerationRuntimeRetirementListener(Engine current) {
@@ -362,13 +362,15 @@ public final class ModdedForcedDatapack {
KList<String> presetIds = new KList<>();
Path worldRoot = worldRootOrNull();
Set<PackSelection> legacySelections = unadoptedSelections(worldRoot);
for (File pack : compilationPackRoots(worldRoot)) {
List<File> packRoots = compilationPackRoots(worldRoot);
for (File pack : packRoots) {
PackStageContext context = packStageContext(pack.toPath(), legacySelections);
if (stagePack(context, fixer, stagingDirectory, seenBiomes, presetIds)) {
packCount++;
}
}
IrisDatapackCompiler.installRetainedRegistrySources(packRoots, List.of(stagingDirectory.toFile()), fixer);
writePackMeta(stagingDirectory);
// Forge only: FML has no block-drops event Iris can use, so drops are routed through a global loot
// modifier. NeoForge does not need one - IrisNeoForgeBootstrap listens to BlockDropsEvent and both
@@ -996,7 +998,7 @@ public final class ModdedForcedDatapack {
return currentSource;
}
PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey);
return GenerationRegistryContractFactory.requireGeneratedSource(retainedContract, key, fixer);
return GenerationRegistryContractFactory.renderGeneratedSource(retainedContract, key, fixer);
}
}
@@ -18,6 +18,10 @@
package art.arcane.iris.modded;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.framework.Engine;
@@ -341,7 +345,8 @@ final class ModdedNativeStructureStage {
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
BoundingBox footprint = start.getBoundingBox();
if (!current.getComplex().allowsNewGenerationFootprint(
if (!isHistoricalStructureStart(current, world, start)
&& !current.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
@@ -574,6 +579,16 @@ final class ModdedNativeStructureStage {
return blockState;
}
private boolean isHistoricalStructureStart(Engine current, WorldGenLevel world, StructureStart start) {
ChunkPos origin = start.getChunkPos();
if (!current.getComplex().allowsMantleChunkWrite(origin.x(), origin.z())) {
return true;
}
ChunkAccess source = world.getChunk(origin.x(), origin.z(), ChunkStatus.EMPTY, false);
return source != null && NativeGenerationWriteGuard.isHistoricalStructure(
current, ModdedNativeTerrainReceipts.structureActivation(source));
}
private BoundingBox writableArea(ChunkAccess chunk) {
ChunkPos chunkPos = chunk.getPos();
int minX = chunkPos.getMinBlockX();
@@ -0,0 +1,53 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.NativeTerrainReceipt;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import java.io.IOException;
public final class ModdedNativeTerrainReceipts {
private ModdedNativeTerrainReceipts() {
}
public static void persist(ChunkAccess chunk, GenerationHistoryRuntimeRouter.RuntimeRoute route) throws IOException {
if (route == null || route.naturalTerrain().isEmpty()) {
return;
}
set(chunk, NativeTerrainReceipt.encode(route.naturalTerrain().orElseThrow(),
route.activation().activationId(), route.epoch().epochId()));
chunk.markUnsaved();
}
public static long structureActivation(ChunkAccess chunk) {
return holder(chunk).iris$getStructureActivation();
}
public static void setStructureActivation(ChunkAccess chunk, long activation) {
holder(chunk).iris$setStructureActivation(activation);
}
public static void persistStructureActivation(ChunkAccess chunk, GenerationHistoryRuntimeRouter.RuntimeRoute route) {
if (route != null) {
setStructureActivation(chunk, route.activation().activationId());
chunk.markUnsaved();
}
}
public static byte[] get(ChunkAccess chunk) {
return holder(chunk).iris$getNaturalTerrain();
}
public static void set(ChunkAccess chunk, byte[] receipt) {
holder(chunk).iris$setNaturalTerrain(receipt);
}
private static NativeTerrainReceiptHolder holder(ChunkAccess chunk) {
ChunkAccess target = chunk instanceof ImposterProtoChunk imposter ? imposter.getWrapped() : chunk;
if (!(target instanceof NativeTerrainReceiptHolder holder)) {
throw new IllegalStateException("Native terrain receipt storage is unavailable for " + chunk.getPos());
}
return holder;
}
}
@@ -0,0 +1,142 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.modded.mixin.ChunkMapTerrainReceiptAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
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;
public final class ModdedSavedTerrainCapture {
private ModdedSavedTerrainCapture() {
}
public static CompletableFuture<SavedTerrainChunk> capture(Engine engine, int chunkX, int chunkZ) {
ServerLevel level = requireLevel(engine);
CompletableFuture<CompoundTag> snapshot = new CompletableFuture<>();
runOwned(level, () -> {
try {
ChunkMap map = level.getChunkSource().chunkMap;
ChunkHolder holder = map.getUpdatingChunkIfPresent(ChunkPos.pack(chunkX, chunkZ));
ChunkAccess chunk = holder == null ? null : holder.getLatestChunk();
if (chunk == null) {
map.read(new ChunkPos(chunkX, chunkZ)).whenComplete((data, failure) -> {
if (failure != null) {
snapshot.completeExceptionally(failure);
} else {
snapshot.complete(data.orElse(null));
}
});
return;
}
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
if (!SavedTerrainChunk.hasTerrain(status)) {
throw new IOException("Native chunk " + chunkX + "," + chunkZ + " has no terrain at " + status);
}
CompoundTag data = SerializableChunkData.copyOf(level, chunk).write();
if (chunk.isUnsaved()) {
((ChunkMapTerrainReceiptAccess) map).iris$saveTerrainCheckpoint(chunk);
}
snapshot.complete(data);
} catch (Throwable failure) {
snapshot.completeExceptionally(failure);
}
});
return snapshot.thenApplyAsync(data -> {
try {
if (data == null) {
throw new IOException("Saved native chunk is unavailable: " + chunkX + "," + chunkZ);
}
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(bytes)) {
NbtIo.write(data, output);
}
return SavedTerrainChunk.readNbt(bytes.toByteArray(), chunkX, chunkZ, engine.getMinHeight(), engine.getHeight());
} catch (IOException failure) {
throw new CompletionException(failure);
}
});
}
public static CompletableFuture<Void> flush(Engine engine) {
ServerLevel level = requireLevel(engine);
CompletableFuture<Void> complete = new CompletableFuture<>();
runOwned(level, () -> {
try {
ChunkMap map = level.getChunkSource().chunkMap;
List<Checkpoint> checkpoints = new ArrayList<>();
for (ChunkHolder holder : ((ChunkMapTerrainReceiptAccess) map).iris$updatingChunks().values()) {
ChunkAccess chunk = holder.getLatestChunk();
if (chunk == null) {
continue;
}
String status = BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus()).toString();
if (!SavedTerrainChunk.hasTerrain(status)
&& ModdedNativeTerrainReceipts.structureActivation(chunk) == 0) {
continue;
}
checkpoints.add(new Checkpoint(chunk.getPos().x(), chunk.getPos().z(), status,
ModdedNativeTerrainReceipts.get(chunk), ModdedNativeTerrainReceipts.structureActivation(chunk)));
if (chunk.isUnsaved()) {
((ChunkMapTerrainReceiptAccess) map).iris$saveTerrainCheckpoint(chunk);
}
}
map.synchronize(true).thenRunAsync(() -> verifyCheckpoints(engine, checkpoints))
.whenComplete((ignored, failure) -> {
if (failure != null) {
complete.completeExceptionally(failure);
} else {
complete.complete(null);
}
});
} catch (Throwable failure) {
complete.completeExceptionally(failure);
}
});
return complete;
}
private static void verifyCheckpoints(Engine engine, List<Checkpoint> checkpoints) {
try {
for (Checkpoint checkpoint : checkpoints) {
SavedTerrainChunk.verifyCheckpoint(engine.getWorld().worldFolder().toPath(),
checkpoint.chunkX(), checkpoint.chunkZ(), checkpoint.status(), checkpoint.receipt(),
checkpoint.structureActivation());
}
} catch (IOException failure) {
throw new CompletionException(failure);
}
}
private record Checkpoint(int chunkX, int chunkZ, String status, byte[] receipt, long structureActivation) {
}
private static ServerLevel requireLevel(Engine engine) {
if (!(engine.getWorld().platformWorld().nativeHandle() instanceof ServerLevel level)) {
throw new IllegalStateException("The level is unavailable for terrain capture.");
}
return level;
}
private static void runOwned(ServerLevel level, Runnable action) {
if (level.getServer().isSameThread()) {
action.run();
} else {
level.getServer().execute(action);
}
}
}
@@ -191,6 +191,8 @@ public final class ModdedWorldEngines {
ModdedGenerationHistoryStorage.ActivePack opened
) throws IOException {
GenerationHistory history = opened.history();
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
opened = ModdedGenerationHistoryStorage.resolveActive(history);
long openedActivationId = history.activeActivation().activationId();
IrisEngine candidate = buildEngine(level, seed, opened);
boolean ready = false;
@@ -0,0 +1,11 @@
package art.arcane.iris.modded;
public interface NativeTerrainReceiptHolder {
long iris$getStructureActivation();
void iris$setStructureActivation(long activation);
byte[] iris$getNaturalTerrain();
void iris$setNaturalTerrain(byte[] receipt);
}
@@ -25,6 +25,7 @@ import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus;
import art.arcane.iris.core.pregenerator.methods.CachedPregenMethod;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.format.Form;
@@ -64,7 +65,9 @@ public final class ModdedPregenJob {
ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync);
PregeneratorMethod method = moddedMethod;
if (cached) {
method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task);
method = new CachedPregenMethod(new CachedPregenMethod.Configuration(method,
PregenCache.create(cacheDirectory(level)).sync(), task,
PregenSavedChunkStatus.fromWorld(engine.getWorld().worldFolder().toPath())));
}
ActivePregen active = new ActivePregen(engine, moddedMethod);
ACTIVE.set(active);
@@ -0,0 +1,18 @@
package art.arcane.iris.modded.mixin;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.world.level.chunk.ChunkAccess;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(ChunkMap.class)
public interface ChunkMapTerrainReceiptAccess {
@Invoker("save")
boolean iris$saveTerrainCheckpoint(ChunkAccess chunk);
@Accessor("updatingChunkMap")
Long2ObjectLinkedOpenHashMap<ChunkHolder> iris$updatingChunks();
}
@@ -0,0 +1,37 @@
package art.arcane.iris.modded.mixin;
import art.arcane.iris.modded.NativeTerrainReceiptHolder;
import net.minecraft.world.level.chunk.ChunkAccess;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(ChunkAccess.class)
public abstract class ChunkTerrainReceiptMixin implements NativeTerrainReceiptHolder {
@Unique
private byte[] iris$naturalTerrain;
@Unique
private long iris$structureActivation;
@Override
public long iris$getStructureActivation() {
return iris$structureActivation;
}
@Override
public void iris$setStructureActivation(long activation) {
if (activation < 0) {
throw new IllegalArgumentException("Native structure activation cannot be negative");
}
iris$structureActivation = activation;
}
@Override
public byte[] iris$getNaturalTerrain() {
return iris$naturalTerrain == null ? null : iris$naturalTerrain.clone();
}
@Override
public void iris$setNaturalTerrain(byte[] receipt) {
iris$naturalTerrain = receipt == null ? null : receipt.clone();
}
}
@@ -0,0 +1,21 @@
package art.arcane.iris.modded.mixin;
import art.arcane.iris.modded.ModdedNativeTerrainReceipts;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.ProtoChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(LevelChunk.class)
public abstract class LevelChunkTerrainReceiptMixin {
@Inject(method = "<init>(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ProtoChunk;Lnet/minecraft/world/level/chunk/LevelChunk$PostLoadProcessor;)V", at = @At("RETURN"))
private void iris$transferTerrain(ServerLevel level, ProtoChunk source, LevelChunk.PostLoadProcessor processor,
CallbackInfo callback) {
ModdedNativeTerrainReceipts.setStructureActivation((LevelChunk) (Object) this,
ModdedNativeTerrainReceipts.structureActivation(source));
ModdedNativeTerrainReceipts.set((LevelChunk) (Object) this, ModdedNativeTerrainReceipts.get(source));
}
}
@@ -0,0 +1,89 @@
package art.arcane.iris.modded.mixin;
import art.arcane.iris.engine.history.NativeTerrainReceipt;
import art.arcane.iris.modded.ModdedNativeTerrainReceipts;
import art.arcane.iris.modded.NativeTerrainReceiptHolder;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(SerializableChunkData.class)
public abstract class SerializableTerrainReceiptMixin implements NativeTerrainReceiptHolder {
@Unique
private byte[] iris$naturalTerrain;
@Unique
private long iris$structureActivation;
@Override
public long iris$getStructureActivation() {
return iris$structureActivation;
}
@Override
public void iris$setStructureActivation(long activation) {
if (activation < 0) {
throw new IllegalArgumentException("Native structure activation cannot be negative");
}
iris$structureActivation = activation;
}
@Override
public byte[] iris$getNaturalTerrain() {
return iris$naturalTerrain == null ? null : iris$naturalTerrain.clone();
}
@Override
public void iris$setNaturalTerrain(byte[] receipt) {
iris$naturalTerrain = receipt == null ? null : receipt.clone();
}
@Inject(method = "parse", at = @At("RETURN"))
private static void iris$parseTerrain(LevelHeightAccessor height, PalettedContainerFactory containers,
CompoundTag tag, CallbackInfoReturnable<SerializableChunkData> callback) {
SerializableChunkData parsed = callback.getReturnValue();
if (parsed != null) {
((NativeTerrainReceiptHolder) (Object) parsed).iris$setStructureActivation(
tag.getLongOr(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, 0));
((NativeTerrainReceiptHolder) (Object) parsed).iris$setNaturalTerrain(
tag.getByteArray(NativeTerrainReceipt.NBT_KEY).orElse(null));
}
}
@Inject(method = "copyOf", at = @At("RETURN"))
private static void iris$copyTerrain(ServerLevel level, ChunkAccess chunk,
CallbackInfoReturnable<SerializableChunkData> callback) {
((NativeTerrainReceiptHolder) (Object) callback.getReturnValue()).iris$setStructureActivation(
ModdedNativeTerrainReceipts.structureActivation(chunk));
((NativeTerrainReceiptHolder) (Object) callback.getReturnValue()).iris$setNaturalTerrain(
ModdedNativeTerrainReceipts.get(chunk));
}
@Inject(method = "read", at = @At("RETURN"))
private void iris$restoreTerrain(ServerLevel level, PoiManager points, RegionStorageInfo storage, ChunkPos position,
CallbackInfoReturnable<ProtoChunk> callback) {
ModdedNativeTerrainReceipts.set(callback.getReturnValue(), iris$naturalTerrain);
ModdedNativeTerrainReceipts.setStructureActivation(callback.getReturnValue(), iris$structureActivation);
}
@Inject(method = "write", at = @At("RETURN"))
private void iris$writeTerrain(CallbackInfoReturnable<CompoundTag> callback) {
if (iris$structureActivation > 0) {
callback.getReturnValue().putLong(NativeTerrainReceipt.STRUCTURE_ACTIVATION_KEY, iris$structureActivation);
}
if (iris$naturalTerrain != null) {
callback.getReturnValue().putByteArray(NativeTerrainReceipt.NBT_KEY, iris$naturalTerrain.clone());
}
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.modded.mixin;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(WorldGenRegion.class)
public abstract class WorldGenerationWriteGuardMixin {
@Inject(method = "ensureCanWrite", at = @At("RETURN"), cancellable = true)
private void iris$guardCompletedTerrain(BlockPos position, CallbackInfoReturnable<Boolean> result) {
WorldGenRegion region = (WorldGenRegion) (Object) this;
if (result.getReturnValue() && region.getLevel().getChunkSource().getGenerator()
instanceof IrisModdedChunkGenerator generator
&& region.getChunk(position.getX() >> 4, position.getZ() >> 4)
.getPersistedStatus().isOrAfter(ChunkStatus.FULL)
&& !generator.allowsNativeChunkWrite(position.getX() >> 4, position.getZ() >> 4)) {
result.setReturnValue(false);
}
}
}
@@ -23,6 +23,10 @@ import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.WorldMaintenance;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.modded.ModdedSavedTerrainCapture;
import java.util.concurrent.CompletableFuture;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.object.IrisDimension;
@@ -64,6 +68,16 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
private volatile ExecutorService executor;
private long lastPollAt;
@Override
public CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
return ModdedSavedTerrainCapture.flush(engine);
}
@Override
public CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) {
return ModdedSavedTerrainCapture.capture(engine, chunkX, chunkZ);
}
@Override
public void onEnable() {
if (executor != null) {
@@ -4,6 +4,11 @@
"package": "art.arcane.iris.modded.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"WorldGenerationWriteGuardMixin",
"ChunkMapTerrainReceiptAccess",
"ChunkTerrainReceiptMixin",
"LevelChunkTerrainReceiptMixin",
"SerializableTerrainReceiptMixin",
"EntityPersistenceMixin",
"LivingEntityLootMixin",
"MobAwarenessMixin",
@@ -19,6 +19,16 @@
package art.arcane.iris.modded;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisCustomBiomeAliasResolver;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionType;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import java.lang.reflect.Constructor;
import java.util.Set;
import org.junit.Test;
import java.io.IOException;
@@ -39,6 +49,33 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedForcedDatapackTest {
@Test
public void retainedDimensionSourceRendersBeforeNativeRegistryExists() throws Exception {
DataFixerV1217 fixer = new DataFixerV1217();
IrisDimensionType type = new IrisDimension().getDimensionType();
String source = type.toJson(fixer);
GenerationRegistryContract.PhysicalResourceKey key = new GenerationRegistryContract.PhysicalResourceKey(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY, "iris:test_type");
GenerationRegistryContract.GeneratedSource saved = new GenerationRegistryContract.GeneratedSource(
GenerationRegistryContractFactory.DIMENSION_TYPE_EFFECTIVE_SOURCE_SCHEMA,
GenerationEpochContractFactory.fingerprintDimensionType(type),
GenerationEpochContractFactory.dimensionTypeSemanticJson(type), "test-renderer",
GenerationRegistryContractFactory.fingerprintDefinition(
key, PlatformGenerationRegistry.Definition.exactJson(source), "generated"), source);
GenerationRegistryContract contract = GenerationRegistryContract.fromDefinitionsAndGeneratedSources(
Map.of(key, GenerationRegistryContractFactory.fingerprintGeneratedSemantic(key, saved)),
Map.of(key, saved));
Class<?> stageType = Class.forName(ModdedForcedDatapack.class.getName() + "$PackStageContext");
Constructor<?> constructor = stageType.getDeclaredConstructor(Path.class, String.class, String.class,
String.class, boolean.class, boolean.class, boolean.class, Set.class, GenerationRegistryContract.class);
constructor.setAccessible(true);
IrisCustomBiomeAliasResolver resolver = (IrisCustomBiomeAliasResolver) constructor.newInstance(
Path.of("retained"), "test", "test", "iris:test_type", false, false, true, Set.of(), contract);
assertEquals(source, resolver.generatedSource(key.registryKey(), key.resourceKey(), "{}", fixer));
assertThrows(IOException.class, () -> resolver.generatedSource(
key.registryKey(), "iris:missing_type", "{}", fixer));
}
@Test
public void packScopedIdsCannotCollideAndHaveReadableLabels() {
String first = ModdedWorldgenIds.presetRef("overworld", "overworld");
@@ -25,7 +25,15 @@ public class ModdedGenerationHistoryRoutingContractTest {
assertTrue(createHistoryEngine.contains("IrisBoundarySignatureSampler.INSTANCE"));
assertTrue(createHistoryEngine.contains("getGenerationTransitionWidthBlocks()"));
assertTrue(createHistoryEngine.contains("router.preloadActiveRuntimes();"));
assertBefore(createHistoryEngine, "close(candidate);", "ModdedGenerationHistoryStorage.resolveActive(history)");
assertBefore(createHistoryEngine, "history.prepareCurrentGenerator(",
"opened = ModdedGenerationHistoryStorage.resolveActive(history)");
assertBefore(createHistoryEngine, "opened = ModdedGenerationHistoryStorage.resolveActive(history)",
"IrisEngine candidate = buildEngine(level, seed, opened)");
String promoted = method(createHistoryEngine,
"if (history.activeActivation().activationId() != openedActivationId)");
assertBefore(promoted, "close(candidate);", "ModdedGenerationHistoryStorage.resolveActive(history)");
assertBefore(promoted, "ModdedGenerationHistoryStorage.resolveActive(history)",
"candidate = buildEngine(level, seed, promoted)");
assertTrue(createHistoryEngine.contains("GenerationHistoryRuntimeRouter.attach("));
assertTrue(buildEngine.contains("active.packRoot().toFile()"));
assertTrue(buildEngine.contains("history.paths().activationMantleRoot(activation.activationId())"));
@@ -84,10 +92,16 @@ public class ModdedGenerationHistoryRoutingContractTest {
assertTrue(fill.contains("generateTerrain(chunk, generationEngine, pos, air, route)"));
assertTrue(fill.contains("return closeRouteOnCompletion(pipeline, route);"));
assertTrue(terrain.contains("openHistoryRuntimeScope(route)"));
assertBefore(terrain, "generationEngine.generate(", "route.claimGeneratedSemantics();");
assertBefore(terrain, "route.claimGeneratedSemantics();", "return chunk;");
assertBefore(terrain, "generationEngine.generate(", "writeBlocks(chunk, blocks, dimMinY, height)");
assertBefore(terrain, "writeBlocks(chunk, blocks, dimMinY, height)", "route.claimGeneratedSemantics(");
assertTrue(terrain.contains("chunk.getBlockState(position.set(x, dimMinY + y, z))"));
assertTrue(terrain.contains("state.isAir() || state.liquid()"));
assertBefore(terrain, "route.claimGeneratedSemantics(", "ModdedNativeTerrainReceipts.persist(chunk, route)");
assertBefore(terrain, "ModdedNativeTerrainReceipts.persist(chunk, route)", "return chunk;");
assertTrue(completion.contains("pipeline.whenComplete("));
assertTrue(completion.contains("route.close();"));
assertBefore(completion, "pipeline.whenComplete(", "route.detachThread()");
assertBefore(completion, "route.detachThread()", "return completion;");
}
@Test
@@ -108,7 +122,7 @@ public class ModdedGenerationHistoryRoutingContractTest {
}
@Test
public void discreteStagesResumeIncompleteHistoricalChunksAndRejectTransitionOwnership() throws IOException {
public void pendingNativeStagesPreserveSavedStartsAndGuardNewFeatureFootprints() throws IOException {
String generator = source("IrisModdedChunkGenerator.java");
String discrete = method(generator, "private boolean allowsRoutedDiscreteGeneration(");
String decoration = method(generator, "public void applyBiomeDecoration(");
@@ -117,19 +131,28 @@ public class ModdedGenerationHistoryRoutingContractTest {
String nativeStage = source("ModdedNativeStructureStage.java");
String adjust = method(nativeStage, "void adjustGeneratedStructures(");
String place = method(nativeStage, "void placeVanillaStructures(");
String savedStart = method(nativeStage, "private boolean isHistoricalStructureStart(");
assertTrue(discrete.contains("chunk.getPersistedStatus().isOrAfter(stage)"));
assertFalse(discrete.contains("router.history().activeActivation().activationId()"));
assertFalse(discrete.contains("return true;"));
assertTrue(discrete.contains("allowsNewGenerationChunk("));
assertTrue(discrete.contains("NativeGenerationWriteGuard.allowsPendingStage(current, chunk, stage)"));
assertTrue(decoration.contains("ChunkStatus.FEATURES"));
assertTrue(structures.contains("ChunkStatus.STRUCTURE_STARTS"));
assertTrue(mobs.contains("ChunkStatus.SPAWN"));
assertBefore(decoration, "allowsRoutedDiscreteGeneration(", "nativeStructures.placeVanillaStructures(");
assertBefore(decoration, "chunk.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)",
"nativeStructures.placeVanillaStructures(");
assertBefore(decoration, "nativeStructures.placeVanillaStructures(", "allowsRoutedDiscreteGeneration(");
assertBefore(decoration, "allowsRoutedDiscreteGeneration(", "importedFeatures.run(");
assertBefore(decoration, "NativeGenerationWriteGuard.allowsDecoration(", "importedFeatures.run(");
assertBefore(structures, "allowsRoutedDiscreteGeneration(", "super.createStructures(");
assertBefore(mobs, "allowsRoutedDiscreteGeneration(", "NaturalSpawner.spawnMobsForChunkGeneration(");
assertBefore(adjust, "allowsNewGenerationFootprint(", "recordWorldCheckStructureShift(");
assertTrue(place.contains("allowsNewGenerationFootprint("));
assertBefore(place, "!isHistoricalStructureStart(current, world, start)", "allowsNewGenerationFootprint(");
assertTrue(savedStart.contains("world.getChunk(origin.x(), origin.z(), ChunkStatus.EMPTY, false)"));
assertTrue(savedStart.contains("ModdedNativeTerrainReceipts.structureActivation(source)"));
assertBefore(structures, "super.createStructures(", "ModdedNativeTerrainReceipts.persistStructureActivation(");
}
private static void assertRuntimeRoute(String source, String signature) {
+1 -1
View File
@@ -26,7 +26,7 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
apply from: file('../../gradle/modded-generation-build-revision.gradle')
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
+3 -2
View File
@@ -113,6 +113,7 @@ nmsBindings.each { key, value ->
compileOnly(rootProject.libs.annotations)
compileOnly(rootProject.libs.byteBuddy.core)
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(project(':core'))
testCompileOnly(volmLibCoordinate) {
transitive = false
@@ -427,7 +428,7 @@ tasks.named('neoforgeJar').configure {
List<String> commonModdedArtifactEntries = [
'META-INF/iris/generation-kernels/catalog.tsv',
'META-INF/iris/generation-kernels/abi-1.seal',
'META-INF/iris/generation-kernels/abi-1.revision',
'art/arcane/iris/engine/IrisEngine.class',
'art/arcane/iris/spi/IrisPlatform.class',
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
@@ -533,7 +534,7 @@ tasks.register('verifyModdedArtifacts') {
// them, so a shadowJar exclude that drops one is silent until a server hits that path.
List<String> requiredBukkitArtifactEntries = [
'META-INF/iris/generation-kernels/catalog.tsv',
'META-INF/iris/generation-kernels/abi-1.seal',
'META-INF/iris/generation-kernels/abi-1.revision',
'plugin.yml',
'paper-plugin.yml',
'slimjar.dat',
@@ -24,18 +24,18 @@ import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class GenerationKernelSeal {
public static final String FORMAT = "iris-generation-kernel-seal-v1";
public final class GenerationBuildRevision {
public static final String FORMAT = "iris-generation-build-revision-v1";
private static final Pattern HASH = Pattern.compile("[0-9a-f]{64}");
private static final long MAXIMUM_SEAL_BYTES = 8L * 1024L * 1024L;
private static final long MAXIMUM_REVISION_BYTES = 8L * 1024L * 1024L;
private static final int MAXIMUM_ENTRIES = 30_000;
private GenerationKernelSeal() {
private GenerationBuildRevision() {
}
public static void main(String[] arguments) throws IOException {
if (arguments.length < 4 || arguments.length % 2 != 0 || !arguments[0].equals("verify-dependencies")) {
throw new IllegalArgumentException("Expected verify-dependencies <seal> <identity> <artifact> pairs.");
throw new IllegalArgumentException("Expected verify-dependencies <revision> <identity> <artifact> pairs.");
}
Map<String, Path> dependencies = new TreeMap<>();
for (int index = 2; index < arguments.length; index += 2) {
@@ -46,8 +46,8 @@ public final class GenerationKernelSeal {
verifyDependencySubset(Path.of(arguments[1]), dependencies);
}
public static void verifyDependencySubset(Path seal, Map<String, Path> dependencies) throws IOException {
SourceManifest expected = read(seal);
public static void verifyDependencySubset(Path revision, Map<String, Path> dependencies) throws IOException {
SourceManifest expected = read(revision);
for (Map.Entry<String, String> dependency : dependencyHashes(dependencies).entrySet()) {
if (!Objects.equals(expected.dependencies().get(dependency.getKey()), dependency.getValue())) {
throw new IOException("Resolved platform dependency does not match generation ABI " + expected.abi()
@@ -75,7 +75,7 @@ public final class GenerationKernelSeal {
AlgorithmVersion algorithms = new AlgorithmVersion(positive(current[2]), positive(current[3]));
SourceManifest selected = manifests.get(currentAbi);
if (selected == null || !selected.algorithms().contains(algorithms)) {
throw new IllegalArgumentException("Current generation version has no matching sealed factory.");
throw new IllegalArgumentException("Current generation version has no matching build factory.");
}
TreeSet<Integer> registered = new TreeSet<>();
int previous = 0;
@@ -92,7 +92,7 @@ public final class GenerationKernelSeal {
registered.add(abi);
}
if (!registered.equals(manifests.keySet())) {
throw new IllegalArgumentException("Kernel catalog must register exactly the retained sealed kernels.");
throw new IllegalArgumentException("Kernel catalog must register exactly the packaged kernels.");
}
} catch (IllegalArgumentException failure) {
throw new IOException("Invalid generation kernel catalog.", failure);
@@ -116,33 +116,32 @@ public final class GenerationKernelSeal {
sourceHashes, dependencyHashes(dependencies));
}
public static void verify(Path repository, Path seal, Map<String, Path> dependencies) throws IOException {
SourceManifest expected = read(seal);
public static void verifySnapshot(Path repository, Path revision, Map<String, Path> dependencies) throws IOException {
SourceManifest expected = read(revision);
SourceManifest actual = capture(new CaptureOptions(repository, expected.abi(), expected.factoryClass(), expected.algorithms(),
expected.roots(), expected.exclusions()), dependencies);
List<String> failures = new ArrayList<>();
differences("source", expected.sources(), actual.sources(), failures);
differences("dependency", expected.dependencies(), actual.dependencies(), failures);
if (!failures.isEmpty()) {
throw new IOException("Generation ABI " + expected.abi() + " no longer matches its sealed implementation:\n"
+ String.join("\n", failures)
+ "\nRetain this kernel's implementation and add a separately versioned kernel for changed generation.");
throw new IOException("Generation build inputs changed after revision capture:\n"
+ String.join("\n", failures));
}
}
public static void writeNew(Path destination, SourceManifest manifest) throws IOException {
public static void write(Path destination, SourceManifest manifest) throws IOException {
Files.createDirectories(destination.toAbsolutePath().getParent());
Files.writeString(destination, encode(manifest), StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
}
public static SourceManifest read(Path path) throws IOException {
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > MAXIMUM_SEAL_BYTES) {
throw new IOException("Missing or oversized generation kernel seal: " + path);
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > MAXIMUM_REVISION_BYTES) {
throw new IOException("Missing or oversized generation build revision: " + path);
}
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
if (lines.size() < 4 || !FORMAT.equals(lines.getFirst()) || lines.size() > MAXIMUM_ENTRIES) {
throw new IOException("Invalid generation kernel seal: " + path);
throw new IOException("Invalid generation build revision: " + path);
}
int abi = 0;
String factory = null;
@@ -155,7 +154,7 @@ public final class GenerationKernelSeal {
for (int index = 1; index < lines.size(); index++) {
String[] entry = lines.get(index).split("\t", -1);
if (entry.length < 2 || entry.length > 3) {
throw new IllegalArgumentException("Invalid seal entry.");
throw new IllegalArgumentException("Invalid revision entry.");
}
switch (entry[0]) {
case "abi" -> {
@@ -180,16 +179,16 @@ public final class GenerationKernelSeal {
case "exclude" -> addPath(exclusions, entry);
case "source" -> addHash(sources, entry, true);
case "dependency" -> addHash(dependencies, entry, false);
default -> throw new IllegalArgumentException("Unknown generation kernel seal entry.");
default -> throw new IllegalArgumentException("Unknown generation build revision entry.");
}
}
SourceManifest manifest = new SourceManifest(abi, factory, algorithms, roots, exclusions, sources, dependencies);
if (!Files.readString(path, StandardCharsets.UTF_8).equals(encode(manifest))) {
throw new IllegalArgumentException("Generation kernel seal is not canonical.");
throw new IllegalArgumentException("Generation build revision is not canonical.");
}
return manifest;
} catch (RuntimeException failure) {
throw new IOException("Invalid generation kernel seal: " + path, failure);
throw new IOException("Invalid generation build revision: " + path, failure);
}
}
@@ -352,11 +351,11 @@ public final class GenerationKernelSeal {
private static void addHash(Map<String, String> hashes, String[] entry, boolean path) {
if (entry.length != 3 || !HASH.matcher(entry[2]).matches()) {
throw new IllegalArgumentException("Invalid sealed hash.");
throw new IllegalArgumentException("Invalid revision hash.");
}
String key = path ? relative(entry[1]) : token(entry[1]);
if (hashes.putIfAbsent(key, entry[2]) != null) {
throw new IllegalArgumentException("Duplicate sealed entry.");
throw new IllegalArgumentException("Duplicate revision entry.");
}
}
@@ -377,9 +376,9 @@ public final class GenerationKernelSeal {
}
private static String token(String value) {
String required = Objects.requireNonNull(value, "seal token");
String required = Objects.requireNonNull(value, "revision token");
if (required.isBlank() || required.chars().anyMatch(Character::isWhitespace)) {
throw new IllegalArgumentException("Invalid generation seal token.");
throw new IllegalArgumentException("Invalid generation revision token.");
}
return required;
}
@@ -442,7 +441,7 @@ public final class GenerationKernelSeal {
sources = Map.copyOf(sources);
dependencies = Map.copyOf(dependencies);
if (sources.isEmpty() || dependencies.isEmpty() || sources.size() + dependencies.size() > MAXIMUM_ENTRIES) {
throw new IllegalArgumentException("Invalid generation kernel seal entry count.");
throw new IllegalArgumentException("Invalid generation build revision entry count.");
}
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.buildtools;
import java.nio.file.Path;
import java.util.List;
public final class GenerationRevisionScope {
private static final List<String> SOURCE_ROOTS = List.of(
"lombok.config",
"core/agent/build.gradle",
"core/agent/src/main/java",
"core/src/main/java",
"spi/src/main/java",
"adapters/bukkit/plugin/src/main/java",
"adapters/bukkit/nms/v26_2_R1/src/main/java",
"adapters/minecraft-common/src/main/java",
"adapters/modded-common/src/main/java",
"adapters/client-common/src/main/java",
"adapters/fabric/src/main/java",
"adapters/forge/src/main/java",
"adapters/neoforge/src/main/java"
);
private static final List<String> OPERATIONAL_EXCLUSIONS = List.of(
"core/src/main/java/art/arcane/iris/engine/EngineDiagnostics.java",
"core/src/main/java/art/arcane/iris/engine/EngineMetricsReport.java",
"core/src/main/java/art/arcane/iris/core/IrisStartupValidation.java",
"core/src/main/java/art/arcane/iris/core/safeguard/IrisSafeguard.java",
"core/src/main/java/art/arcane/iris/core/safeguard/Mode.java",
"core/src/main/java/art/arcane/iris/core/safeguard/task/Task.java",
"core/src/main/java/art/arcane/iris/core/safeguard/task/Tasks.java",
"core/src/main/java/art/arcane/iris/core/safeguard/task/Diagnostic.java",
"core/src/main/java/art/arcane/iris/core/safeguard/task/ValueWithDiagnostics.java",
"core/src/main/java/art/arcane/iris/core/splash/IrisSplashRenderer.java",
"adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsWorldLifecycle.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandIris.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandJigsaw.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStudio.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandObject.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/PaperCommandRegistrar.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPack.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandStructure.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandWhat.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandFind.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandEdit.java",
"adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java"
);
private GenerationRevisionScope() {
}
public static GenerationBuildRevision.CaptureOptions current(Path repository) {
return new GenerationBuildRevision.CaptureOptions(repository, 1,
"art.arcane.iris.engine.history.GenerationKernelV1",
List.of(new GenerationBuildRevision.AlgorithmVersion(1, 1)), SOURCE_ROOTS, OPERATIONAL_EXCLUSIONS);
}
}
@@ -1,4 +1,4 @@
import art.arcane.iris.buildtools.GenerationKernelSeal;
import art.arcane.iris.buildtools.GenerationBuildRevision;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -17,20 +17,20 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationKernelSealTest {
public class GenerationBuildRevisionTest {
@Rule
public final TemporaryFolder temporary = new TemporaryFolder();
@Test
public void verifiesUnchangedSourceAndCanonicalDependencyContents() throws Exception {
Fixture fixture = fixture();
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
String original = GenerationBuildRevision.artifactHash(fixture.dependency());
artifact(fixture.dependency(), "same code", "another version", 1_700_000_000_000L);
assertEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
assertEquals(GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())),
GenerationKernelSeal.fingerprint(GenerationKernelSeal.capture(fixture.options(), fixture.dependencies())));
assertEquals(original, GenerationBuildRevision.artifactHash(fixture.dependency()));
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
assertEquals(GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())),
GenerationBuildRevision.fingerprint(GenerationBuildRevision.capture(fixture.options(), fixture.dependencies())));
}
@Test
@@ -39,120 +39,118 @@ public class GenerationKernelSealTest {
Path source = fixture.root().resolve("generation/Noise.java");
Files.writeString(source, "class Noise { int value = 2; }");
assertTrue(assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()))
.getMessage().contains("source changed"));
Files.writeString(source, "class Noise { int value = 1; }");
Path added = fixture.root().resolve("generation/Added.java");
Files.writeString(added, "class Added {}");
assertTrue(assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()))
.getMessage().contains("source added"));
Files.delete(added);
Files.delete(source);
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()));
}
@Test
public void rejectsChangedAddedAndDeletedDependencies() throws Exception {
Fixture fixture = fixture();
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
String original = GenerationBuildRevision.artifactHash(fixture.dependency());
artifact(fixture.dependency(), "changed code", "same version", 0L);
assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
assertNotEquals(original, GenerationBuildRevision.artifactHash(fixture.dependency()));
assertTrue(assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()))
.getMessage().contains("dependency changed"));
artifact(fixture.dependency(), "same code", "same version", 0L);
assertThrows(IOException.class, () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(),
assertThrows(IOException.class, () -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(),
Map.of("math", fixture.dependency(), "new-math", fixture.dependency())));
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), Map.of()));
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), Map.of()));
}
@Test
public void rejectsMultiReleaseManifestChanges() throws Exception {
Fixture fixture = fixture();
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
String original = GenerationBuildRevision.artifactHash(fixture.dependency());
artifact(fixture.dependency(), "same code", "same version\r\nMulti-Release: true", 0L);
assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
assertNotEquals(original, GenerationBuildRevision.artifactHash(fixture.dependency()));
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()));
}
@Test
public void rejectsAgentEntrypointAndCapabilityChanges() throws Exception {
Fixture fixture = fixture();
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
String original = GenerationBuildRevision.artifactHash(fixture.dependency());
for (String attribute : List.of("Agent-Class", "Premain-Class", "Launcher-Agent-Class", "Boot-Class-Path",
"Can-Redefine-Classes", "Can-Retransform-Classes", "Can-Set-Native-Method-Prefix")) {
artifact(fixture.dependency(), "same code", "same version\r\n" + attribute + ": changed", 0L);
assertNotEquals(attribute, original, GenerationKernelSeal.artifactHash(fixture.dependency()));
assertNotEquals(attribute, original, GenerationBuildRevision.artifactHash(fixture.dependency()));
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()));
}
}
@Test
public void sealsExplicitAgentBuildInputsAndExecutableSources() throws Exception {
public void capturesExplicitAgentBuildInputsAndExecutableSources() throws Exception {
Fixture fixture = fixture();
Path agentSource = fixture.root().resolve("agent/src/main/java/Installer.java");
Path agentBuild = fixture.root().resolve("agent/build.gradle");
Files.createDirectories(agentSource.getParent());
Files.writeString(agentSource, "class Installer {}");
Files.writeString(agentBuild, "plugins { id 'java' }");
GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions(
fixture.root(), 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)),
GenerationBuildRevision.CaptureOptions options = new GenerationBuildRevision.CaptureOptions(
fixture.root(), 1, "example.KernelV1", List.of(new GenerationBuildRevision.AlgorithmVersion(1, 1)),
List.of("generation", "agent/build.gradle", "agent/src/main/java"), List.of("generation/kernels"));
Path seal = fixture.root().resolve("agent.seal");
GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, fixture.dependencies()));
GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies());
Path revision = fixture.root().resolve("agent.revision");
GenerationBuildRevision.write(revision, GenerationBuildRevision.capture(options, fixture.dependencies()));
GenerationBuildRevision.verifySnapshot(fixture.root(), revision, fixture.dependencies());
Files.writeString(agentBuild, "plugins { id 'application' }");
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies()));
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), revision, fixture.dependencies()));
Files.writeString(agentBuild, "plugins { id 'java' }");
Files.writeString(agentSource, "class Installer { boolean defers; }");
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies()));
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), revision, fixture.dependencies()));
}
@Test
public void checksTheActualPlatformDependencySubset() throws Exception {
Fixture fixture = fixture();
GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies());
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(),
GenerationBuildRevision.verifyDependencySubset(fixture.revision(), fixture.dependencies());
assertThrows(IOException.class, () -> GenerationBuildRevision.verifyDependencySubset(fixture.revision(),
Map.of("unknown-library", fixture.dependency())));
artifact(fixture.dependency(), "platform override", "same version", 0L);
assertThrows(IOException.class,
() -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies()));
() -> GenerationBuildRevision.verifyDependencySubset(fixture.revision(), fixture.dependencies()));
}
@Test
public void catalogCannotSelectAnUnsealedAlgorithmTuple() throws Exception {
public void catalogRequiresAPackagedAlgorithmTuple() throws Exception {
Fixture fixture = fixture();
Path catalog = fixture.root().resolve("catalog.tsv");
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\n");
Map<Integer, GenerationKernelSeal.SourceManifest> manifests = Map.of(1, GenerationKernelSeal.read(fixture.seal()));
GenerationKernelSeal.verifyCatalog(catalog, manifests);
Map<Integer, GenerationBuildRevision.SourceManifest> manifests = Map.of(1, GenerationBuildRevision.read(fixture.revision()));
GenerationBuildRevision.verifyCatalog(catalog, manifests);
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t2\t1\nkernel\t1\n");
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyCatalog(catalog, manifests));
assertThrows(IOException.class, () -> GenerationBuildRevision.verifyCatalog(catalog, manifests));
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\nkernel\t2\n");
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyCatalog(catalog, manifests));
assertThrows(IOException.class, () -> GenerationBuildRevision.verifyCatalog(catalog, manifests));
}
@Test
public void permitsSeparatelyScopedFutureKernelsWithoutChangingRetainedSeal() throws Exception {
public void recapturesChangedGenerationWithoutChangingAbi() throws Exception {
Fixture fixture = fixture();
Path future = fixture.root().resolve("generation/kernels/v2/NewMath.java");
Files.createDirectories(future.getParent());
Files.writeString(future, "class NewMath {}");
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
GenerationKernelSeal.SourceManifest next = GenerationKernelSeal.capture(new GenerationKernelSeal.CaptureOptions(
fixture.root(), 2, "example.KernelV2", List.of(new GenerationKernelSeal.AlgorithmVersion(2, 3)),
List.of("generation/kernels/v2"), List.of()), fixture.dependencies());
assertNotEquals(GenerationKernelSeal.fingerprint(next),
GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())));
assertThrows(IOException.class,
() -> GenerationKernelSeal.writeNew(fixture.seal(), next));
String original = GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision()));
Files.writeString(fixture.root().resolve("generation/Noise.java"), "class Noise { int value = 9; }");
artifact(fixture.dependency(), "new generation code", "new version", 0L);
GenerationBuildRevision.SourceManifest next = GenerationBuildRevision.capture(fixture.options(), fixture.dependencies());
GenerationBuildRevision.write(fixture.revision(), next);
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
assertEquals(1, next.abi());
assertNotEquals(original, GenerationBuildRevision.fingerprint(next));
assertEquals(next, GenerationBuildRevision.read(fixture.revision()));
}
private Fixture fixture() throws Exception {
@@ -161,13 +159,13 @@ public class GenerationKernelSealTest {
Files.writeString(root.resolve("generation/Noise.java"), "class Noise { int value = 1; }");
Path dependency = root.resolve("math.jar");
artifact(dependency, "same code", "same version", 0L);
GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions(
root, 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)),
GenerationBuildRevision.CaptureOptions options = new GenerationBuildRevision.CaptureOptions(
root, 1, "example.KernelV1", List.of(new GenerationBuildRevision.AlgorithmVersion(1, 1)),
List.of("generation"), List.of("generation/kernels"));
Path seal = root.resolve("abi-1.seal");
Path revision = root.resolve("abi-1.revision");
Map<String, Path> dependencies = Map.of("math", dependency);
GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, dependencies));
return new Fixture(root, seal, dependency, dependencies, options);
GenerationBuildRevision.write(revision, GenerationBuildRevision.capture(options, dependencies));
return new Fixture(root, revision, dependency, dependencies, options);
}
private static void artifact(Path path, String content, String version, long timestamp) throws IOException {
@@ -186,7 +184,7 @@ public class GenerationKernelSealTest {
}
}
private record Fixture(Path root, Path seal, Path dependency, Map<String, Path> dependencies,
GenerationKernelSeal.CaptureOptions options) {
private record Fixture(Path root, Path revision, Path dependency, Map<String, Path> dependencies,
GenerationBuildRevision.CaptureOptions options) {
}
}
@@ -0,0 +1,163 @@
import art.arcane.iris.buildtools.GenerationRevisionScope;
import art.arcane.iris.buildtools.GenerationBuildRevision;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class GenerationRevisionScopeTest {
private static final String ENGINE_ROOT = "core/src/main/java/art/arcane/iris/engine/";
private static final String NMS_ROOT = "adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/";
private static final String COMMAND_ROOT = "adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/";
@Rule
public final TemporaryFolder temporary = new TemporaryFolder();
@Test
public void operationalChangesPreserveTheGenerationFingerprint() throws Exception {
Fixture fixture = fixture();
String original = GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision()));
for (String exclusion : fixture.options().exclusions()) {
if (!exclusion.endsWith(".java")) {
continue;
}
Path operationalSource = fixture.root().resolve(exclusion);
write(operationalSource, "class Operational { String message = \"updated\"; }");
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
assertEquals(exclusion, original, GenerationBuildRevision.fingerprint(
GenerationBuildRevision.capture(fixture.options(), fixture.dependencies())));
Files.delete(operationalSource);
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
}
}
@Test
public void generationChangesProduceANewBuildRevision() throws Exception {
Fixture fixture = fixture();
for (String relative : generationSources()) {
Path source = fixture.root().resolve(relative);
Files.writeString(source, "class Generation { int sample = 2; }");
GenerationBuildRevision.SourceManifest changed = GenerationBuildRevision.capture(fixture.options(), fixture.dependencies());
assertNotEquals(relative, GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())),
GenerationBuildRevision.fingerprint(changed));
GenerationBuildRevision.write(fixture.revision(), changed);
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
Files.writeString(source, "class Generation { int sample = 1; }");
GenerationBuildRevision.write(fixture.revision(), GenerationBuildRevision.capture(fixture.options(), fixture.dependencies()));
}
}
@Test
public void newSourcesChangeRevisionAcrossPlatforms() throws Exception {
Fixture fixture = fixture();
for (String root : fixture.options().roots()) {
if (!root.endsWith("/java")) {
continue;
}
assertNewSourceChangesRevision(fixture, root + "/NewSource.java");
}
assertNewSourceChangesRevision(fixture, ENGINE_ROOT + "NewDiagnostics.java");
assertNewSourceChangesRevision(fixture, NMS_ROOT + "NmsWorldLifecycleHelper.java");
assertNewSourceChangesRevision(fixture, COMMAND_ROOT + "NewCommand.java");
assertNewSourceChangesRevision(fixture, "core/src/main/java/art/arcane/iris/core/safeguard/task/NewTask.java");
}
@Test
public void onlyExplicitOperationalFilesAreExcluded() throws Exception {
Fixture fixture = fixture();
List<String> directoryExclusions = fixture.options().exclusions().stream()
.filter(path -> !path.endsWith(".java"))
.toList();
assertTrue(directoryExclusions.isEmpty());
GenerationBuildRevision.SourceManifest manifest = GenerationBuildRevision.read(fixture.revision());
for (String relative : generationSources()) {
assertTrue(relative, manifest.sources().containsKey(relative));
}
assertFalse(manifest.sources().containsKey(ENGINE_ROOT + "EngineDiagnostics.java"));
assertFalse(manifest.sources().containsKey(NMS_ROOT + "NmsWorldLifecycle.java"));
assertFalse(manifest.sources().containsKey(COMMAND_ROOT + "CommandIris.java"));
}
private static void assertNewSourceChangesRevision(Fixture fixture, String relative) throws IOException {
Path source = fixture.root().resolve(relative);
write(source, "class NewSource {}");
GenerationBuildRevision.SourceManifest changed = GenerationBuildRevision.capture(fixture.options(), fixture.dependencies());
assertTrue(changed.sources().containsKey(relative));
assertNotEquals(GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())),
GenerationBuildRevision.fingerprint(changed));
Files.delete(source);
}
private Fixture fixture() throws Exception {
Path root = temporary.newFolder().toPath();
GenerationBuildRevision.CaptureOptions options = GenerationRevisionScope.current(root);
for (String scope : options.roots()) {
Path path = root.resolve(scope);
if (scope.endsWith("/java")) {
Files.createDirectories(path);
} else {
write(path, "build input");
}
}
for (String relative : generationSources()) {
write(root.resolve(relative), "class Generation { int sample = 1; }");
}
for (String exclusion : options.exclusions()) {
if (exclusion.endsWith(".java")) {
write(root.resolve(exclusion), "class Operational {}");
}
}
Path dependency = root.resolve("dependency.jar");
try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(dependency))) {
output.putNextEntry(new ZipEntry("generation/Math.class"));
output.write("generation dependency".getBytes(StandardCharsets.UTF_8));
output.closeEntry();
}
Map<String, Path> dependencies = Map.of("volmlib", dependency);
Path revision = root.resolve("abi-1.revision");
GenerationBuildRevision.write(revision, GenerationBuildRevision.capture(options, dependencies));
return new Fixture(root, revision, options, dependencies);
}
private static List<String> generationSources() {
return List.of(
ENGINE_ROOT + "IrisEngine.java",
ENGINE_ROOT + "IrisComplex.java",
ENGINE_ROOT + "IrisEngineMantle.java",
ENGINE_ROOT + "EngineRuntimeBuilder.java",
ENGINE_ROOT + "history/GenerationKernelV1.java",
ENGINE_ROOT + "object/IrisDimension.java",
ENGINE_ROOT + "mode/ModeOverworld.java",
"core/src/main/java/art/arcane/iris/core/loader/IrisData.java",
"core/src/main/java/art/arcane/iris/core/IrisSettings.java",
"core/agent/src/main/java/GenerationTransformer.java",
NMS_ROOT + "NmsGenerationHooks.java",
NMS_ROOT + "NmsGenerationRegistry.java",
NMS_ROOT + "NMSBinding.java",
"spi/src/main/java/art/arcane/iris/spi/PlatformGenerationRegistry.java",
"adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java"
);
}
private static void write(Path path, String content) throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(path, content);
}
private record Fixture(Path root, Path revision, GenerationBuildRevision.CaptureOptions options,
Map<String, Path> dependencies) {
}
}
+1 -1
View File
@@ -63,7 +63,7 @@ public class JarCompactorTest {
writeEntry(output, "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\r\nMulti-Release: true\r\n\r\n"
.getBytes(StandardCharsets.UTF_8));
writeEntry(output, "JarCompactorTest.class", classResource.readAllBytes());
writeEntry(output, "META-INF/iris/generation-kernels/abi-1.seal", "registry fixture".getBytes(StandardCharsets.UTF_8));
writeEntry(output, "META-INF/iris/generation-kernels/abi-1.revision", "registry fixture".getBytes(StandardCharsets.UTF_8));
writeEntry(output, "example/caf\u00e9.txt", "unicode name".getBytes(StandardCharsets.UTF_8));
for (int index = 0; index < 100; index++) {
writeEntry(output, "example/item-" + index + ".txt", ("value-" + index).getBytes(StandardCharsets.UTF_8));
+1 -1
View File
@@ -522,4 +522,4 @@ tasks.named('check').configure {
dependsOn('bukkitPurityRatchet')
}
apply from: rootProject.file('gradle/generation-kernel-seals.gradle')
apply from: rootProject.file('gradle/generation-build-revision.gradle')
File diff suppressed because it is too large Load Diff
@@ -22,6 +22,11 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import com.google.gson.JsonObject;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -53,7 +58,7 @@ import java.util.regex.Pattern;
import java.util.stream.Stream;
public final class IrisDatapackCompiler {
private static final int INPUT_FINGERPRINT_SCHEMA = 3;
private static final int INPUT_FINGERPRINT_SCHEMA = 4;
private static final int INPUT_BUFFER_BYTES = 64 * 1024;
private static final int WORLD_PACK_SCAN_DEPTH = 8;
private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
@@ -177,6 +182,9 @@ public final class IrisDatapackCompiler {
}
}
}
for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) {
updateDigestString(digest, contract.fingerprint());
}
return HexFormat.of().formatHex(digest.digest());
}
@@ -220,6 +228,14 @@ public final class IrisDatapackCompiler {
data.close();
}
}
for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) {
for (PhysicalResourceKey key : contract.generatedSources().keySet()) {
putRegistryRequirement(requirements, registryPath(key) + "/" + key.resourceKey(),
fingerprintContent(GenerationRegistryContractFactory.renderGeneratedSource(contract, key, fixer)));
}
contract.biomeTags().forEach((biome, tags) ->
biomeTags.computeIfAbsent(biome, ignored -> new TreeSet<>()).addAll(tags));
}
addBiomeTagRequirements(requirements, biomeTags);
return Map.copyOf(requirements);
}
@@ -494,11 +510,111 @@ public final class IrisDatapackCompiler {
}
IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight);
installRetainedRegistrySources(packRoots, datapackRoots, fixer);
installLevelStemBindings(normalizedBindings, dimensions, datapackRoots);
validateOutputs(datapackRoots, dimensionCount, normalizedBindings.size());
return new CompilationResult(packCount, dimensionCount, countBiomes(biomes));
}
public static void installRetainedRegistrySources(List<File> packRoots, Collection<File> datapackRoots,
IDataFixer fixer) throws IOException {
Map<PhysicalResourceKey, String> sources = new TreeMap<>();
Map<String, Set<String>> tags = new TreeMap<>();
for (GenerationRegistryContract contract : retainedRegistryContracts(packRoots)) {
for (PhysicalResourceKey key : contract.generatedSources().keySet()) {
String source = GenerationRegistryContractFactory.renderGeneratedSource(contract, key, fixer);
String previous = sources.putIfAbsent(key, source);
if (previous != null && !JsonParser.parseString(previous).equals(JsonParser.parseString(source))) {
throw new IOException("Conflicting retained registry source for " + key);
}
}
contract.biomeTags().forEach((biome, memberships) -> {
for (String tag : memberships) {
tags.computeIfAbsent(tag, ignored -> new TreeSet<>()).add(biome);
}
});
}
for (File root : datapackRoots) {
for (Map.Entry<PhysicalResourceKey, String> entry : sources.entrySet()) {
Path output = registryOutput(root.toPath(), registryPath(entry.getKey()), entry.getKey().resourceKey());
if (Files.exists(output)) {
String previous = Files.readString(output, StandardCharsets.UTF_8);
if (!JsonParser.parseString(previous).equals(JsonParser.parseString(entry.getValue()))) {
throw new IOException("Conflicting retained registry output at " + output);
}
continue;
}
Files.createDirectories(output.getParent());
Files.writeString(output, entry.getValue(), StandardCharsets.UTF_8);
}
for (Map.Entry<String, Set<String>> entry : tags.entrySet()) {
writeRetainedBiomeTag(registryOutput(root.toPath(), "tags/worldgen/biome", entry.getKey()), entry.getValue());
}
}
}
private static void writeRetainedBiomeTag(Path output, Set<String> biomes) throws IOException {
Set<String> merged = new TreeSet<>(biomes);
if (Files.exists(output)) {
JsonObject existing = JsonParser.parseString(Files.readString(output, StandardCharsets.UTF_8)).getAsJsonObject();
for (JsonElement value : existing.getAsJsonArray("values")) {
merged.add(value.getAsString());
}
}
JsonArray values = new JsonArray();
merged.forEach(values::add);
JsonObject tag = new JsonObject();
tag.addProperty("replace", false);
tag.add("values", values);
Files.createDirectories(output.getParent());
Files.writeString(output, tag.toString(), StandardCharsets.UTF_8);
}
private static Path registryOutput(Path root, String registryPath, String resourceKey) throws IOException {
int separator = resourceKey.indexOf(':');
Path directory = root.toAbsolutePath().normalize().resolve("data")
.resolve(resourceKey.substring(0, separator)).resolve(registryPath);
Path output = directory.resolve(resourceKey.substring(separator + 1) + ".json").normalize();
if (!output.startsWith(directory)) {
throw new IOException("Unsafe retained registry resource key " + resourceKey);
}
return output;
}
private static String registryPath(PhysicalResourceKey key) {
return key.registryKey().substring(key.registryKey().indexOf(':') + 1);
}
private static List<GenerationRegistryContract> retainedRegistryContracts(List<File> packRoots) throws IOException {
Map<String, GenerationRegistryContract> contracts = new TreeMap<>();
Set<Path> loadedWorlds = new LinkedHashSet<>();
for (File pack : packRoots) {
Path dimensionRoot = historyDimensionRoot(pack.toPath());
if (dimensionRoot == null || !loadedWorlds.add(dimensionRoot)) {
continue;
}
for (GenerationEpoch epoch : GenerationHistory.open(dimensionRoot).manifest().epochs()) {
contracts.putIfAbsent(epoch.registryContract().fingerprint(), epoch.registryContract());
}
}
return List.copyOf(contracts.values());
}
private static Path historyDimensionRoot(Path packRoot) {
Path pack = packRoot.toAbsolutePath().normalize();
Path epoch = pack.getParent();
Path epochs = epoch == null ? null : epoch.getParent();
Path generation = epochs == null ? null : epochs.getParent();
Path iris = generation == null ? null : generation.getParent();
if (iris == null || iris.getParent() == null || !"pack".equals(pack.getFileName().toString())
|| !"epochs".equals(epochs.getFileName().toString())
|| !"generation".equals(generation.getFileName().toString())
|| !"iris".equals(iris.getFileName().toString())) {
return null;
}
return iris.getParent();
}
private static void installLevelStemBindings(
List<IrisGeneratorBinding> bindings,
Map<String, List<DimensionCandidate>> dimensions,
@@ -648,11 +764,9 @@ public final class IrisDatapackCompiler {
&& (Files.exists(generationRoot, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(generationRoot))) {
GenerationHistory history = GenerationHistory.open(worldRoot);
for (GenerationEpoch epoch : history.manifest().epochs()) {
Path retainedPack = history.paths().packRoot(epoch.epochId());
if (hasDimensions(retainedPack)) {
candidates.add(retainedPack);
}
candidates.add(history.activePackRoot());
if (history.pendingActivation().isPresent()) {
candidates.add(history.packRoot(history.pendingActivation().orElseThrow().activationId()));
}
return true;
}
@@ -10,25 +10,50 @@ public final class IrisStartupValidation {
}
public static synchronized void begin() {
snapshot = new Snapshot(true, ValidationState.PENDING, ValidationState.PENDING, List.of(), List.of());
snapshot = new Snapshot(true, ValidationState.PENDING, ValidationState.PENDING, List.of(), List.of(), ValidationState.DISABLED, "");
}
public static synchronized void disable() {
snapshot = Snapshot.disabled();
}
public static synchronized void beginRuntimeValidation() {
if (!snapshot.enforced()) {
return;
}
snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(),
snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.PENDING, "");
}
public static synchronized void markRuntimeReady() {
if (!snapshot.enforced()) {
return;
}
snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(),
snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.READY, "");
}
public static synchronized void markRuntimeInvalid(String failure) {
if (!snapshot.enforced()) {
return;
}
snapshot = new Snapshot(true, snapshot.datapacks(), snapshot.packs(),
snapshot.datapackFailures(), snapshot.packFailures(), ValidationState.INVALID,
normalizeFailure(failure, "Iris runtime injection failed. Resolve the startup errors and restart the server."));
}
public static synchronized void beginDatapackValidation() {
if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) {
return;
}
snapshot = new Snapshot(true, ValidationState.PENDING, snapshot.packs(), List.of(), snapshot.packFailures());
snapshot = new Snapshot(true, ValidationState.PENDING, snapshot.packs(), List.of(), snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void markDatapacksReady() {
if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) {
return;
}
snapshot = new Snapshot(true, ValidationState.READY, snapshot.packs(), List.of(), snapshot.packFailures());
snapshot = new Snapshot(true, ValidationState.READY, snapshot.packs(), List.of(), snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void markDatapacksInvalid(String failure) {
@@ -40,7 +65,7 @@ public final class IrisStartupValidation {
ValidationState.INVALID,
snapshot.packs(),
List.of(normalizeFailure(failure, "External datapack validation failed.")),
snapshot.packFailures());
snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void requireRestart(String reason) {
@@ -52,14 +77,14 @@ public final class IrisStartupValidation {
ValidationState.RESTART_REQUIRED,
snapshot.packs(),
List.of(normalizeFailure(reason, "A restart is required to load validated datapacks.")),
snapshot.packFailures());
snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void markPacksReady() {
if (!snapshot.enforced()) {
return;
}
snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.READY, snapshot.datapackFailures(), List.of());
snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.READY, snapshot.datapackFailures(), List.of(), snapshot.runtime(), snapshot.runtimeFailure());
}
public static synchronized void markPacksInvalid(List<String> failures) {
@@ -72,7 +97,7 @@ public final class IrisStartupValidation {
.map(failure -> normalizeFailure(failure, "Iris dimension-pack validation failed."))
.toList();
snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.INVALID,
snapshot.datapackFailures(), normalized);
snapshot.datapackFailures(), normalized, snapshot.runtime(), snapshot.runtimeFailure());
}
public static boolean isReady() {
@@ -92,6 +117,7 @@ public final class IrisStartupValidation {
Snapshot current = snapshot;
if (force
&& current.enforced()
&& isRuntimeReady(current)
&& current.datapacks() == ValidationState.RESTART_REQUIRED
&& current.packs() == ValidationState.READY) {
return Optional.empty();
@@ -103,6 +129,11 @@ public final class IrisStartupValidation {
if (!current.enforced() || isReady(current)) {
return Optional.empty();
}
if (!isRuntimeReady(current)) {
return Optional.of(current.runtime() == ValidationState.INVALID
? current.runtimeFailure()
: "Iris is still validating runtime injection.");
}
if (current.datapacks() == ValidationState.RESTART_REQUIRED) {
return Optional.of(firstFailure(current.datapackFailures(),
"Iris installed validated datapacks and requires a restart before the server is safe."));
@@ -131,6 +162,7 @@ public final class IrisStartupValidation {
public static void requireWorldReplacementStagingReady() {
Snapshot current = snapshot;
if (current.enforced()
&& isRuntimeReady(current)
&& current.datapacks() == ValidationState.RESTART_REQUIRED
&& current.packs() == ValidationState.READY) {
return;
@@ -150,9 +182,14 @@ public final class IrisStartupValidation {
return failures == null || failures.isEmpty() ? fallback : failures.getFirst();
}
private static boolean isRuntimeReady(Snapshot current) {
return current.runtime() == ValidationState.DISABLED || current.runtime() == ValidationState.READY;
}
private static boolean isReady(Snapshot current) {
return !current.enforced()
|| current.datapacks() == ValidationState.READY
|| isRuntimeReady(current)
&& current.datapacks() == ValidationState.READY
&& current.packs() == ValidationState.READY;
}
@@ -169,7 +206,9 @@ public final class IrisStartupValidation {
ValidationState datapacks,
ValidationState packs,
List<String> datapackFailures,
List<String> packFailures
List<String> packFailures,
ValidationState runtime,
String runtimeFailure
) {
Snapshot {
datapackFailures = List.copyOf(datapackFailures);
@@ -177,7 +216,7 @@ public final class IrisStartupValidation {
}
private static Snapshot disabled() {
return new Snapshot(false, ValidationState.DISABLED, ValidationState.DISABLED, List.of(), List.of());
return new Snapshot(false, ValidationState.DISABLED, ValidationState.DISABLED, List.of(), List.of(), ValidationState.DISABLED, "");
}
}
}
@@ -143,7 +143,7 @@ public final class ImageMapStudioGUI {
private ImageMapStudioGUI(Engine engine) {
this.engine = Objects.requireNonNull(engine, "Image Map Studio engine");
this.packFolder = engine.getData().getDataFolder();
this.packFolder = engine.getPackSource().toFile();
this.dimensionKey = engine.getDimension().getLoadKey();
this.boundary = engine.getDimension().getWorldBoundary() == null
? null
@@ -163,7 +163,6 @@ public final class ImageMapStudioGUI {
}
static void reloadActiveEngine(Engine engine) {
engine.getData().getImageMapLoader().clearCache();
engine.hotloadSilently();
}
@@ -431,9 +430,14 @@ public final class ImageMapStudioGUI {
}
private void refreshPresets() {
String[] keys = engine.getData().getImageMapLoader().getPossibleKeys();
IrisData data = IrisData.openRuntime(packFolder);
try {
String[] keys = data.getImageMapLoader().getPossibleKeys();
Arrays.sort(keys);
presetPicker.setModel(new DefaultComboBoxModel<>(keys));
} finally {
data.close();
}
}
private void loadPreset(String key) {
@@ -32,9 +32,8 @@ final class BukkitPublicBackend implements WorldLifecycleBackend {
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
}
INMS.get().ensureServerLevelInjection();
try {
INMS.get().ensureServerLevelInjection();
World world = creator.createWorld();
return CompletableFuture.completedFuture(world);
} catch (Throwable e) {
@@ -189,6 +189,26 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return Optional.ofNullable(dataLoaders.get(dataFolder));
}
public static void invalidateLoadedAuthoringResources(File source) {
Path requested = dataFolderIdentity(Objects.requireNonNull(source, "authoring pack source"));
for (Map.Entry<File, IrisData> entry : dataLoaders.entrySet()) {
if (dataFolderIdentity(entry.getKey()).equals(requested)) {
entry.getValue().invalidateAuthoringResources();
}
}
}
private synchronized void invalidateAuthoringResources() {
if (generationRegistryContract != null) {
throw new IllegalStateException("Cannot invalidate an immutable generation pack as an authoring source.");
}
dump();
clearLists();
generationPackFingerprint = null;
contentGate = null;
dimensionCustomBiomeResourceKeys.clear();
}
public String customBiomeResourceKey(IrisDimension dimension, IrisBiomeCustom customBiome) {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
@@ -18,6 +18,8 @@
package art.arcane.iris.core.nms;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import java.util.concurrent.CompletableFuture;
import art.arcane.iris.core.datapack.DatapackStructureScopeIndex;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
@@ -57,9 +59,19 @@ import org.bukkit.inventory.ItemStack;
import java.awt.Color;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public interface INMSBinding {
default CompletableFuture<Void> flushSavedTerrainCapture(World world) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"Native saved terrain checkpoints are unavailable."));
}
default CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(
World world, int chunkX, int chunkZ, int minimumY, int height) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"Native saved terrain capture is unavailable."));
}
default PlatformGenerationRegistry generationRegistry() {
throw new UnsupportedOperationException(
"The active NMS binding does not expose generation registry definitions."
@@ -110,6 +110,11 @@ final class PackStructurePlacementValidator {
continue;
}
if (hasNativeStructures) {
Object anchor = placement.opt("anchor");
if (anchor != null && anchor != JSONObject.NULL && !"LEGACY".equals(anchor)) {
blockingErrors.add(placementPath
+ ".anchor must be omitted, null, or LEGACY for nativeStructures.");
}
validateNativeStructures(
placementPath, nativeStructures,
registries.structures(), registries.jigsaws(),
@@ -211,7 +211,6 @@ public class IrisPregenerator {
historyLastCached.set(cached.get());
historyLastMillis.set(startedAt);
ticker.start();
checkRegions();
int[] regionBounds = task.regionBounds();
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
generator.onPregenStart(task.getCenter().getX(), task.getCenter().getZ());
@@ -280,10 +279,6 @@ public class IrisPregenerator {
+ "] chunks/s";
}
private void checkRegions() {
task.iterateRegions(this::checkRegion);
}
private void init() {
generator.init();
}
@@ -413,9 +408,6 @@ public class IrisPregenerator {
}
generatedRegions.add(pos);
// No checkRegions() here: re-spiraling every region after every completed region
// was O(regions^2) dead work on the submitter thread (result discarded, and the
// only side effect was CachedPregenMethod faulting plates against eviction).
}
}
@@ -437,14 +429,6 @@ public class IrisPregenerator {
MantleHeapPressure.requestPanicReclaim();
}
private void checkRegion(int x, int z) {
if (generatedRegions.contains(new Position2(x, z))) {
return;
}
generator.supportsRegions(x, z, listener);
}
public long getFailedChunks() {
return failed.get();
}
@@ -21,22 +21,15 @@ package art.arcane.iris.core.pregenerator;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.CachedDoubleStream2D;
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.Objects;
public final class PregenPerformanceProfile {
private static final long HOTLOAD_ACQUISITION_TIMEOUT_SECONDS = 30L;
private static final AtomicBoolean JVM_HINT_LOGGED = new AtomicBoolean(false);
private PregenPerformanceProfile() {
}
@@ -44,7 +37,6 @@ public final class PregenPerformanceProfile {
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
int previousNoiseCacheSize = performance.getNoiseCacheSize();
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
boolean changed = false;
if (targetNoiseCacheSize != previousNoiseCacheSize) {
@@ -52,73 +44,41 @@ public final class PregenPerformanceProfile {
changed = true;
}
if (!fastCacheEnabledBefore) {
System.setProperty("iris.cache.fast", "true");
changed = true;
}
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(Runtime.getRuntime().availableProcessors()))) {
changed = true;
}
if (JVM_HINT_LOGGED.compareAndSet(false, true) && !fastCacheEnabledBefore) {
IrisLogging.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true");
}
return changed;
}
public static void apply(Engine engine) {
apply();
if (!prepareNoiseCacheInPlace(engine)) {
engine.hotloadComplex();
logApplied();
if (engine != null) {
prepareNoiseCacheInPlace(engine);
IrisLogging.info("Pregen profile applied: noiseCacheSize="
+ IrisSettings.get().getPerformance().getNoiseCacheSize());
}
}
public static void applyToGenerator(PlatformChunkGenerator generator) {
apply();
Engine engine = generator == null ? null : generator.getEngine();
if (!prepareNoiseCacheInPlace(engine)) {
generator.hotloadComplexAsync(HOTLOAD_ACQUISITION_TIMEOUT_SECONDS, TimeUnit.SECONDS).join();
logApplied();
}
apply(generator == null ? null : generator.getEngine());
}
static int pregenBurstParallelism(int availableProcessors) {
return Math.max(4, availableProcessors * 2);
}
private static boolean prepareNoiseCacheInPlace(Engine engine) {
if (engine == null) {
return true;
}
IrisComplex complex = engine.getComplex();
if (complex == null) {
return false;
}
ProceduralStream<IrisBiome> caveBiomeStream = complex.getCaveBiomeStream();
if (!(caveBiomeStream instanceof CachedStream2D<?> cachedStream) || !cachedStream.usesFastCache()) {
return false;
}
private static void prepareNoiseCacheInPlace(Engine engine) {
IrisComplex complex = Objects.requireNonNull(engine.getComplex(), "Pregeneration requires an active biome complex");
int configuredChunks = IrisSettings.get().getPerformance().getNoiseCacheSize();
if (!resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks)) {
return false;
}
return resizeNoiseCache(complex.getHeightStream(), configuredChunks);
resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks);
resizeNoiseCache(complex.getRawHeightStream(), configuredChunks);
}
private static boolean resizeNoiseCache(ProceduralStream<Double> stream, int configuredChunks) {
if (stream instanceof CachedDoubleStream2D cachedStream) {
private static void resizeNoiseCache(ProceduralStream<Double> stream, int configuredChunks) {
if (!(stream instanceof CachedDoubleStream2D cachedStream)) {
throw new IllegalStateException("Pregeneration requires a mutable double terrain cache");
}
cachedStream.setMaximumChunks(configuredChunks);
return true;
}
if (!(stream instanceof MeteredCache cache)) {
return false;
}
return cache.getMaxSize() == (long) configuredChunks * 256L;
}
private static void logApplied() {
IrisLogging.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast"));
}
}
@@ -0,0 +1,72 @@
package art.arcane.iris.core.pregenerator.cache;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.BitSet;
import java.util.LinkedHashMap;
import java.util.Objects;
public final class PregenSavedChunkStatus {
private static final int MAXIMUM_REGIONS = 256;
private final StatusReader reader;
private final LinkedHashMap<Long, RegionStatus> regions = new LinkedHashMap<>(16, 0.75f, true);
public PregenSavedChunkStatus(StatusReader reader) {
this.reader = Objects.requireNonNull(reader);
}
public static PregenSavedChunkStatus fromWorld(Path dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot).toAbsolutePath().normalize();
return new PregenSavedChunkStatus((x, z) -> readFull(root, x, z));
}
public synchronized boolean isFull(int chunkX, int chunkZ) {
long key = ((long) (chunkX >> 5) << 32) | ((chunkZ >> 5) & 0xffffffffL);
RegionStatus status = regions.get(key);
if (status == null) {
status = new RegionStatus(new BitSet(1024), new BitSet(1024));
regions.put(key, status);
if (regions.size() > MAXIMUM_REGIONS) {
regions.remove(regions.keySet().iterator().next());
}
}
int index = ((chunkZ & 31) << 5) | (chunkX & 31);
if (!status.checked().get(index)) {
try {
status.full().set(index, reader.isFull(chunkX, chunkZ));
status.checked().set(index);
} catch (IOException failure) {
throw new UncheckedIOException("Cannot verify saved pregeneration chunk " + chunkX + "," + chunkZ, failure);
}
}
return status.full().get(index);
}
private static boolean readFull(Path root, int chunkX, int chunkZ) throws IOException {
Path region = root.resolve("region/r." + (chunkX >> 5) + "." + (chunkZ >> 5) + ".mca");
if (Files.notExists(region)) {
return false;
}
try (RandomAccessFile input = new RandomAccessFile(region.toFile(), "r")) {
input.seek((long) (((chunkZ & 31) << 5) | (chunkX & 31)) * Integer.BYTES);
if (input.readInt() == 0) {
return false;
}
}
return SavedTerrainChunk.isComplete(SavedTerrainChunk.readStatus(root, chunkX, chunkZ));
}
@FunctionalInterface
public interface StatusReader {
boolean isFull(int chunkX, int chunkZ) throws IOException;
}
private record RegionStatus(BitSet checked, BitSet full) {
}
}
@@ -4,19 +4,22 @@ import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
public class CachedPregenMethod implements PregeneratorMethod {
private final PregeneratorMethod method;
private final PregenCache cache;
private final PregenTask task;
private final PregenSavedChunkStatus savedStatus;
private volatile PregenListener wrappedSource;
private volatile PregenListener wrappedListener;
public CachedPregenMethod(PregeneratorMethod method, PregenCache cache, PregenTask task) {
this.method = method;
this.cache = cache.sync();
this.task = task;
public CachedPregenMethod(Configuration configuration) {
this.method = configuration.method();
this.cache = configuration.cache().sync();
this.task = configuration.task();
this.savedStatus = configuration.savedStatus();
}
@Override
@@ -26,7 +29,6 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public void close() {
cache.write();
method.close();
cache.write();
}
@@ -39,12 +41,12 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return cache.isRegionCached(x, z) || method.supportsRegions(x, z, listener);
return isSavedRegionCached(x, z) || method.supportsRegions(x, z, listener);
}
@Override
public String getMethod(int x, int z) {
if (cache.isRegionCached(x, z)) {
if (isSavedRegionCached(x, z)) {
return "Cached";
}
return method.getMethod(x, z);
@@ -52,7 +54,7 @@ public class CachedPregenMethod implements PregeneratorMethod {
@Override
public void generateRegion(int x, int z, PregenListener listener) {
if (cache.isRegionCached(x, z)) {
if (isSavedRegionCached(x, z)) {
listener.onRegionGenerated(x, z);
task.iterateChunks(x, z, (cX, cZ) -> {
listener.onChunkGenerated(cX, cZ, true);
@@ -60,13 +62,16 @@ public class CachedPregenMethod implements PregeneratorMethod {
});
return;
}
method.generateRegion(x, z, listener);
cache.cacheRegion(x, z);
if (!method.supportsRegions(x, z, listener)) {
task.iterateChunks(x, z, (chunkX, chunkZ) -> generateChunk(chunkX, chunkZ, listener));
return;
}
method.generateRegion(x, z, cachingListener(listener));
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
if (cache.isChunkCached(x, z)) {
if (cache.isChunkCached(x, z) && savedStatus.isFull(x, z)) {
listener.onChunkGenerated(x, z, true);
listener.onChunkCleaned(x, z);
return;
@@ -74,6 +79,20 @@ public class CachedPregenMethod implements PregeneratorMethod {
method.generateChunk(x, z, cachingListener(listener));
}
private boolean isSavedRegionCached(int x, int z) {
if (!cache.isRegionCached(x, z)) {
return false;
}
for (int localX = 0; localX < 32; localX++) {
for (int localZ = 0; localZ < 32; localZ++) {
if (!savedStatus.isFull((x << 5) + localX, (z << 5) + localZ)) {
return false;
}
}
}
return true;
}
private PregenListener cachingListener(PregenListener listener) {
PregenListener source = wrappedSource;
PregenListener wrapped = wrappedListener;
@@ -185,6 +204,10 @@ public class CachedPregenMethod implements PregeneratorMethod {
method.onRegionSubmitted(regionX, regionZ);
}
public record Configuration(PregeneratorMethod method, PregenCache cache, PregenTask task,
PregenSavedChunkStatus savedStatus) {
}
@Override
public Mantle getMantle() {
return method.getMantle();
@@ -1,5 +1,6 @@
package art.arcane.iris.core.safeguard;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.core.safeguard.task.Diagnostic;
import art.arcane.iris.core.safeguard.task.Task;
import art.arcane.iris.core.safeguard.task.Tasks;
@@ -24,6 +25,7 @@ public final class IrisSafeguard {
}
public static void execute() {
IrisStartupValidation.beginRuntimeValidation();
List<Task> tasks = Tasks.getTasks();
LinkedHashMap<Task, ValueWithDiagnostics<Mode>> resultValues = new LinkedHashMap<>(tasks.size());
LinkedHashMap<String, String> contextValues = new LinkedHashMap<>(tasks.size());
@@ -36,9 +38,13 @@ public final class IrisSafeguard {
try {
result = task.run();
} catch (Throwable e) {
boolean injectionFailure = "injection".equals(task.getId());
if (injectionFailure) {
IrisStartupValidation.markRuntimeInvalid("Iris runtime injection failed. Resolve the startup errors and restart the server.");
}
IrisLogging.reportError(e);
result = new ValueWithDiagnostics<>(
Mode.WARNING,
injectionFailure ? Mode.UNSTABLE : Mode.WARNING,
new Diagnostic(Diagnostic.Logger.ERROR, "Error while running task " + task.getId(), e)
);
}
@@ -1,6 +1,7 @@
package art.arcane.iris.core.safeguard.task;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.IrisWorldStorage;
@@ -119,18 +120,22 @@ public final class Tasks {
private static final Task INJECTION = Task.of("injection", () -> {
if (!Agent.install()) {
IrisStartupValidation.markRuntimeInvalid("Iris Java agent is unavailable. Add -javaagent:"
+ Agent.AGENT_JAR.getPath() + " to the JVM arguments before -jar and restart the server.");
return withDiagnostics(Mode.UNSTABLE,
Diagnostic.Logger.ERROR.create("Java Agent"),
Diagnostic.Logger.ERROR.create("- Please enable dynamic agent loading by adding -XX:+EnableDynamicAgentLoading to your jvm arguments."),
Diagnostic.Logger.ERROR.create("- or add the jvm argument -javaagent:" + Agent.AGENT_JAR.getPath()));
Diagnostic.Logger.ERROR.create("- Add -javaagent:" + Agent.AGENT_JAR.getPath() + " before -jar in your startup command and restart the server."),
Diagnostic.Logger.ERROR.create("- Dynamic attachment requires -XX:+EnableDynamicAgentLoading and a host that permits JVM attachment."));
}
if (!INMS.get().injectBukkit()) {
IrisStartupValidation.markRuntimeInvalid("Iris runtime injection failed. Resolve the startup errors and restart the server.");
return withDiagnostics(Mode.UNSTABLE,
Diagnostic.Logger.ERROR.create("Code Injection"),
Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support"));
}
IrisStartupValidation.markRuntimeReady();
return withDiagnostics(Mode.STABLE);
});
@@ -91,7 +91,7 @@ public class ObjectStudioSaveService implements IrisService {
Map<String, File> objectsDirs = new ConcurrentHashMap<>();
for (Map.Entry<String, IrisData> e : sources.entrySet()) {
File dir = resolveObjectsDir(e.getValue());
File dir = resolveObjectsDir(e.getValue(), engine);
if (dir != null) {
objectsDirs.put(e.getKey(), dir);
}
@@ -200,9 +200,13 @@ public class ObjectStudioSaveService implements IrisService {
return false;
}
private static File resolveObjectsDir(IrisData data) {
static File resolveObjectsDir(IrisData data, Engine engine) {
File root = data.getDataFolder();
if (root == null) return null;
if (root.toPath().toAbsolutePath().normalize().equals(
engine.getData().getDataFolder().toPath().toAbsolutePath().normalize())) {
root = engine.getPackSource().toFile();
}
File objects = new File(root, "objects");
if (!objects.exists()) {
objects.mkdirs();
@@ -21,8 +21,6 @@ package art.arcane.iris.core.structure;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisStructure;
import com.google.gson.GsonBuilder;
import java.io.File;
@@ -38,8 +36,6 @@ import java.util.concurrent.ConcurrentHashMap;
public final class StructureIndexService {
private static final String INDEX_PATH = ".iris/structure-index.json";
private static final String LEGACY_INDEX_PATH = "structures/structure-index.json";
private static final String LEGACY_INDEX_KEY = "structure-index";
private static final Set<String> GENERATED = ConcurrentHashMap.newKeySet();
private StructureIndexService() {
@@ -66,7 +62,6 @@ public final class StructureIndexService {
}
public static File write(IrisData data) {
removeLegacyIndex(data);
List<String> structures = IrisPlatforms.get().structureHooks().structureKeys();
List<String> sets = IrisPlatforms.get().structureHooks().structureSetKeys();
@@ -93,7 +88,6 @@ public final class StructureIndexService {
List<String> iris = new ArrayList<>();
if (data.getStructureLoader() != null) {
Collections.addAll(iris, data.getStructureLoader().getPossibleKeys());
iris.removeIf(LEGACY_INDEX_KEY::equals);
}
Collections.sort(vanilla);
@@ -138,22 +132,4 @@ public final class StructureIndexService {
return new File(dataFolder, INDEX_PATH);
}
static File legacyIndexFile(File dataFolder) {
return new File(dataFolder, LEGACY_INDEX_PATH);
}
static void removeLegacyIndex(IrisData data) {
File legacyFile = legacyIndexFile(data.getDataFolder());
try {
Files.deleteIfExists(legacyFile.toPath());
ResourceLoader<IrisStructure> structureLoader = data.getStructureLoader();
if (structureLoader != null) {
structureLoader.getLoadCache().invalidate(LEGACY_INDEX_KEY);
structureLoader.clearList();
}
} catch (Throwable e) {
IrisLogging.reportError("Failed to remove legacy structure index '"
+ legacyFile.getAbsolutePath() + "'", e);
}
}
}
@@ -118,9 +118,9 @@ public class IrisCreator {
*/
private String name = "irisworld";
/**
* Studio mode makes the engine hotloadable and uses the dimension in
* your Iris/packs folder instead of copying the dimension files into
* the world itself. Studio worlds are deleted when they are unloaded.
* Studio mode snapshots edits from your Iris/packs folder into generation history.
* Existing chunks retain their generation while new chunks use accepted edits.
* Studio worlds are deleted when they are unloaded.
*/
private boolean studio = false;
/**
@@ -260,18 +260,15 @@ public class IrisCreator {
+ "Restart the server, then run the command again.");
}
IrisDimension installedDimension = resolvedDimension;
if (!studio() || benchmark) {
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
StudioSVC studioService = IrisServices.get(StudioSVC.class);
installedDimension = benchmark
IrisDimension installedDimension = benchmark && !studio
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
if (installedDimension == null) {
throw new IrisException("Failed to install dimension pack for " + dimension());
}
dimension = installedDimension.getLoadKey();
}
if (studio()) {
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
IrisLogging.debug("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
@@ -285,6 +282,7 @@ public class IrisCreator {
long generatorPrepareStart = System.nanoTime();
WorldCreator wc = new IrisWorldCreator()
.dimension(installedDimension)
.studioPackSource(resolvedDimension.getLoader().getDataFolder())
.name(name)
.seed(seed)
.studio(studio)
@@ -33,6 +33,7 @@ import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.core.pregenerator.cache.PregenCache;
import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackDownloader;
import art.arcane.iris.core.project.IrisProject;
@@ -316,7 +317,8 @@ public class IrisToolbelt {
useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA;
}
PregeneratorMethod activeMethod = useCachedWrapper
? new CachedPregenMethod(method, resolvePregenCache(engine.getWorld()), task)
? new CachedPregenMethod(new CachedPregenMethod.Configuration(method, resolvePregenCache(engine.getWorld()), task,
PregenSavedChunkStatus.fromWorld(engine.getWorld().worldFolder().toPath())))
: method;
return new PregeneratorJob(new PregeneratorJob.Configuration(
task,
@@ -33,6 +33,7 @@ import org.bukkit.generator.ChunkGenerator;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
public class IrisWorldCreator {
private String name;
@@ -41,6 +42,7 @@ public class IrisWorldCreator {
private IrisDimension dimension;
private long seed = 1337;
private boolean persistent;
private File studioPackSource;
public IrisWorldCreator() {
@@ -83,6 +85,11 @@ public class IrisWorldCreator {
return this;
}
public IrisWorldCreator studioPackSource(File source) {
this.studioPackSource = Objects.requireNonNull(source, "Studio authoring source");
return this;
}
public WorldCreator create() {
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
NamespacedKey worldKey = IrisWorldStorage.keyFromName(name);
@@ -102,13 +109,13 @@ public class IrisWorldCreator {
.seed(seed)
.worldFolder(worldFolder)
.build();
GenerationHistory generationHistory = persistent
GenerationHistory generationHistory = persistent || studio
? requireGenerationHistory(w.worldFolder(), seed)
: null;
File packRoot = generationHistory != null
File packRoot = studio
? Objects.requireNonNull(studioPackSource, "Studio authoring source")
: generationHistory != null
? requireActivePack(generationHistory)
: studio
? dim.getLoader().getDataFolder()
: new File(w.worldFolder(), "iris/pack");
ChunkGenerator g = new BukkitChunkGenerator(
w,
@@ -0,0 +1,63 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import java.io.File;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
final class EngineDiagnostics {
private final IrisEngine engine;
EngineDiagnostics(IrisEngine engine) {
this.engine = engine;
}
/**
* One line per engine naming what this pack cannot generate on the running Minecraft version. The published
* validation result is complete (validation force-loads the whole pack), so it is preferred; an unvalidated pack
* falls back to what this engine has gated while building its runtime (dimension, regions, biomes). Never throws:
* a report failure must not take an otherwise working world down with it.
*/
void logPackCompatSummary() {
try {
File folder = engine.getData().getDataFolder();
// Runtime data comes from an immutable snapshot; the dimension key identifies the pack.
String pack = engine.getDimension().getLoadKey();
String world = engine.getTarget().getWorld().name();
String version = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
PackValidationResult published = PackValidationRegistry.get(folder.toPath());
if (published == null) {
published = PackValidationRegistry.get(folder.getName());
}
PackCompatReport report = published != null && !published.getCompatFindings().isEmpty()
? PackCompatReport.of(published.getCompatFindings())
: engine.getData().getCompatReport();
if (!report.isEmpty()) {
IrisLogging.info("World '" + world + "' pack '" + pack + "' " + report.summaryLine(version));
}
if (engine.getDimension().isCompatExcluded()) {
IrisLogging.error("World '" + world + "' pack '" + pack + "' cannot generate on Minecraft "
+ (version == null || version.isBlank() ? "unknown" : version));
}
} catch (Throwable e) {
IrisLogging.debug("Pack compat summary failed: " + e.getMessage());
}
}
void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) {
if (!engine.isStudio()) {
return;
}
IrisLogging.info("[Studio engine timing] world=%s kind=%s phase=%s duration=%dms skipped=%s",
engine.getTarget().getWorld().name(),
engine.getInitializationMode().name().toLowerCase(Locale.ROOT),
phase,
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos),
Boolean.toString(skipped));
}
}
@@ -19,6 +19,7 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
@@ -27,6 +28,10 @@ import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.GenerationManifest;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.project.context.IrisContext;
@@ -34,12 +39,13 @@ import art.arcane.iris.util.project.context.IrisContext;
import static art.arcane.iris.engine.EngineShutdownSequence.runCleanup;
/**
* Performs the two supported live reload transitions for an {@link IrisEngine}: a full pack hotload
* and a biome-complex-only rebuild. Both run entirely under the engine lifecycle lock, seal and
* drain generation before swapping anything, and roll the previous runtime back on failure.
* Reloads mutable engine runtimes and publishes immutable Studio generation activations.
* Studio transitions drain sessions outside the lifecycle lock before freezing chunk ownership.
* Failures before publication preserve the previous runtime; durable publication failures stop generation.
*/
final class EngineHotloader {
private final IrisEngine engine;
private final Object studioTransitionLock = new Object();
EngineHotloader(IrisEngine engine) {
this.engine = engine;
@@ -51,6 +57,10 @@ final class EngineHotloader {
}
void hotloadComplex() {
if (engine.isStudio() && engine.getGenerationHistoryRuntimeRouter().isPresent()) {
hotloadSilently();
return;
}
synchronized (engine.lifecycleLock) {
engine.requireRunning("rebuild the biome complex");
requireMutableGenerationRuntime("rebuild the biome complex");
@@ -114,6 +124,11 @@ final class EngineHotloader {
}
void hotloadSilently() {
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElse(null);
if (engine.isStudio() && router != null) {
hotloadStudio(router);
return;
}
synchronized (engine.lifecycleLock) {
engine.requireRunning("hotload");
requireMutableGenerationRuntime("hotload");
@@ -192,6 +207,95 @@ final class EngineHotloader {
}
}
private void hotloadStudio(GenerationHistoryRuntimeRouter router) {
synchronized (studioTransitionLock) {
EngineRuntime previous = null;
GenerationManifest previousManifest = router.history().manifest();
GenerationHistoryRuntimeRouter.StudioCutover cutover = null;
StudioGenerationUpdate update = null;
boolean backgroundAdmissionClosed = false;
try {
engine.requireRunning("hotload the Studio pack");
GenerationHistory history = router.history();
update = StudioGenerationUpdate.prepare(engine, history);
if (update == null) {
return;
}
engine.awaitNativeStructureBootstrap("Studio generation cutover");
engine.backgroundTasks.closeBackgroundTaskAdmission();
backgroundAdmissionClosed = true;
engine.backgroundTasks.drainBackgroundTasks("Studio generation cutover")
.requireComplete("Studio generation cutover");
cutover = router.beginStudioCutover(IrisEngine.SESSION_DRAIN_TIMEOUT_MILLIS);
synchronized (engine.lifecycleLock) {
engine.requireRunning("prepare the Studio pack cutover");
previous = engine.runtime;
engine.studioGenerationTransition = true;
engine.lifecycleState = LifecycleState.HOTLOADING;
}
engine.sealForTransition("Studio generation cutover", false);
IrisBoundarySignatureSampler.checkpoint(engine);
synchronized (engine.lifecycleLock) {
if (engine.lifecycleState != LifecycleState.HOTLOADING || engine.runtime != previous) {
throw new IllegalStateException("Studio engine changed while its generation cutover was draining.");
}
engine.prepareRuntimeHotload();
history.stageUpdate(update.source(), update.fingerprint(), update.dimensionContract(),
update.registryContract(), IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
cutover.promotePending();
IrisStructureLocator.invalidate(engine);
StructureReachability.invalidate(engine);
engine.runtimeBuilder.refreshStudioRuntimeServices();
engine.getPrefetchSaveStarted().set(false);
engine.getEngineData().getStatistics().hotloaded();
engine.getPlatformHooks().applyWorldBoundary(engine);
}
broadcastStudioHotload(false, "");
} catch (Throwable failure) {
if (previous != null) {
synchronized (engine.lifecycleLock) {
if (engine.lifecycleState == LifecycleState.HOTLOADING
|| engine.lifecycleState == LifecycleState.RUNNING) {
boolean unchanged = false;
try {
unchanged = router.history().manifest() == previousManifest;
} catch (Throwable historyFailure) {
failure.addSuppressed(historyFailure);
}
if (unchanged) {
engine.runtimeBuilder.restoreRuntimeAfterFailedTransition(previous);
} else {
engine.lifecycleState = LifecycleState.FAILED;
engine.getClosing().set(true);
engine.backgroundTasks.closeBackgroundTaskAdmission();
}
}
}
} else if (backgroundAdmissionClosed && engine.lifecycleState == LifecycleState.RUNNING
&& !engine.getClosing().get()) {
engine.backgroundTasks.openBackgroundTaskAdmission();
}
IrisLogging.reportError("Studio generation update failed for '" + engine.getWorld().name() + "'.", failure);
broadcastStudioHotload(true, failure.getClass().getSimpleName() + ": " + failure.getMessage());
throw EngineShutdownSequence.propagate(failure);
} finally {
synchronized (engine.lifecycleLock) {
engine.studioGenerationTransition = false;
}
if (cutover != null) {
cutover.close();
}
if (update != null) {
try {
update.close();
} catch (Throwable cleanupFailure) {
IrisLogging.reportError("Failed to remove the staged Studio pack after its update.", cleanupFailure);
}
}
}
}
}
private void requireMutableGenerationRuntime(String operation) {
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
throw new IllegalStateException("Cannot " + operation
@@ -210,7 +314,7 @@ final class EngineHotloader {
if (protocolServer == null) {
return;
}
String packKey = engine.getData().getDataFolder().getName();
String packKey = engine.getPackSource().getFileName().toString();
protocolServer.broadcastStudioHotload(packKey, 0, failed, message);
} catch (Throwable broadcastFailure) {
IrisLogging.error("Iris studio hotload broadcast failed: " + broadcastFailure.getClass().getSimpleName()
@@ -248,6 +248,39 @@ final class EngineRuntimeBuilder {
IrisLogging.debug("Engine Setup Complete " + next.generation().cacheId());
}
void refreshStudioRuntimeServices() {
EngineRuntime previous = requireRuntime("refresh Studio runtime services");
EngineEffects effects = null;
EngineWorldManager worldManager = null;
try {
effects = Objects.requireNonNull(IrisServices.get(EngineEffectsProvider.class).create(engine),
"Studio engine effects");
worldManager = Objects.requireNonNull(IrisServices.get(EngineWorldManagerProvider.class).create(engine),
"Studio world manager");
} catch (Throwable failure) {
if (effects != null) {
EngineEffects createdEffects = effects;
EngineShutdownSequence.runCleanup(failure, createdEffects::close);
}
throw propagate(failure);
}
EngineRuntime next = new EngineRuntime(previous.generation(), effects, worldManager);
Throwable retirementFailure = EngineShutdownSequence.runCleanup(null, previous.worldManager()::close);
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, previous.effects()::close);
if (retirementFailure != null) {
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, worldManager::close);
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, effects::close);
throw new IllegalStateException("Failed to retire Studio runtime services.", retirementFailure);
}
engine.runtime = next;
worldManager.start();
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
scheduleRuntimeTasks(next);
}
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
schedulePackHash(engineRuntime.generation());
try {
@@ -18,6 +18,8 @@
package art.arcane.iris.engine;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
@@ -25,6 +27,7 @@ import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationBlend;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
@@ -79,6 +82,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@@ -140,6 +144,8 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
private ProceduralStream<Double> naturalHeightStream;
private ProceduralStream<Double> unblendedNaturalHeightStream;
private final ResolvedTerrainProvider resolvedTerrain;
private ProceduralStream<Double> placementHeightStream;
private ProceduralStream<Double> heightStream;
private ProceduralStream<Integer> roundedHeighteightStream;
private ProceduralStream<Double> maxHeightStream;
@@ -188,6 +194,7 @@ public class IrisComplex implements DataProvider {
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan) {
this.transitionGenerationPlan = transitionGenerationPlan;
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize());
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
@@ -314,11 +321,7 @@ public class IrisComplex implements DataProvider {
(x, z) -> sampleUnblendedNaturalTerrainHeight(engine, x, z),
Interpolated.DOUBLE
).cache2DDouble("unblendedNaturalHeightStream", engine, cacheSize);
naturalHeightStream = transitionGenerationPlan == null ? unblendedNaturalHeightStream
: unblendedNaturalHeightStream.convertAware2D((newHeight, x, z) -> blendNaturalTerrainHeight(
newHeight,
transitionGenerationPlan.terrainSampleAt(blockCoordinate(x), blockCoordinate(z))))
.cache2DDouble("naturalHeightStream", engine, cacheSize);
naturalHeightStream = unblendedNaturalHeightStream;
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
@@ -355,10 +358,11 @@ public class IrisComplex implements DataProvider {
}
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
.cache2DDouble("heightStream", engine, cacheSize);
roundedHeighteightStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
placementHeightStream = ProceduralStream.ofDouble(this::samplePlacementHeight);
roundedHeighteightStream = placementHeightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.round();
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
slopeStream = placementHeightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
.slope(3);
naturalSlopeStream = naturalHeightStream.slope(3).cache2DDouble("naturalSlopeStream", engine, cacheSize);
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
b -> focusBiome))
@@ -480,14 +484,7 @@ public class IrisComplex implements DataProvider {
}
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
double newHeight = sampleUnblendedNaturalTerrainHeight(engine, x, z);
if (transitionGenerationPlan == null) {
return newHeight;
}
TransitionGenerationPlan.TerrainSample sample = transitionGenerationPlan.terrainSampleAt(
blockCoordinate(x),
blockCoordinate(z));
return blendNaturalTerrainHeight(newHeight, sample);
return sampleUnblendedNaturalTerrainHeight(engine, x, z);
}
private double sampleUnblendedNaturalTerrainHeight(Engine engine, double x, double z) {
@@ -495,20 +492,6 @@ public class IrisComplex implements DataProvider {
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
}
static double blendNaturalTerrainHeight(
double newHeight,
TransitionGenerationPlan.TerrainSample sample
) {
Objects.requireNonNull(sample, "terrain transition sample");
if (sample.newEpochWeight() == 1D || !sample.hasHistoricalSignature()) {
return newHeight;
}
double historicalHeight = sample.nearestSignature().fluidHeight().isPresent()
? sample.historicalOceanFloorHeight()
: sample.historicalSurfaceHeight();
return GenerationBlend.interpolate(historicalHeight, newHeight, sample.newEpochWeight());
}
static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
double deltaX = easternHeight - naturalHeight;
double deltaZ = southernHeight - naturalHeight;
@@ -808,6 +791,9 @@ public class IrisComplex implements DataProvider {
int naturalHeight,
double hydrologyWeight
) {
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
return layer;
}
return new HydrologyColumnLayer(
layer.feature(),
GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight),
@@ -847,6 +833,42 @@ public class IrisComplex implements DataProvider {
|| hydrologyRuntime.isPlanned(x, z);
}
public ProceduralStream<Double> getRawHeightStream() {
return heightStream;
}
public ProceduralStream<Double> getHeightStream() {
return placementHeightStream;
}
public Optional<TerrainBoundarySignature> resolvedTerrainColumn(int blockX, int blockZ) {
if (transitionGenerationPlan == null || isNaturalTerrainContext()
|| !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return Optional.empty();
}
return Optional.of(resolvedTerrain.column(blockX, blockZ));
}
public OptionalInt resolvedTerrainHeight(int blockX, int blockZ, boolean ignoreFluid) {
if (transitionGenerationPlan == null || isNaturalTerrainContext()
|| !transitionGenerationPlan.hasTransitionAtChunk(blockX >> 4, blockZ >> 4)) {
return OptionalInt.empty();
}
return OptionalInt.of(resolvedTerrain.height(blockX, blockZ, ignoreFluid));
}
boolean isNaturalTerrainContext() {
IrisContext context = IrisContext.get();
return context != null && context.getChunkContext() != null
&& context.getChunkContext().getComplex() == this
&& context.getChunkContext().isNaturalTerrain();
}
private double samplePlacementHeight(double x, double z) {
OptionalInt resolved = resolvedTerrainHeight(blockCoordinate(x), blockCoordinate(z), true);
return resolved.isPresent() ? resolved.getAsInt() : heightStream.getDouble(x, z);
}
public boolean isHistoricalChunk(int chunkX, int chunkZ) {
return transitionGenerationPlan != null
&& transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
@@ -1783,6 +1805,7 @@ public class IrisComplex implements DataProvider {
}
public void close() {
resolvedTerrain.clear();
if (hydrologyRuntime != null) {
hydrologyRuntime.close();
}
@@ -37,6 +37,7 @@ import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.history.GenerationBoundarySignatureSampler;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.ChunkGenerationSemantics;
import art.arcane.iris.engine.history.GenerationKernelRegistry;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.framework.SeedManager;
@@ -46,10 +47,7 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisEngineData;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.core.compat.PackCompatReport;
import art.arcane.iris.core.pack.PackValidationCache;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.spi.IrisLogging;
import com.google.common.util.concurrent.AtomicDouble;
import art.arcane.iris.spi.IrisPlatforms;
@@ -78,20 +76,17 @@ import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -118,6 +113,11 @@ public class IrisEngine implements Engine {
private final EngineMetrics metrics;
private final CompletableFuture<Void> generationCacheWarm;
private final boolean studio;
@Setter(AccessLevel.NONE)
private volatile Path studioGenerationSource;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
boolean studioGenerationTransition;
private final AtomicRollingSequence wallClock;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@@ -163,6 +163,9 @@ public class IrisEngine implements Engine {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final EngineMetricsReport metricsReport = new EngineMetricsReport(this);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final EngineDiagnostics diagnostics = new EngineDiagnostics(this);
private final AtomicBoolean cleaning;
private final ChronoLatch cleanLatch;
private final SeedManager seedManager;
@@ -267,7 +270,7 @@ public class IrisEngine implements Engine {
this.initialKernelVersion = Objects.requireNonNull(kernelVersion, "generation kernel version");
this.initialTransitionPlan = transitionPlan;
this.platformHooks = IrisServices.get(EnginePlatformHooks.class);
this.generationSessions = new GenerationSessionManager();
this.generationSessions = new GenerationSessionManager(requiredMode.studio());
this.closing = new AtomicBoolean(true);
this.nativeStructureVolumeQueriesEnabled = new AtomicBoolean(!requiredMode.studio());
this.lifecycleState = LifecycleState.INITIALIZING;
@@ -313,7 +316,7 @@ public class IrisEngine implements Engine {
String studioCacheIdentity = currentStudioCacheIdentity();
getData().loadPrefetch(this);
IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
diagnostics.logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
try {
StructureIndexService.writeOnce(getData());
} catch (Throwable e) {
@@ -326,8 +329,8 @@ public class IrisEngine implements Engine {
enableStableStudioCache(initialRuntime, studioCacheIdentity);
runtimeBuilder.publishRuntime(initialRuntime, null);
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
logPackCompatSummary();
logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
diagnostics.logPackCompatSummary();
diagnostics.logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
phaseStartedAt = System.nanoTime();
if (requiredMode.warmGenerationCaches()) {
if (requiredMode.studio()) {
@@ -337,7 +340,7 @@ public class IrisEngine implements Engine {
}
} else {
generationCacheWarm.complete(null);
logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
}
EngineTickRegistry.registerTicking(this);
} catch (Throwable e) {
@@ -347,38 +350,6 @@ public class IrisEngine implements Engine {
IrisLogging.debug("Engine Initialized " + getCacheID());
}
/**
* One line per engine naming what this pack cannot generate on the running Minecraft version. The published
* validation result is complete (validation force-loads the whole pack), so it is preferred; an unvalidated pack
* falls back to what this engine has gated while building its runtime (dimension, regions, biomes). Never throws:
* a report failure must not take an otherwise working world down with it.
*/
private void logPackCompatSummary() {
try {
File folder = getData().getDataFolder();
// A world engine reads its snapshot copy under <world>/iris/pack, so the dimension key is the pack's name.
String pack = getDimension().getLoadKey();
String world = target.getWorld().name();
String version = IrisPlatforms.isBound() ? IrisPlatforms.get().minecraftVersion() : null;
PackValidationResult published = PackValidationRegistry.get(folder.toPath());
if (published == null) {
published = PackValidationRegistry.get(folder.getName());
}
PackCompatReport report = published != null && !published.getCompatFindings().isEmpty()
? PackCompatReport.of(published.getCompatFindings())
: getData().getCompatReport();
if (!report.isEmpty()) {
IrisLogging.info("World '" + world + "' pack '" + pack + "' " + report.summaryLine(version));
}
if (getDimension().isCompatExcluded()) {
IrisLogging.error("World '" + world + "' pack '" + pack + "' cannot generate on Minecraft "
+ (version == null || version.isBlank() ? "unknown" : version));
}
} catch (Throwable e) {
IrisLogging.debug("Pack compat summary failed: " + e.getMessage());
}
}
public void awaitGenerationCacheWarm() {
if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) {
return;
@@ -394,18 +365,6 @@ public class IrisEngine implements Engine {
return !generationCacheWarm.isDone();
}
private void logStudioInitializationPhase(String phase, long startedAtNanos, boolean skipped) {
if (!studio) {
return;
}
IrisLogging.info("[Studio engine timing] world=%s kind=%s phase=%s duration=%dms skipped=%s",
target.getWorld().name(),
initializationMode.name().toLowerCase(Locale.ROOT),
phase,
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAtNanos),
Boolean.toString(skipped));
}
private String currentStudioCacheIdentity() {
if (initializationMode != InitializationMode.STUDIO || !IrisPlatforms.isBound()) {
return "";
@@ -469,7 +428,7 @@ public class IrisEngine implements Engine {
throw new IllegalStateException("Generation cache warming failed.", failure);
} finally {
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - startedAtMillis) + "ms");
logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false);
diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAtNanos, false);
}
}
@@ -575,6 +534,9 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public IrisBiome getCaveBiome(int x, int z) {
if (usesScopedNaturalTerrain()) {
return Engine.super.getCaveBiome(x, z);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave biome")) {
return Engine.super.getCaveBiome(x, z);
@@ -595,6 +557,9 @@ public class IrisEngine implements Engine {
int z,
IrisDimensionCarvingResolver.State state
) {
if (usesScopedNaturalTerrain()) {
return Engine.super.getCaveBiome(x, y, z, state);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical cave biome")) {
return Engine.super.getCaveBiome(x, y, z, state);
@@ -604,6 +569,9 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public IrisBiome getSurfaceBiome(int x, int z) {
if (usesScopedNaturalTerrain()) {
return Engine.super.getSurfaceBiome(x, z);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome")) {
return Engine.super.getSurfaceBiome(x, z);
@@ -637,6 +605,10 @@ public class IrisEngine implements Engine {
@ChunkCoordinates
@Override
public Set<String> getObjectsAt(int x, int z) {
Optional<ChunkGenerationSemantics> recorded = recordedChunkSemantics(x, z);
if (recorded.isPresent()) {
return recorded.get().objectKeys();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x << 4, z << 4, "resolve chunk objects")) {
return getMantle().getObjectComponent().guess(x, z);
@@ -646,6 +618,15 @@ public class IrisEngine implements Engine {
@ChunkCoordinates
@Override
public Set<Pair<String, BlockPos>> getPOIsAt(int chunkX, int chunkZ) {
Optional<ChunkGenerationSemantics> recorded = recordedChunkSemantics(chunkX, chunkZ);
if (recorded.isPresent()) {
Set<Pair<String, BlockPos>> points = new HashSet<>();
for (ChunkGenerationSemantics.PointOfInterest point : recorded.get().pointsOfInterest()) {
ChunkGenerationSemantics.BlockPosition position = point.position();
points.add(new Pair<>(point.key(), new BlockPos(position.x(), position.y(), position.z())));
}
return points;
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(
chunkX << 4, chunkZ << 4, "resolve chunk points of interest")) {
@@ -654,11 +635,19 @@ public class IrisEngine implements Engine {
chunkX,
chunkZ,
MatterStructurePOI.class,
(x, y, z, data) -> pois.add(new Pair<>(data.getType(), new BlockPos(x, y, z))));
(x, y, z, data) -> pois.add(new Pair<>(data.getType(), new BlockPos(
Math.addExact(Math.multiplyExact(chunkX, 16), x), y,
Math.addExact(Math.multiplyExact(chunkZ, 16), z)))));
return pois;
}
}
private Optional<ChunkGenerationSemantics> recordedChunkSemantics(int chunkX, int chunkZ) {
GenerationHistoryRuntimeRouter router = getGenerationHistoryRuntimeRouter().orElse(null);
return router == null ? Optional.empty()
: router.history().semantics(chunkX, chunkZ).filter(ChunkGenerationSemantics::sealed);
}
private void warmupChunk(int x, int z) {
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
@@ -675,6 +664,30 @@ public class IrisEngine implements Engine {
hotloader.hotload();
}
public void configureStudioGenerationSource(Path source) {
if (!studio) {
throw new IllegalStateException("An authoring pack source requires a Studio engine.");
}
Path required = Objects.requireNonNull(source, "Studio generation source").toAbsolutePath().normalize();
synchronized (lifecycleLock) {
requireRunning("configure the Studio authoring pack");
if (studioGenerationSource != null && !studioGenerationSource.equals(required)) {
throw new IllegalStateException("The Studio authoring pack is already configured.");
}
studioGenerationSource = required;
}
}
public Path getStudioGenerationSource() {
return Objects.requireNonNull(studioGenerationSource, "Studio authoring pack is not configured");
}
@Override
public Path getPackSource() {
Path source = studioGenerationSource;
return source == null ? getData().getDataFolder().toPath() : source;
}
public void hotloadComplex() {
hotloader.hotloadComplex();
}
@@ -827,6 +840,10 @@ public class IrisEngine implements Engine {
return generationRuntimeScopes.current() != null;
}
private boolean usesScopedNaturalTerrain() {
return hasGenerationRuntimeScope() && getComplex().isNaturalTerrainContext();
}
private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScopeUnchecked(
int blockX,
int blockZ,
@@ -956,7 +973,7 @@ public class IrisEngine implements Engine {
EngineTarget requiredTarget = Objects.requireNonNull(runtimeTarget, "detached generation target");
Path requiredMantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
synchronized (lifecycleLock) {
requireRunning("build a detached generation runtime");
requireGenerationRuntimeMutation("build a detached generation runtime");
GenerationRuntime active = runtime.generation();
if (requiredTarget.getWorld() != active.target().getWorld()) {
throw new IllegalArgumentException("Detached generation runtime must target this Iris world.");
@@ -1066,7 +1083,7 @@ public class IrisEngine implements Engine {
public void setDefaultGenerationRuntime(GenerationRuntimeBinding binding) {
GenerationRuntimeBinding required = Objects.requireNonNull(binding, "generation runtime binding");
synchronized (lifecycleLock) {
requireRunning("select the default generation runtime");
requireGenerationRuntimeMutation("select the default generation runtime");
if (required.engine != this) {
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
}
@@ -1439,6 +1456,14 @@ public class IrisEngine implements Engine {
}
}
private void requireGenerationRuntimeMutation(String operation) {
if (!closed && studio && studioGenerationTransition && lifecycleState == LifecycleState.HOTLOADING
&& generationHistoryRuntimeRouter != null) {
return;
}
requireRunning(operation);
}
private boolean EngineSafe() {
// Todo: this has potential if done right
int EngineMCVersion = getEngineData().getStatistics().getMCVersion();
@@ -0,0 +1,240 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.actuator.IrisTransitionGeometryActuator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.TransitionFluidContainment;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.hunk.Hunk;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class ResolvedTerrainProvider {
private static final int MAXIMUM_CACHED_CHUNKS = 64;
private static final int MAXIMUM_HEIGHT_CHUNKS = 4_096;
private final Engine engine;
private final LinkedHashMap<Long, SavedTerrainChunk> completed = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> pending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, SavedTerrainChunk> rawCompleted = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> rawPending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, int[]> heights = new LinkedHashMap<>(64, 0.75F, true);
public ResolvedTerrainProvider(Engine engine) {
this.engine = engine;
}
public TerrainBoundarySignature column(int blockX, int blockZ) {
return resolve(blockX >> 4, blockZ >> 4).column(blockX, blockZ);
}
public int height(int blockX, int blockZ, boolean ignoreFluid) {
long key = chunkKey(blockX >> 4, blockZ >> 4);
synchronized (heights) {
int[] cached = heights.get(key);
if (cached != null) {
return cached[(ignoreFluid ? 256 : 0) + (blockX & 15) * 16 + (blockZ & 15)];
}
}
TerrainBoundarySignature column = resolve(blockX >> 4, blockZ >> 4).column(blockX, blockZ);
return ignoreFluid ? column.oceanFloorHeight() : column.surfaceHeight();
}
public void generate(EngineMode mode, int x, int z, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, boolean multicore, ChunkContext context) {
SavedTerrainChunk terrain;
if (context.getComplex().getTransitionGenerationPlan() != null
&& context.getComplex().getTransitionGenerationPlan().hasTransitionAtChunk(x >> 4, z >> 4)) {
terrain = resolve(x >> 4, z >> 4);
copy(terrain, blocks, biomes, context);
} else {
mode.generateTerrain(x, z, blocks, biomes, multicore, context);
terrain = capture(x, z, blocks, biomes, context, true);
}
if (engine instanceof IrisEngine irisEngine) {
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router != null) {
router.recordNaturalTerrain(terrain.boundaryOnly());
}
}
}
public void clear() {
synchronized (completed) {
completed.clear();
}
synchronized (rawCompleted) {
rawCompleted.clear();
}
synchronized (heights) {
heights.clear();
}
}
private SavedTerrainChunk resolve(int chunkX, int chunkZ) {
return resolve(chunkX, chunkZ, false);
}
private SavedTerrainChunk resolve(int chunkX, int chunkZ, boolean raw) {
LinkedHashMap<Long, SavedTerrainChunk> cache = raw ? rawCompleted : completed;
ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> loads = raw ? rawPending : pending;
long key = chunkKey(chunkX, chunkZ);
synchronized (cache) {
SavedTerrainChunk cached = cache.get(key);
if (cached != null) {
return cached;
}
}
CompletableFuture<SavedTerrainChunk> result = new CompletableFuture<>();
CompletableFuture<SavedTerrainChunk> existing = loads.putIfAbsent(key, result);
if (existing != null) {
return existing.join();
}
try {
synchronized (cache) {
SavedTerrainChunk published = cache.get(key);
if (published != null) {
result.complete(published);
return published;
}
}
SavedTerrainChunk terrain = raw ? compute(chunkX, chunkZ) : contain(chunkX, chunkZ);
if (!raw) {
cacheHeights(key, terrain);
}
synchronized (cache) {
cache.put(key, terrain);
while (cache.size() > MAXIMUM_CACHED_CHUNKS) {
cache.pollFirstEntry();
}
}
result.complete(terrain);
return terrain;
} catch (RuntimeException | Error failure) {
result.completeExceptionally(failure);
throw failure;
} finally {
loads.remove(key, result);
}
}
private static long chunkKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
private void cacheHeights(long key, SavedTerrainChunk terrain) {
int[] values = new int[512];
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x,
(terrain.chunkZ() << 4) + z);
int index = x * 16 + z;
values[index] = column.surfaceHeight();
values[256 + index] = column.oceanFloorHeight();
}
}
synchronized (heights) {
heights.put(key, values);
while (heights.size() > MAXIMUM_HEIGHT_CHUNKS) {
heights.pollFirstEntry();
}
}
}
private SavedTerrainChunk contain(int chunkX, int chunkZ) {
SavedTerrainChunk raw = resolve(chunkX, chunkZ, true);
TransitionGenerationPlan plan = engine.getComplex().getTransitionGenerationPlan();
if (plan == null || !plan.hasTransitionAtChunk(chunkX, chunkZ)) {
return raw;
}
try {
return TransitionFluidContainment.contain(raw, plan, (x, z) -> rawColumn(plan, raw, x, z));
} catch (IOException failure) {
throw new IllegalStateException("Unable to contain transition fluids at " + chunkX + "," + chunkZ, failure);
}
}
private BoundaryColumnGeometry rawColumn(TransitionGenerationPlan plan, SavedTerrainChunk current, int x, int z)
throws IOException {
if (plan.isHistoricalBlock(x, z)) {
BoundaryColumnGeometry geometry = plan.terrainSignatures().signatureAt(x, z)
.orElseThrow(() -> new IOException("Missing saved terrain boundary column at " + x + "," + z))
.geometry();
if (geometry.minimumY() != engine.getMinHeight() || geometry.height() != engine.getHeight()) {
throw new IOException("Saved terrain boundary column has the wrong vertical layout at " + x + "," + z);
}
return geometry;
}
return (x >> 4) == current.chunkX() && (z >> 4) == current.chunkZ()
? current.column(x, z).geometry() : resolve(x >> 4, z >> 4, true).column(x, z).geometry();
}
private SavedTerrainChunk compute(int chunkX, int chunkZ) {
int x = Math.multiplyExact(chunkX, 16);
int z = Math.multiplyExact(chunkZ, 16);
try (GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = engine instanceof IrisEngine irisEngine
? irisEngine.openGenerationHistoryCoordinateScope(x, z) : null) {
IrisContext active = IrisContext.get();
long sessionId = active == null ? 0L : active.getGenerationSessionId();
ChunkContext context = new ChunkContext(x, z, engine.getComplex(), sessionId, true,
ChunkContext.PrefillPlan.NATURAL_TERRAIN, engine.getMetrics(), engine.getDimensionStackContext());
context.beginSpeculativeTerrain();
Hunk<PlatformBlockState> blocks = Hunk.newArrayHunk(16, engine.getHeight(), 16);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, engine.getHeight(), 16);
try (IrisContext.Scope ignored = IrisContext.open(engine, sessionId, context)) {
engine.getMode().generateTerrain(x, z, blocks, biomes, false, context);
return capture(x, z, blocks, biomes, context, false);
}
} catch (IOException failure) {
throw new IllegalStateException("Unable to scope natural terrain at " + chunkX + "," + chunkZ, failure);
}
}
private SavedTerrainChunk capture(int x, int z, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, ChunkContext context, boolean boundaryOnly) {
try {
return IrisTransitionGeometryActuator.capture(x, z, blocks, biomes, engine.getMinHeight(), context, boundaryOnly);
} catch (IOException failure) {
throw new IllegalStateException("Unable to capture natural terrain at " + (x >> 4) + "," + (z >> 4), failure);
}
}
private static void copy(SavedTerrainChunk terrain, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, ChunkContext context) {
PlatformRegistries registries = IrisPlatforms.get().registries();
Map<String, PlatformBlockState> states = new HashMap<>();
Map<String, PlatformBiome> physicalBiomes = new HashMap<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
TerrainBoundarySignature column = terrain.column((terrain.chunkX() << 4) + x, (terrain.chunkZ() << 4) + z);
BoundaryColumnGeometry geometry = column.geometry();
for (int offset = 0; offset < blocks.getHeight(); offset++) {
String stateKey = geometry.voxelAt(geometry.minimumY() + offset).stateKey();
PlatformBlockState state = states.computeIfAbsent(stateKey, registries::blockOrNull);
if (state == null) {
throw new IllegalStateException("Resolved terrain requires unavailable block state " + stateKey);
}
blocks.setRaw(x, offset, z, state);
String biomeKey = column.biomeAtSample(offset / 4);
biomes.setRaw(x, offset, z, physicalBiomes.computeIfAbsent(biomeKey, registries::biome));
}
context.setTerrainHeight(x, z, column.oceanFloorHeight());
}
}
}
}
@@ -0,0 +1,88 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationPackRepository;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisDimension;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Files;
import java.util.List;
record StudioGenerationUpdate(
Path stagingRoot,
Path source,
String fingerprint,
GenerationEpoch.DimensionContract dimensionContract,
GenerationRegistryContract registryContract
) implements AutoCloseable {
static StudioGenerationUpdate prepare(IrisEngine engine, GenerationHistory history) throws IOException {
Path source = engine.getStudioGenerationSource();
IrisData.invalidateLoadedAuthoringResources(source.toFile());
String fingerprint = GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION);
GenerationEpoch active = history.activeEpoch();
if (active.packFingerprintVersion() == GenerationPackFingerprint.CURRENT_VERSION
&& fingerprint.equals(active.packFingerprint())) {
return null;
}
Path stagingRoot = Files.createTempDirectory(history.paths().generationRoot(), ".studio-");
Path snapshot = stagingRoot.resolve(source.getFileName());
try {
GenerationPackRepository.copyPackTree(source, snapshot);
if (!fingerprint.equals(GenerationPackFingerprint.compute(snapshot, GenerationPackFingerprint.CURRENT_VERSION))) {
throw new IOException("Studio pack changed while its update snapshot was being captured.");
}
return prepareSnapshot(engine, history, stagingRoot, snapshot, fingerprint);
} catch (Throwable failure) {
try {
AtomicDirectoryPublisher.deleteTree(stagingRoot);
} catch (IOException cleanupFailure) {
failure.addSuppressed(cleanupFailure);
}
throw failure;
}
}
private static StudioGenerationUpdate prepareSnapshot(IrisEngine engine, GenerationHistory history,
Path stagingRoot, Path source, String fingerprint)
throws IOException {
GenerationEpoch active = history.activeEpoch();
PackValidationResult validation = PackValidator.validate(source.toFile());
if (!validation.isLoadable()) {
throw new IOException("Studio pack update is invalid: " + String.join("; ", validation.getBlockingErrors()));
}
IrisData data = IrisData.openRuntime(source.toFile());
try {
IrisDimension replacement = data.getDimensionLoader().load(active.dimensionContract().dimensionKey());
if (replacement == null) {
throw new IOException("Studio pack update is missing dimension '"
+ active.dimensionContract().dimensionKey() + "'.");
}
engine.getPlatformHooks().validateDimensionHotload(engine, replacement);
GenerationEpoch.DimensionContract contract = GenerationEpochContractFactory.create(
replacement, replacement.getLoadKey(), active.dimensionContract().dimensionTypeKey());
if (!active.dimensionContract().hasSameLayout(contract)) {
throw new IOException("Studio pack update changes the world's immutable dimension layout.");
}
GenerationRegistryContract registry = GenerationRegistryContractFactory.create(data, replacement, fingerprint);
registry.requireDefinitionsAvailableIn(GenerationRegistryContractFactory.captureRequiredDefinitions(List.of(registry)));
return new StudioGenerationUpdate(stagingRoot, source, fingerprint, contract, registry);
} finally {
data.close();
}
}
@Override
public void close() throws IOException {
AtomicDirectoryPublisher.deleteTree(stagingRoot);
}
}
@@ -20,8 +20,6 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationBlend;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
@@ -33,16 +31,12 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
public class UpperDimensionContext implements DataProvider {
private final DimensionTerrainContext terrainContext;
private final ProceduralStream<Double> lowerHeightStream;
private final ProceduralStream<Double> unblendedLowerHeightStream;
private final TransitionGenerationPlan transitionPlan;
private final CeilingLayout ceilingLayout;
private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) {
this.terrainContext = terrainContext;
IrisComplex complex = engine.getComplex();
lowerHeightStream = complex.getHeightStream();
unblendedLowerHeightStream = complex.getUnblendedNaturalHeightStream();
transitionPlan = complex.getTransitionGenerationPlan();
ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap());
}
@@ -103,18 +97,6 @@ public class UpperDimensionContext implements DataProvider {
0D,
Math.min(ceilingLayout.height() - 1D, terrainContext.getNormalTerrainHeight(x, z))
);
if (transitionPlan != null) {
TransitionGenerationPlan.TerrainSample sample = transitionPlan.terrainSampleAt(x, z);
if (sample.newEpochWeight() < 1D && sample.hasHistoricalSignature()) {
int targetLowerSurfaceY = (int) Math.min(
ceilingLayout.height(),
Math.round(unblendedLowerHeightStream.getDouble(x, z))
);
int targetSurfaceY = effectiveSurfaceY(depth, ceilingLayout, targetLowerSurfaceY);
double targetDepth = Math.max(0D, ceilingLayout.height() - 1D - targetSurfaceY);
depth = blendCeilingDepth(targetDepth, sample);
}
}
int lowerSurfaceY = (int) Math.min(
ceilingLayout.height(),
Math.round(lowerHeightStream.getDouble(x, z))
@@ -122,14 +104,6 @@ public class UpperDimensionContext implements DataProvider {
return effectiveSurfaceY(depth, ceilingLayout, lowerSurfaceY);
}
static double blendCeilingDepth(double newDepth, TransitionGenerationPlan.TerrainSample sample) {
if (sample.newEpochWeight() == 1D || !sample.hasHistoricalSignature()) {
return newDepth;
}
return GenerationBlend.interpolate(
sample.historicalUpperCeilingDepth(), newDepth, sample.newEpochWeight());
}
static int effectiveSurfaceY(double depth, CeilingLayout layout, int lowerSurfaceY) {
long mirroredSurface = layout.height() - 1L - Math.round(depth);
long gapSurface = (long) lowerSurfaceY + layout.minimumGap();
@@ -36,6 +36,7 @@ import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
@@ -65,7 +66,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
int depth = h.getDepth();
int height = h.getHeight();
Engine engine = getEngine();
Mantle<Matter> mantle = engine.getMantle().getMantle();
Mantle<Matter> mantle = context.isSpeculativeTerrain() ? null : engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
IrisComplex complex = context.getComplex();
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
@@ -74,21 +75,6 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
for (int zf = 0; zf < depth; zf++) {
int worldX = x + xf;
int worldZ = z + zf;
if (!complex.allowsNewDiscreteContentAt(worldX, worldZ)) {
writeHistoricalColumn(
h,
mantle,
xf,
zf,
worldX,
worldZ,
height,
engine,
complex
);
continue;
}
IrisBiome biome = biomeCache.get(xf, zf);
ResolvedBiome resolved = resolve(biomeKey(
biome,
@@ -117,11 +103,39 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
context.getDimensionStackLayout(xf, zf)
);
}
writeHistoricalColumn(h, mantle, xf, zf, worldX, worldZ, height, engine, complex);
}
}
engine.getMetrics().getBiome().put(p.getMilliseconds());
}
public static void publishNaturalMetadata(Engine engine, int x, int z, Hunk<PlatformBiome> biomes,
ChunkContext context) {
if (context.isSpeculativeTerrain() || !context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) {
return;
}
Mantle<Matter> mantle = engine.getMantle().getMantle();
MantleChunk<Matter> chunk = mantle.getChunk(x >> 4, z >> 4).use();
try {
synchronized (chunk) {
IrisDimensionStackActuator.clearNaturalMetadata(chunk, context, biomes.getHeight());
chunk.deleteSlices(MatterBiomeInject.class);
for (int localX = 0; localX < biomes.getWidth(); localX += 4) {
for (int localZ = 0; localZ < biomes.getDepth(); localZ += 4) {
for (int y = 0; y < biomes.getHeight(); y += 4) {
PlatformBiome biome = biomes.getRaw(localX, y, localZ);
if (biome != null) {
mantle.set(x + localX, y, z + localZ, BiomeInjectMatter.get(biome.key()));
}
}
}
}
}
} finally {
chunk.release();
}
}
private void writeHistoricalColumn(
Hunk<PlatformBiome> output,
Mantle<Matter> mantle,
@@ -137,14 +151,17 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
if (transitionPlan == null) {
return;
}
TransitionGenerationPlan.TerrainSample sample = transitionPlan.terrainSampleAt(worldX, worldZ);
TransitionGenerationPlan.TerrainSample terrainSample = transitionPlan.terrainSampleAt(worldX, worldZ);
if (terrainSample.newEpochWeight() == 1D) {
return;
}
int minimumWorldY = engine.getMinHeight();
String activeKey = sample.historicalPhysicalBiomeKeyAt(minimumWorldY).orElse(null);
String activeKey = transitionPlan.historicalPhysicalBiomeKeyAt(worldX, minimumWorldY, worldZ, terrainSample).orElse(null);
int rangeStart = 0;
for (int internalY = 1; internalY <= height; internalY++) {
String nextKey = internalY == height
? null
: sample.historicalPhysicalBiomeKeyAt(minimumWorldY + internalY).orElse(null);
: transitionPlan.historicalPhysicalBiomeKeyAt(worldX, minimumWorldY + internalY, worldZ, terrainSample).orElse(null);
if (Objects.equals(activeKey, nextKey)) {
continue;
}
@@ -179,8 +196,10 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
if (resolved.biome() != null) {
output.set(localX, 0, localZ, localX, height - 1, localZ, resolved.biome());
}
if (mantle != null) {
mantle.set(worldX, 0, worldZ, resolved.matter());
}
}
private void applyDimensionStackBiomes(
int localX,
@@ -256,7 +275,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
if (minimumY > maximumY) {
return;
}
if (mantle != null) {
clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY);
}
if (resolved.biome() != null) {
output.set(
localX,
@@ -268,7 +289,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
resolved.biome()
);
}
if (resolved.matter() != null) {
if (mantle != null && resolved.matter() != null) {
mantle.set(worldX, minimumY, worldZ, resolved.matter());
}
}
@@ -33,6 +33,7 @@ import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.matter.TileWrapper;
import art.arcane.iris.util.project.matter.PreObjectMatterCell;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -81,8 +82,8 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
if (getEngine().getDimensionStackContext() == null) {
return;
}
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle()
.getChunk(x >> 4, z >> 4).use();
MantleChunk<Matter> mantleChunk = context.isSpeculativeTerrain() ? null
: getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
try {
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
for (int localX = 0; localX < blocks.getWidth(); localX++) {
@@ -100,9 +101,28 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
}
}
} finally {
if (mantleChunk != null) {
mantleChunk.release();
}
}
}
public static void clearNaturalMetadata(MantleChunk<Matter> chunk, ChunkContext context, int height) {
MetadataCleaner metadata = new MetadataCleaner(chunk, height);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
DimensionStackLayout layout = context.getDimensionStackLayout(x, z);
if (layout == null) {
continue;
}
for (int y = 0; y < height; y++) {
if (layout.isHostFeatureProtectedY(y)) {
metadata.clear(x, y, z);
}
}
}
}
}
private void renderLayers(
int localX,
@@ -229,7 +249,12 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
}
void clear(int x, int y, int z) {
if (chunk == null) {
return;
}
synchronized (chunk) {
int sectionIndex = y >> 4;
captureNaturalFacts(sectionIndex, x, y & 15, z);
List<MatterSlice<?>> slices = slicesBySection[sectionIndex];
if (slices == null) {
slices = resolveSlices(sectionIndex);
@@ -239,6 +264,30 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
slice.set(x, y & 15, z, null);
}
}
}
private void captureNaturalFacts(int sectionIndex, int x, int y, int z) {
Matter section = chunk.get(sectionIndex);
if (section == null) {
return;
}
MatterSlice<MatterCavern> caverns = section.getSlice(MatterCavern.class);
MatterSlice<HydrologyCaveCell> hydrology = section.getSlice(HydrologyCaveCell.class);
MatterCavern cavern = caverns == null ? null : caverns.get(x, y, z);
HydrologyCaveCell cell = hydrology == null ? null : hydrology.get(x, y, z);
if (cavern == null && cell == null) {
return;
}
MatterSlice<PreObjectMatterCell> journal = section.slice(PreObjectMatterCell.class);
PreObjectMatterCell previous = journal.get(x, y, z);
if (cavern != null) {
previous = previous == null ? PreObjectMatterCell.cavern(cavern) : previous.captureCavern(cavern);
}
if (cell != null) {
previous = previous == null ? PreObjectMatterCell.hydrology(cell) : previous.captureHydrology(cell);
}
journal.set(x, y, z, previous);
}
private List<MatterSlice<?>> resolveSlices(int sectionIndex) {
Matter section = chunk.get(sectionIndex);
@@ -0,0 +1,157 @@
package art.arcane.iris.engine.actuator;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedComponent;
import art.arcane.iris.engine.framework.EngineStage;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.engine.history.BoundaryGeometryInfluence;
import art.arcane.iris.engine.history.GenerationBlend;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.TransitionGeometryBlender;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.hunk.Hunk;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
public final class IrisTransitionGeometryActuator extends EngineAssignedComponent implements EngineStage {
public IrisTransitionGeometryActuator(Engine engine) {
super(engine, "Terrain Transition");
}
@Override
public void close() {
super.close();
}
@Override
public void generate(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes,
boolean multicore, ChunkContext context) {
TransitionGenerationPlan plan = context.getComplex().getTransitionGenerationPlan();
if (plan == null || !plan.hasTransitionAtChunk(x >> 4, z >> 4)) {
return;
}
GeometrySource source = new GeometrySource(blocks, biomes, getEngine().getMinHeight(), context);
if (plan != null && plan.hasTransitionAtChunk(x >> 4, z >> 4)) {
blendGeometry(x, z, blocks, context, source, plan);
}
}
public static SavedTerrainChunk capture(int x, int z, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, int minimumY,
ChunkContext context, boolean boundaryOnly) throws IOException {
GeometrySource source = new GeometrySource(blocks, biomes, minimumY, context);
return boundaryOnly
? SavedTerrainChunk.captureBoundary(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source)
: SavedTerrainChunk.capture(x >> 4, z >> 4, minimumY, blocks.getHeight(), "minecraft:noise", source);
}
private void blendGeometry(int x, int z, Hunk<PlatformBlockState> blocks, ChunkContext context,
GeometrySource source, TransitionGenerationPlan plan) {
PlatformRegistries registries = IrisPlatforms.get().registries();
Map<String, PlatformBlockState> resolved = new HashMap<>();
for (int localX = 0; localX < blocks.getWidth(); localX++) {
for (int localZ = 0; localZ < blocks.getDepth(); localZ++) {
BoundaryGeometryInfluence influence = plan.geometryAt(x + localX, z + localZ);
if (influence.newTerrainWeight() == 1D || influence.contributions().isEmpty()) {
continue;
}
BoundaryColumnGeometry current = source.column(localX, localZ);
BoundaryColumnGeometry blended = TransitionGeometryBlender.blendColumn(
influence, x + localX, z + localZ, current);
if (blended == current) {
continue;
}
double expectedFloor = GenerationBlend.interpolate(
plan.terrainSampleAt(x + localX, z + localZ).historicalOceanFloorHeight(),
context.getRoundedHeight(localX, localZ), influence.newTerrainWeight());
int floor = blended.surfaceOffsetNear(expectedFloor);
List<BoundaryColumnGeometry.Voxel> voxels = blended.voxels();
for (int offset = 0; offset < voxels.size(); offset++) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(offset);
PlatformBlockState existing = blocks.getRaw(localX, offset, localZ);
if (existing != null && existing.key().equals(voxel.stateKey())) {
continue;
}
PlatformBlockState replacement = resolved.computeIfAbsent(voxel.stateKey(), registries::blockOrNull);
if (replacement == null) {
throw new IllegalStateException("Terrain boundary requires unavailable block state " + voxel.stateKey());
}
blocks.setRaw(localX, offset, localZ, replacement);
}
context.setTerrainHeight(localX, localZ, floor);
}
}
}
private static final class GeometrySource implements SavedTerrainChunk.VoxelSource {
private final Hunk<PlatformBlockState> blocks;
private final Hunk<PlatformBiome> biomes;
private final int minimumY;
private final ChunkContext context;
private final Map<PlatformBlockState, BoundaryColumnGeometry.Voxel> voxels = new IdentityHashMap<>();
private GeometrySource(Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes, int minimumY, ChunkContext context) {
this.blocks = blocks;
this.biomes = biomes;
this.minimumY = minimumY;
this.context = context;
}
@Override
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
PlatformBlockState state = blocks.getRaw(localX, worldY - minimumY, localZ);
if (state == null) {
state = IrisPlatforms.get().registries().air();
}
return voxels.computeIfAbsent(state, GeometrySource::encode);
}
@Override
public OptionalInt groundSurface(int localX, int localZ) {
return OptionalInt.of(Math.max(0, Math.min(blocks.getHeight() - 1, context.getRoundedHeight(localX, localZ))));
}
@Override
public String biome(int localX, int worldY, int localZ) throws IOException {
PlatformBiome biome = biomes.getRaw(localX, worldY - minimumY, localZ);
if (biome == null) {
throw new IOException("Natural terrain has no physical biome at " + localX + "," + worldY + "," + localZ);
}
return biome.key();
}
private BoundaryColumnGeometry column(int localX, int localZ) {
ArrayList<BoundaryColumnGeometry.Voxel> values = new ArrayList<>(blocks.getHeight());
for (int offset = 0; offset < blocks.getHeight(); offset++) {
values.add(voxel(localX, minimumY + offset, localZ));
}
return BoundaryColumnGeometry.fromVoxels(minimumY, values);
}
private static BoundaryColumnGeometry.Voxel encode(PlatformBlockState state) {
boolean custom = state.isCustom();
PlatformBlockState nativeState = state.placementBaseState();
if (nativeState != null) {
state = nativeState;
}
BoundaryColumnGeometry.Phase phase = state.isAir() ? BoundaryColumnGeometry.Phase.AIR
: state.isFluid() ? BoundaryColumnGeometry.Phase.FLUID : BoundaryColumnGeometry.Phase.SOLID;
String fluidKey = state.isFluid() ? state.key()
: state.isWaterLogged() ? "minecraft:water[level=0]" : "";
boolean protectedContent = custom || state.isDecorant() || state.isFoliage() || state.isTreeBlock()
|| state.hasTileEntity();
return new BoundaryColumnGeometry.Voxel(state.key(), phase, fluidKey, protectedContent);
}
}
}
@@ -40,17 +40,16 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
double localFluidHeight = getComplex().getRiverWaterSurfaceStream().get(realX, realZ);
double localFluidHeight = getEngine().getMantle().getFluidHeight(realX, realZ);
if (height != Math.round(localFluidHeight)) {
return;
}
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
&& Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1))
&& Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) {
if (Math.round(heightStream.get(realX1, realZ)) >= getEngine().getMantle().getFluidHeight(realX1, realZ)
&& Math.round(heightStream.get(realX_1, realZ)) >= getEngine().getMantle().getFluidHeight(realX_1, realZ)
&& Math.round(heightStream.get(realX, realZ1)) >= getEngine().getMantle().getFluidHeight(realX, realZ1)
&& Math.round(heightStream.get(realX, realZ_1)) >= getEngine().getMantle().getFluidHeight(realX, realZ_1)) {
return;
}
@@ -60,7 +60,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
@BlockCoordinates
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, InferredType inferredType, int height, int max) {
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(realX, realZ));
int fluidHeight = getEngine().getMantle().getFluidHeight(realX, realZ);
if (inferredType == InferredType.SHORE && height < fluidHeight) {
return;
}
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.engine.framework.render.Renderer;
@@ -75,6 +76,9 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
import org.jetbrains.annotations.Nullable;
import java.awt.Color;
import java.nio.file.Path;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
@@ -166,12 +170,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
if (context != null && context.getEngine() == this && context.getGenerationSessionId() != 0L) {
return generationSessions.continueSession(operation, context.getGenerationSessionId());
}
if (isClosing() || isClosed()) {
throw new GenerationSessionException("Generation session rejected new work for " + operation
+ " while the Iris engine is closing.", isClosed());
}
return generationSessions.acquire(operation);
return generationSessions.acquireForEngine(this, operation);
}
default long getGenerationSessionId() {
@@ -238,6 +237,8 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return getTarget().getData();
}
Path getPackSource();
default IrisWorld getWorld() {
return getTarget().getWorld();
}
@@ -448,6 +449,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default int getHeight(int x, int z, boolean ignoreFluid) {
OptionalInt resolved = getComplex().resolvedTerrainHeight(x, z, ignoreFluid);
if (resolved.isPresent()) {
return resolved.getAsInt();
}
DimensionStackContext dimensionStackContext = getDimensionStackContext();
if (dimensionStackContext != null) {
return ignoreFluid
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.framework;
import art.arcane.iris.util.project.context.IrisContext;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
public final class EngineLifecycleTasks {
@@ -13,10 +14,16 @@ public final class EngineLifecycleTasks {
Objects.requireNonNull(engine);
Objects.requireNonNull(operation);
Objects.requireNonNull(task);
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
try {
Optional<GenerationSessionLease> admitted = tryAcquire(engine, operation);
if (admitted.isEmpty()) {
return false;
}
try (GenerationSessionLease lease = admitted.get();
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
task.run();
return true;
}
} catch (GenerationSessionException exception) {
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
return false;
@@ -27,9 +34,17 @@ public final class EngineLifecycleTasks {
public static <T> T call(Engine engine, String operation, Supplier<T> task, T unavailable) {
Objects.requireNonNull(task);
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
Objects.requireNonNull(engine);
Objects.requireNonNull(operation);
try {
Optional<GenerationSessionLease> admitted = tryAcquire(engine, operation);
if (admitted.isEmpty()) {
return unavailable;
}
try (GenerationSessionLease lease = admitted.get();
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return task.get();
}
} catch (GenerationSessionException exception) {
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
return unavailable;
@@ -37,4 +52,11 @@ public final class EngineLifecycleTasks {
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
}
}
private static Optional<GenerationSessionLease> tryAcquire(Engine engine, String operation)
throws GenerationSessionException {
GenerationSessionManager sessions = engine.getGenerationSessions();
return sessions == null ? Optional.of(GenerationSessionLease.noop())
: sessions.tryAcquireForEngine(engine, operation);
}
}
@@ -19,6 +19,8 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.actuator.IrisBiomeActuator;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.util.project.context.ChunkContext;
@@ -31,6 +33,21 @@ import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
public interface EngineMode extends Staged {
KList<EngineStage> getTerrainStages();
void registerTerrainStage(EngineStage stage);
EngineStage getTransitionStage();
default void generateTerrain(int x, int z, Hunk<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes,
boolean multicore, ChunkContext context) {
context.setTerrainBiomeOutput(biomes);
for (EngineStage stage : getTerrainStages()) {
stage.generate(x, z, blocks, biomes, multicore, context);
}
getTransitionStage().generate(x, z, blocks, biomes, multicore, context);
}
void close();
Engine getEngine();
@@ -159,8 +176,12 @@ public interface EngineMode extends Staged {
return getEngine().getMantle();
}
default void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateMatter(x, z, multicore, context);
default void generateTerrainMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateTerrainMatter(x, z, multicore, context);
}
default void generateContentMatter(int x, int z, boolean multicore, ChunkContext context) {
getMantle().generateContentMatter(x, z, multicore, context);
}
@BlockCoordinates
@@ -180,6 +201,9 @@ public interface EngineMode extends Staged {
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
getComplex().getResolvedTerrain().generate(this, x, z, blocks, biomes, multicore, ctx);
IrisBiomeActuator.publishNaturalMetadata(getEngine(), x, z, biomes, ctx);
ctx.beginContent();
for (EngineStage i : stages) {
i.generate(x, z, blocks, biomes, multicore, ctx);
}
@@ -19,10 +19,23 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import java.util.concurrent.CompletableFuture;
import art.arcane.iris.engine.mantle.MantleComponent;
import art.arcane.volmlib.util.collection.KList;
public interface EnginePlatformHooks {
default CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"This platform does not support saved terrain checkpoints."));
}
default CompletableFuture<SavedTerrainChunk> captureSavedTerrainChunk(Engine engine, int chunkX, int chunkZ) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"This platform does not support saved terrain boundary capture."));
}
/**
* World-space piece bounds of every native structure that will generate inside the given XZ rect. Platforms
* without native structures return no volumes, which keeps the object veto free on those platforms.
@@ -1,17 +1,20 @@
package art.arcane.iris.engine.framework;
public final class GenerationSessionLease implements AutoCloseable {
private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L);
private static final GenerationSessionLease NOOP = new GenerationSessionLease(null, null, 0L, null);
private final GenerationSessionManager manager;
private final GenerationSessionManager.GenerationSessionState state;
private final long sessionId;
private final GenerationTransitionGate.Participation participation;
private boolean released;
GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state, long sessionId) {
GenerationSessionLease(GenerationSessionManager manager, GenerationSessionManager.GenerationSessionState state,
long sessionId, GenerationTransitionGate.Participation participation) {
this.manager = manager;
this.state = state;
this.sessionId = sessionId;
this.participation = participation;
this.released = false;
}
@@ -23,6 +26,12 @@ public final class GenerationSessionLease implements AutoCloseable {
return sessionId;
}
public void detachThread() {
if (participation != null) {
participation.detachThread();
}
}
@Override
public void close() {
if (released || state == null) {
@@ -30,6 +39,10 @@ public final class GenerationSessionLease implements AutoCloseable {
}
released = true;
try {
manager.releaseLease(state);
} finally {
participation.close();
}
}
}
@@ -1,22 +1,94 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.util.project.context.IrisContext;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.CancellationException;
public final class GenerationSessionManager {
private final AtomicLong sessionSequence;
private final AtomicReference<GenerationSessionState> current;
private final Object drainMonitor;
private final GenerationTransitionGate transitionGate;
public GenerationSessionManager() {
this(false);
}
public GenerationSessionManager(boolean studioTransitions) {
this.transitionGate = new GenerationTransitionGate(studioTransitions);
this.sessionSequence = new AtomicLong(0L);
this.current = new AtomicReference<>(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
this.drainMonitor = new Object();
}
public GenerationSessionLease acquire(String operation) throws GenerationSessionException {
GenerationTransitionGate.Participation participation = transitionGate.enter();
try {
return acquireAdmitted(operation, participation);
} catch (Throwable failure) {
participation.close();
throw failure;
}
}
public GenerationTransitionGate transitionGate() {
return transitionGate;
}
public GenerationSessionLease acquireForEngine(Engine engine, String operation) throws GenerationSessionException {
GenerationTransitionGate.Participation participation;
try {
participation = transitionGate.enter();
} catch (CancellationException cancellation) {
GenerationSessionException failure = new GenerationSessionException(
"Generation session cancelled while waiting to run " + operation + ".",
engine.isClosing() || engine.isClosed());
failure.initCause(cancellation);
throw failure;
}
try {
if (engine.isClosing() || engine.isClosed()) {
throw new GenerationSessionException("Generation session rejected new work for " + operation
+ " while the Iris engine is closing.", engine.isClosed());
}
return acquireAdmitted(operation, participation);
} catch (Throwable failure) {
participation.close();
throw failure;
}
}
public Optional<GenerationSessionLease> tryAcquireForEngine(Engine engine, String operation)
throws GenerationSessionException {
Optional<GenerationTransitionGate.Participation> entered = transitionGate.tryEnter();
if (entered.isEmpty()) {
return Optional.empty();
}
GenerationTransitionGate.Participation participation = entered.get();
try {
if (engine.isClosing() || engine.isClosed()) {
throw new GenerationSessionException("Generation session rejected new work for " + operation
+ " while the Iris engine is closing.", engine.isClosed());
}
IrisContext context = IrisContext.get();
return Optional.of(context != null && context.getEngine() == engine && context.getGenerationSessionId() != 0L
? continueAdmittedSession(operation, context.getGenerationSessionId(), participation)
: acquireAdmitted(operation, participation));
} catch (Throwable failure) {
participation.close();
throw failure;
}
}
private GenerationSessionLease acquireAdmitted(String operation,
GenerationTransitionGate.Participation participation)
throws GenerationSessionException {
while (true) {
GenerationSessionState state = current.get();
if (state == null || !state.accepting().get()) {
@@ -34,11 +106,23 @@ public final class GenerationSessionManager {
throw rejected(operation, state);
}
return new GenerationSessionLease(this, state, state.sessionId());
return new GenerationSessionLease(this, state, state.sessionId(), participation);
}
}
public GenerationSessionLease continueSession(String operation, long sessionId) throws GenerationSessionException {
GenerationTransitionGate.Participation participation = transitionGate.continueAdmittedWork();
try {
return continueAdmittedSession(operation, sessionId, participation);
} catch (Throwable failure) {
participation.close();
throw failure;
}
}
private GenerationSessionLease continueAdmittedSession(String operation, long sessionId,
GenerationTransitionGate.Participation participation)
throws GenerationSessionException {
while (true) {
GenerationSessionState state = current.get();
if (state == null || state.sessionId() != sessionId) {
@@ -51,7 +135,7 @@ public final class GenerationSessionManager {
continue;
}
return new GenerationSessionLease(this, state, state.sessionId());
return new GenerationSessionLease(this, state, state.sessionId(), participation);
}
}
@@ -0,0 +1,200 @@
package art.arcane.iris.engine.framework;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
public final class GenerationTransitionGate {
private final boolean enabled;
private final Participation noop = new Participation(null);
private final Map<Thread, Integer> participants = new IdentityHashMap<>();
private int active;
private int waitingTransitions;
private Thread transitionOwner;
public GenerationTransitionGate(boolean enabled) {
this.enabled = enabled;
}
public Participation enter() {
if (!enabled) {
return noop;
}
synchronized (this) {
return enter(false);
}
}
public Optional<Participation> tryEnter() {
if (!enabled) {
return Optional.of(noop);
}
synchronized (this) {
Thread owner = Thread.currentThread();
return blocked(owner, false) ? Optional.empty() : Optional.of(admit(owner));
}
}
Participation continueAdmittedWork() {
if (!enabled) {
return noop;
}
synchronized (this) {
return enter(true);
}
}
private Participation enter(boolean continuation) {
Thread owner = Thread.currentThread();
while (blocked(owner, continuation)) {
awaitChange();
}
return admit(owner);
}
private boolean blocked(Thread owner, boolean continuation) {
return transitionOwner != null && transitionOwner != owner
|| waitingTransitions > 0 && transitionOwner != owner
&& !participants.containsKey(owner) && !(continuation && active > 0);
}
private Participation admit(Thread owner) {
active++;
participants.merge(owner, 1, Integer::sum);
return new Participation(owner);
}
public synchronized Transition beginTransition(long timeoutMillis) {
if (!enabled) {
throw new IllegalStateException("Live generation transitions are disabled for this engine.");
}
if (timeoutMillis < 1L) {
throw new IllegalArgumentException("Generation transition timeout must be positive.");
}
Thread owner = Thread.currentThread();
if (participants.containsKey(owner) || transitionOwner == owner) {
throw new IllegalStateException("Cannot reload generation from inside an active generation operation.");
}
waitingTransitions++;
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
try {
while (transitionOwner != null || active > 0) {
long remaining = deadline - System.nanoTime();
if (remaining <= 0L) {
throw new IllegalStateException("Timed out draining generation for a Studio update after "
+ timeoutMillis + "ms; active operations=" + active + ".");
}
try {
TimeUnit.NANOSECONDS.timedWait(this, remaining);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while draining generation for a Studio update.", failure);
}
}
transitionOwner = owner;
return new Transition(owner);
} finally {
waitingTransitions--;
notifyAll();
}
}
private void awaitChange() {
try {
wait();
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
CancellationException cancellation = new CancellationException(
"Interrupted while waiting for generation admission.");
cancellation.initCause(failure);
throw cancellation;
}
}
public final class Participation implements AutoCloseable {
private final Thread owner;
private boolean closed;
private boolean attached = true;
private Participation(Thread owner) {
this.owner = owner;
}
public Participation attach() {
if (owner == null) {
return this;
}
synchronized (GenerationTransitionGate.this) {
if (closed) {
throw new IllegalStateException("Generation participation has already ended.");
}
return enter(true);
}
}
public void detachThread() {
if (owner == null) {
return;
}
synchronized (GenerationTransitionGate.this) {
if (!closed && attached) {
detach();
}
}
}
private void detach() {
int depth = participants.get(owner);
if (depth == 1) {
participants.remove(owner);
} else {
participants.put(owner, depth - 1);
}
attached = false;
}
@Override
public void close() {
if (owner == null) {
return;
}
synchronized (GenerationTransitionGate.this) {
if (closed) {
return;
}
closed = true;
if (attached) {
detach();
}
active--;
GenerationTransitionGate.this.notifyAll();
}
}
}
public final class Transition implements AutoCloseable {
private final Thread owner;
private boolean closed;
private Transition(Thread owner) {
this.owner = owner;
}
@Override
public void close() {
synchronized (GenerationTransitionGate.this) {
if (closed) {
return;
}
if (Thread.currentThread() != owner || transitionOwner != owner) {
throw new IllegalStateException("Generation transition belongs to another thread.");
}
closed = true;
transitionOwner = null;
GenerationTransitionGate.this.notifyAll();
}
}
}
}
@@ -19,15 +19,20 @@
package art.arcane.iris.engine.framework;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.engine.actuator.IrisTransitionGeometryActuator;
public abstract class IrisEngineMode implements EngineMode {
private final Engine engine;
private final EngineStage transitionStage;
private final KList<EngineStage> stages;
private final KList<EngineStage> terrainStages;
private boolean closed;
public IrisEngineMode(Engine engine) {
this.engine = engine;
this.transitionStage = new IrisTransitionGeometryActuator(engine);
this.stages = new KList<>();
this.terrainStages = new KList<>();
this.closed = false;
}
@@ -39,6 +44,9 @@ public abstract class IrisEngineMode implements EngineMode {
closed = true;
dump();
terrainStages.forEach(EngineStage::close);
terrainStages.clear();
transitionStage.close();
}
@Override
@@ -51,6 +59,21 @@ public abstract class IrisEngineMode implements EngineMode {
return stages;
}
@Override
public EngineStage getTransitionStage() {
return transitionStage;
}
@Override
public KList<EngineStage> getTerrainStages() {
return terrainStages;
}
@Override
public void registerTerrainStage(EngineStage stage) {
terrainStages.add(stage);
}
@Override
public void registerStage(EngineStage stage) {
stages.add(stage);
@@ -0,0 +1,258 @@
package art.arcane.iris.engine.history;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
public final class BoundaryColumnGeometry {
public static final int MAXIMUM_HEIGHT = 65_536;
private static final Comparator<Voxel> VOXEL_ORDER = Comparator.comparing(Voxel::stateKey)
.thenComparing(Voxel::phase)
.thenComparing(Voxel::fluidStateKey)
.thenComparing(Voxel::protectedContent);
private static final Voxel AIR = new Voxel("minecraft:air", Phase.AIR, "", false);
private static final BoundaryColumnGeometry EMPTY = new BoundaryColumnGeometry(0, List.of(), new int[0], new short[0]);
private final int minimumY;
private final List<Voxel> palette;
private final int[] runEnds;
private final short[] paletteIndices;
public BoundaryColumnGeometry(int minimumY, List<Voxel> palette, int[] runEnds, short[] paletteIndices) {
this.minimumY = minimumY;
this.palette = List.copyOf(Objects.requireNonNull(palette, "Geometry palette"));
this.runEnds = Objects.requireNonNull(runEnds, "Geometry run ends").clone();
this.paletteIndices = Objects.requireNonNull(paletteIndices, "Geometry palette indices").clone();
validate();
}
public static BoundaryColumnGeometry empty() {
return EMPTY;
}
public static BoundaryColumnGeometry fromVoxels(int minimumY, List<Voxel> voxels) {
Objects.requireNonNull(voxels, "Geometry voxels");
if (voxels.size() > MAXIMUM_HEIGHT) {
throw new IllegalArgumentException("Geometry column exceeds maximum height");
}
TreeSet<Voxel> used = new TreeSet<>(VOXEL_ORDER);
Voxel previous = null;
for (Voxel voxel : voxels) {
if (previous == null || !previous.equals(voxel)) {
used.add(voxel);
previous = voxel;
}
}
List<Voxel> palette = List.copyOf(used);
Map<Voxel, Short> indices = new HashMap<>(palette.size());
for (int index = 0; index < palette.size(); index++) {
if (index > Short.MAX_VALUE) {
throw new IllegalArgumentException("Geometry palette exceeds compact index capacity");
}
indices.put(palette.get(index), (short) index);
}
int[] ends = new int[voxels.size()];
short[] values = new short[voxels.size()];
int runCount = 0;
previous = null;
for (int offset = 0; offset < voxels.size(); offset++) {
Voxel voxel = voxels.get(offset);
if (previous == null || !previous.equals(voxel)) {
values[runCount++] = indices.get(voxel);
previous = voxel;
}
ends[runCount - 1] = offset + 1;
}
return new BoundaryColumnGeometry(minimumY, palette,
Arrays.copyOf(ends, runCount), Arrays.copyOf(values, runCount));
}
public int minimumY() {
return minimumY;
}
public int height() {
return runEnds.length == 0 ? 0 : runEnds[runEnds.length - 1];
}
public List<Voxel> palette() {
return palette;
}
public int[] runEnds() {
return runEnds.clone();
}
public short[] paletteIndices() {
return paletteIndices.clone();
}
public Voxel voxelAt(int worldY) {
long offset = (long) worldY - minimumY;
if (offset < 0 || offset >= height()) {
return AIR;
}
return palette.get(paletteIndices[runAt((int) offset)]);
}
public boolean isEnclosedOpenAt(int worldY) {
long offset = (long) worldY - minimumY;
if (offset < 0 || offset >= height() || voxelAt(worldY).phase() == Phase.SOLID) {
return false;
}
return hasSolidAbove(worldY);
}
public boolean hasSolidAbove(int worldY) {
long offset = (long) worldY - minimumY;
if (offset >= height() - 1L) {
return false;
}
int startRun = offset < 0 ? 0 : runAt((int) offset);
for (int run = startRun; run < runEnds.length; run++) {
if (runEnds[run] <= offset + 1L) {
continue;
}
if (palette.get(paletteIndices[run]).phase() == Phase.SOLID) {
return true;
}
}
return false;
}
public List<Voxel> voxels() {
ArrayList<Voxel> values = new ArrayList<>(height());
int start = 0;
for (int run = 0; run < runEnds.length; run++) {
Voxel value = palette.get(paletteIndices[run]);
for (int offset = start; offset < runEnds[run]; offset++) {
values.add(value);
}
start = runEnds[run];
}
return List.copyOf(values);
}
public int surfaceOffsetNear(double expectedOffset) {
if (!Double.isFinite(expectedOffset) || height() == 0) {
throw new IllegalArgumentException("A surface reference requires finite height and nonempty geometry");
}
int nearest = -1;
double nearestDistance = Double.POSITIVE_INFINITY;
for (int offset = 0; offset + 1 < height(); offset++) {
Voxel current = voxelAt(minimumY + offset);
Voxel above = voxelAt(minimumY + offset + 1);
if (current.phase() != Phase.SOLID || current.protectedContent()
|| above.phase() == Phase.SOLID && !above.protectedContent()) {
continue;
}
double distance = Math.abs(offset - expectedOffset);
if (distance < nearestDistance) {
nearest = offset;
nearestDistance = distance;
}
}
if (nearest >= 0) {
return nearest;
}
for (int offset = height() - 1; offset >= 0; offset--) {
Voxel current = voxelAt(minimumY + offset);
if (current.phase() == Phase.SOLID && !current.protectedContent()) {
return offset;
}
}
return 0;
}
public double[] solidDistances() {
return phaseDistances(Phase.SOLID);
}
public double[] fluidDistances() {
return phaseDistances(Phase.FLUID);
}
private double[] phaseDistances(Phase phase) {
int height = height();
double[] distances = new double[height];
boolean[] solid = new boolean[height];
int start = 0;
for (int run = 0; run < runEnds.length; run++) {
Voxel value = palette.get(paletteIndices[run]);
boolean occupied = value.phase() == phase && !value.protectedContent();
Arrays.fill(solid, start, runEnds[run], occupied);
start = runEnds[run];
}
double distance = height + 0.5D;
for (int offset = 0; offset < height; offset++) {
distance = offset > 0 && solid[offset] != solid[offset - 1] ? 0.5D : distance + 1D;
distances[offset] = distance;
}
distance = height + 0.5D;
for (int offset = height - 1; offset >= 0; offset--) {
distance = offset + 1 < height && solid[offset] != solid[offset + 1] ? 0.5D : distance + 1D;
distances[offset] = Math.min(distances[offset], distance) * (solid[offset] ? 1D : -1D);
}
return distances;
}
@Override
public boolean equals(Object compared) {
return this == compared || compared instanceof BoundaryColumnGeometry other
&& minimumY == other.minimumY && palette.equals(other.palette)
&& Arrays.equals(runEnds, other.runEnds) && Arrays.equals(paletteIndices, other.paletteIndices);
}
@Override
public int hashCode() {
return Objects.hash(minimumY, palette, Arrays.hashCode(runEnds), Arrays.hashCode(paletteIndices));
}
private int runAt(int offset) {
int index = Arrays.binarySearch(runEnds, offset + 1);
return index >= 0 ? index : -index - 1;
}
private void validate() {
if (palette.size() > Short.MAX_VALUE + 1 || runEnds.length > MAXIMUM_HEIGHT
|| runEnds.length != paletteIndices.length) {
throw new IllegalArgumentException("Invalid compact geometry sizes");
}
int previousEnd = 0;
for (int run = 0; run < runEnds.length; run++) {
if (runEnds[run] <= previousEnd || runEnds[run] > MAXIMUM_HEIGHT
|| paletteIndices[run] < 0 || paletteIndices[run] >= palette.size()) {
throw new IllegalArgumentException("Invalid compact geometry run");
}
previousEnd = runEnds[run];
}
if (height() > 0) {
Math.toIntExact((long) minimumY + height() - 1L);
}
}
public enum Phase {
AIR,
SOLID,
FLUID
}
public record Voxel(String stateKey, Phase phase, String fluidStateKey, boolean protectedContent) {
public Voxel {
Objects.requireNonNull(stateKey, "Block state key");
Objects.requireNonNull(phase, "Voxel phase");
Objects.requireNonNull(fluidStateKey, "Fluid state key");
if (stateKey.isBlank()) {
throw new IllegalArgumentException("Block state key cannot be blank");
}
if (phase == Phase.FLUID && fluidStateKey.isBlank()) {
throw new IllegalArgumentException("Fluid voxels require a fluid state key");
}
}
}
}
@@ -0,0 +1,41 @@
package art.arcane.iris.engine.history;
import java.util.List;
import java.util.Objects;
public record BoundaryGeometryInfluence(double newTerrainWeight, double openingWeight, List<Contribution> contributions) {
private static final BoundaryGeometryInfluence NONE = new BoundaryGeometryInfluence(1D, 1D, List.of());
public BoundaryGeometryInfluence {
if (!Double.isFinite(newTerrainWeight) || newTerrainWeight < 0D || newTerrainWeight > 1D) {
throw new IllegalArgumentException("Geometry blend weight must be between zero and one");
}
if (!Double.isFinite(openingWeight) || openingWeight < 0D || openingWeight > 1D) {
throw new IllegalArgumentException("Opening blend weight must be between zero and one");
}
contributions = List.copyOf(contributions);
double total = 0D;
for (Contribution contribution : contributions) {
total += contribution.weight();
}
if (!contributions.isEmpty() && Math.abs(total - 1D) > 1E-9D) {
throw new IllegalArgumentException("Geometry contribution weights must sum to one");
}
}
public static BoundaryGeometryInfluence none() {
return NONE;
}
public record Contribution(BoundaryColumnGeometry geometry, double weight) {
public Contribution {
Objects.requireNonNull(geometry, "Historical geometry");
if (geometry.height() == 0) {
throw new IllegalArgumentException("Historical boundary geometry is missing");
}
if (!Double.isFinite(weight) || weight <= 0D || weight > 1D) {
throw new IllegalArgumentException("Geometry contribution weight must be positive and at most one");
}
}
}
}
@@ -8,6 +8,7 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Set;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -184,6 +185,34 @@ public final class ChunkGenerationOwnership {
}
}
public int discardUnstoredClaims(WorldChunkInventory stored, Set<Long> activationIds) throws IOException {
Objects.requireNonNull(stored, "stored chunks");
Set<Long> selected = Set.copyOf(activationIds);
for (long activation : selected) {
requireActivation(activation);
}
lock.writeLock().lock();
try {
int removed = 0;
for (long regionKey : orderedRegionKeys()) {
RegionGenerationOwnership region = loadExistingRegion(regionX(regionKey), regionZ(regionKey));
if (region == null) {
continue;
}
int count = region.discardUnstoredClaims(stored, selected);
if (count > 0) {
region.persist(directory);
explicitChunkCount -= count;
regionAssignmentCounts.put(regionKey, region.assignmentCount());
removed += count;
}
}
return removed;
} finally {
lock.writeLock().unlock();
}
}
public int persist() throws IOException {
lock.writeLock().lock();
try {
@@ -6,11 +6,9 @@ import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
public final class ChunkGenerationSemantics {
static final int MAX_KEY_BYTES = 1_024;
@@ -18,12 +16,15 @@ public final class ChunkGenerationSemantics {
static final int MAX_RIVER_FEATURES = 16_384;
static final int MAX_STRUCTURES = 4_096;
private static final Pattern RESOURCE_KEY_PATTERN = Pattern.compile("[a-z0-9_][a-z0-9._:/#@-]*");
private static final Comparator<StructureOccurrence> STRUCTURE_COMPARATOR = Comparator
.comparing(StructureOccurrence::key)
.thenComparingInt(occurrence -> occurrence.position().x())
.thenComparingInt(occurrence -> occurrence.position().z())
.thenComparingInt(occurrence -> occurrence.position().y());
private static final Comparator<PointOfInterest> POI_COMPARATOR = Comparator.comparing(PointOfInterest::key)
.thenComparingInt(point -> point.position().x())
.thenComparingInt(point -> point.position().z())
.thenComparingInt(point -> point.position().y());
private static final Comparator<RiverFeatureOccurrence> RIVER_FEATURE_COMPARATOR = Comparator
.comparing(RiverFeatureOccurrence::profileKey)
.thenComparing(RiverFeatureOccurrence::type)
@@ -43,6 +44,7 @@ public final class ChunkGenerationSemantics {
private final Set<String> objectKeys;
private final Set<RiverFeatureOccurrence> riverFeatures;
private final Set<StructureOccurrence> structures;
private final Set<PointOfInterest> pointsOfInterest;
private ChunkGenerationSemantics(Builder builder) {
chunkX = builder.chunkX;
@@ -56,6 +58,7 @@ public final class ChunkGenerationSemantics {
objectKeys = immutableKeys(builder.objectKeys, "object");
riverFeatures = immutableRiverFeatures(builder.riverFeatures);
structures = immutableStructures(builder.structures);
pointsOfInterest = immutablePoints(builder.pointsOfInterest);
}
public static Builder builder(int chunkX, int chunkZ, long activationId) {
@@ -102,6 +105,10 @@ public final class ChunkGenerationSemantics {
return riverFeatures;
}
public Set<PointOfInterest> pointsOfInterest() {
return pointsOfInterest;
}
public Set<StructureOccurrence> structures() {
return structures;
}
@@ -129,7 +136,8 @@ public final class ChunkGenerationSemantics {
|| !riverProfileKeys.containsAll(requiredUpdate.riverProfileKeys)
|| !objectKeys.containsAll(requiredUpdate.objectKeys)
|| !riverFeatures.containsAll(requiredUpdate.riverFeatures)
|| !structures.containsAll(requiredUpdate.structures);
|| !structures.containsAll(requiredUpdate.structures)
|| !pointsOfInterest.containsAll(requiredUpdate.pointsOfInterest);
if (sealed && addsFacts) {
throw new IllegalStateException(
"Chunk " + chunkX + "," + chunkZ + " has sealed generation semantics"
@@ -153,7 +161,9 @@ public final class ChunkGenerationSemantics {
.addRiverFeatures(riverFeatures)
.addRiverFeatures(requiredUpdate.riverFeatures)
.addStructures(structures)
.addStructures(requiredUpdate.structures);
.addStructures(requiredUpdate.structures)
.addPointsOfInterest(pointsOfInterest)
.addPointsOfInterest(requiredUpdate.pointsOfInterest);
if (sealed || requiredUpdate.sealed) {
merged.seal();
}
@@ -178,7 +188,8 @@ public final class ChunkGenerationSemantics {
&& riverProfileKeys.equals(semantics.riverProfileKeys)
&& objectKeys.equals(semantics.objectKeys)
&& riverFeatures.equals(semantics.riverFeatures)
&& structures.equals(semantics.structures);
&& structures.equals(semantics.structures)
&& pointsOfInterest.equals(semantics.pointsOfInterest);
}
@Override
@@ -194,7 +205,8 @@ public final class ChunkGenerationSemantics {
riverProfileKeys,
objectKeys,
riverFeatures,
structures
structures,
pointsOfInterest
);
}
@@ -230,11 +242,11 @@ public final class ChunkGenerationSemantics {
if (!requiredKey.equals(requiredKey.trim())) {
throw new IllegalArgumentException("Generation semantic resource keys must not contain surrounding whitespace: " + requiredKey);
}
if (!requiredKey.equals(requiredKey.toLowerCase(Locale.ROOT))) {
throw new IllegalArgumentException("Generation semantic resource keys must be lowercase: " + requiredKey);
for (int index = 0; index < requiredKey.length(); index++) {
char character = requiredKey.charAt(index);
if (character == '\\' || Character.isISOControl(character)) {
throw new IllegalArgumentException("Generation semantic resource key contains an invalid character");
}
if (!RESOURCE_KEY_PATTERN.matcher(requiredKey).matches()) {
throw new IllegalArgumentException("Generation semantic resource key is not normalized: " + requiredKey);
}
if (requiredKey.getBytes(StandardCharsets.UTF_8).length > MAX_KEY_BYTES) {
throw new IllegalArgumentException("Generation semantic resource key is too long");
@@ -283,6 +295,28 @@ public final class ChunkGenerationSemantics {
return Collections.unmodifiableSet(validated);
}
private static Set<PointOfInterest> immutablePoints(Collection<PointOfInterest> points) {
if (points.size() > MAX_STRUCTURES) {
throw new IllegalArgumentException("A chunk contains too many points of interest");
}
TreeSet<PointOfInterest> copy = new TreeSet<>(POI_COMPARATOR);
for (PointOfInterest point : points) {
copy.add(Objects.requireNonNull(point, "point of interest"));
}
return Collections.unmodifiableSet(copy);
}
static Comparator<PointOfInterest> pointComparator() {
return POI_COMPARATOR;
}
public record PointOfInterest(String key, BlockPosition position) {
public PointOfInterest {
key = requireResourceKey(key);
position = Objects.requireNonNull(position, "position");
}
}
public record BlockPosition(int x, int y, int z) {
}
@@ -317,6 +351,7 @@ public final class ChunkGenerationSemantics {
private final Set<String> objectKeys;
private final Set<RiverFeatureOccurrence> riverFeatures;
private final Set<StructureOccurrence> structures;
private final Set<PointOfInterest> pointsOfInterest;
private boolean sealed;
private Builder(int chunkX, int chunkZ, long activationId) {
@@ -333,6 +368,7 @@ public final class ChunkGenerationSemantics {
objectKeys = new TreeSet<>();
riverFeatures = new TreeSet<>(RIVER_FEATURE_COMPARATOR);
structures = new TreeSet<>(STRUCTURE_COMPARATOR);
pointsOfInterest = new TreeSet<>(POI_COMPARATOR);
}
public Builder addSurfaceBiome(String key) {
@@ -450,6 +486,22 @@ public final class ChunkGenerationSemantics {
return this;
}
public Builder addPointOfInterest(PointOfInterest point) {
Objects.requireNonNull(point, "point of interest");
if (pointsOfInterest.size() >= MAX_STRUCTURES && !pointsOfInterest.contains(point)) {
throw new IllegalArgumentException("A chunk contains too many points of interest");
}
pointsOfInterest.add(point);
return this;
}
public Builder addPointsOfInterest(Collection<PointOfInterest> points) {
for (PointOfInterest point : points) {
addPointOfInterest(point);
}
return this;
}
public Builder seal() {
sealed = true;
return this;
@@ -0,0 +1,44 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public final class DiskBoundaryCapture implements TerrainBoundarySignatureStore.SignatureSampler {
private static final int MAXIMUM_CACHED_CHUNKS = 32;
private final Path dimensionRoot;
private final int minimumY;
private final int height;
private final LinkedHashMap<Long, SavedTerrainChunk> chunks = new LinkedHashMap<>(32, 0.75F, true);
public DiskBoundaryCapture(Path dimensionRoot, int minimumY, int height) {
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimension root");
this.minimumY = minimumY;
this.height = height;
}
@Override
public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException {
int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16);
long key = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
SavedTerrainChunk chunk = chunks.get(key);
if (chunk == null) {
chunk = SavedTerrainChunk.read(dimensionRoot, chunkX, chunkZ, minimumY, height);
chunks.put(key, chunk);
if (chunks.size() > MAXIMUM_CACHED_CHUNKS) {
Map.Entry<Long, SavedTerrainChunk> oldest = chunks.firstEntry();
chunks.remove(oldest.getKey());
}
}
return chunk.column(blockX, blockZ);
}
@Override
public void close() {
chunks.clear();
}
}
@@ -58,6 +58,23 @@ public final class GenerationBlend {
return (int) Math.round(historicalHeight + ((double) newHeight - historicalHeight) * newEpochWeight);
}
public static boolean usesHistoricalMaterial(int blockX, int blockY, int blockZ, double newWeight) {
validateWeight(newWeight);
if (newWeight == 0D) {
return true;
}
if (newWeight == 1D) {
return false;
}
long hash = Math.floorDiv(blockX, 8) * 341873128712L
^ Math.floorDiv(blockY, 8) * 132897987541L
^ Math.floorDiv(blockZ, 8) * 42317861L;
hash = (hash ^ (hash >>> 30)) * 0xbf58476d1ce4e5b9L;
hash = (hash ^ (hash >>> 27)) * 0x94d049bb133111ebL;
hash ^= hash >>> 31;
return newWeight < (hash >>> 11) * 0x1.0p-53;
}
private static void validateWeight(double weight) {
if (!Double.isFinite(weight) || weight < 0D || weight > 1D) {
throw new IllegalArgumentException("Blend weight must be finite and between zero and one");
@@ -7,4 +7,8 @@ import java.io.IOException;
@FunctionalInterface
public interface GenerationBoundarySignatureSampler {
TerrainBoundarySignature sample(IrisEngine engine, int blockX, int blockZ) throws IOException;
default TerrainBoundarySignatureStore.SignatureSampler open(IrisEngine engine) throws IOException {
return (blockX, blockZ) -> sample(engine, blockX, blockZ);
}
}
@@ -632,13 +632,16 @@ public final class GenerationBoundaryStore {
}
@Override
public synchronized boolean contains(int chunkX, int chunkZ) throws IOException {
public boolean contains(int chunkX, int chunkZ) throws IOException {
long regionKey = packRegion(chunkX >> 5, chunkZ >> 5);
String hash = shardHashes.get(regionKey);
if (hash == null) {
return false;
}
byte[] mask = load(regionKey, hash);
byte[] mask;
synchronized (this) {
mask = load(regionKey, hash);
}
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
| Math.floorMod(chunkX, REGION_SIDE);
return (mask[localIndex >>> 3] & (1 << (localIndex & 7))) != 0;
@@ -11,31 +11,31 @@ import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
final class GenerationKernelSourceSeal {
final class GenerationBuildRevision {
private static final int MAXIMUM_BYTES = 8 * 1024 * 1024;
private static final Pattern SHA256 = Pattern.compile("[0-9a-f]{64}");
private GenerationKernelSourceSeal() {
private GenerationBuildRevision() {
}
static String requireFingerprint(int abi, Class<?> factory) {
Descriptor descriptor = load(abi);
if (!descriptor.factoryClass().equals(factory.getName())) {
throw new IllegalStateException("Generation ABI " + abi + " is sealed for " + descriptor.factoryClass() + ".");
throw new IllegalStateException("Generation ABI " + abi + " declares factory " + descriptor.factoryClass() + ".");
}
return descriptor.fingerprint();
}
static Descriptor load(int abi) {
String resource = "/META-INF/iris/generation-kernels/abi-" + abi + ".seal";
try (InputStream input = GenerationKernelSourceSeal.class.getResourceAsStream(resource)) {
String resource = "/META-INF/iris/generation-kernels/abi-" + abi + ".revision";
try (InputStream input = GenerationBuildRevision.class.getResourceAsStream(resource)) {
if (input == null) {
throw new IOException("Missing generation kernel source seal " + resource + ".");
throw new IOException("Missing generation kernel build revision " + resource + ".");
}
byte[] bytes = input.readNBytes(MAXIMUM_BYTES + 1);
String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\n", -1);
if (lines.length < 4 || !lines[2].startsWith("factory\t")) {
throw new IOException("Generation kernel seal has no factory identity.");
throw new IOException("Generation build revision has no factory identity.");
}
String factoryClass = lines[2].substring("factory\t".length());
if (!factoryClass.matches("[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)+")) {
@@ -52,7 +52,7 @@ final class GenerationKernelSourceSeal {
}
return new Descriptor(abi, factoryClass, List.copyOf(algorithms), fingerprint);
} catch (IOException failure) {
throw new IllegalStateException("Cannot load sealed Iris generation ABI " + abi + ".", failure);
throw new IllegalStateException("Cannot load Iris generation build revision for ABI " + abi + ".", failure);
}
}
@@ -60,13 +60,13 @@ final class GenerationKernelSourceSeal {
Objects.requireNonNull(factoryClass, "factoryClass");
Objects.requireNonNull(bytes, "bytes");
if (abi < 1 || bytes.length == 0 || bytes.length > MAXIMUM_BYTES) {
throw new IOException("Invalid generation kernel seal size or ABI.");
throw new IOException("Invalid generation build revision size or ABI.");
}
String source = new String(bytes, StandardCharsets.UTF_8);
String prefix = "iris-generation-kernel-seal-v1\nabi\t" + abi + "\nfactory\t" + factoryClass + "\n";
String prefix = "iris-generation-build-revision-v1\nabi\t" + abi + "\nfactory\t" + factoryClass + "\n";
if (!source.startsWith(prefix) || !source.endsWith("\n") || source.indexOf('\r') >= 0
|| !source.contains("\nsource\t") || !source.contains("\ndependency\t")) {
throw new IOException("Generation kernel seal does not match ABI " + abi + " and factory " + factoryClass + ".");
throw new IOException("Generation build revision does not match ABI " + abi + " and factory " + factoryClass + ".");
}
requireCanonicalEntries(source.substring(prefix.length()));
try {
@@ -79,7 +79,7 @@ final class GenerationKernelSourceSeal {
private static void requireCanonicalEntries(String source) throws IOException {
String[] entries = source.split("\n", -1);
if (entries.length > 30_000) {
throw new IOException("Generation kernel seal has too many entries.");
throw new IOException("Generation build revision has too many entries.");
}
int previousType = -1;
String previousKey = "";
@@ -92,7 +92,7 @@ final class GenerationKernelSourceSeal {
if (fields[0].equals("algorithm")) {
if (previousType != -1 || fields.length != 3 || !fields[1].matches("[1-9][0-9]{0,8}")
|| !fields[2].matches("[1-9][0-9]{0,8}")) {
throw new IOException("Generation kernel seal contains an invalid algorithm version.");
throw new IOException("Generation build revision contains an invalid algorithm version.");
}
int rng = Integer.parseInt(fields[1]);
int seed = Integer.parseInt(fields[2]);
@@ -116,14 +116,14 @@ final class GenerationKernelSourceSeal {
|| fields[1].chars().anyMatch(Character::isWhitespace)
|| (type == previousType && fields[1].compareTo(previousKey) <= 0)
|| (type >= 2 && !SHA256.matcher(fields[2]).matches())) {
throw new IOException("Generation kernel seal contains an invalid or noncanonical entry.");
throw new IOException("Generation build revision contains an invalid or noncanonical entry.");
}
previousType = type;
previousKey = fields[1];
hasScope |= type == 0;
}
if (!hasScope || !hasAlgorithm) {
throw new IOException("Generation kernel seal has no source scope or algorithm version.");
throw new IOException("Generation build revision has no source scope or algorithm version.");
}
}
@@ -346,15 +346,10 @@ public final class GenerationEpoch {
return dimensionKey.equals(other.dimensionKey)
&& dimensionTypeKey.equals(other.dimensionTypeKey)
&& environment.equals(other.environment)
&& generationMode.equals(other.generationMode)
&& internalFluidHeight == other.internalFluidHeight
&& minHeight == other.minHeight
&& height == other.height
&& logicalHeight == other.logicalHeight
&& Double.compare(coordinateScale, other.coordinateScale) == 0
&& upperTerrainEnabled == other.upperTerrainEnabled
&& upperDimensionKey.equals(other.upperDimensionKey)
&& upperDimensionGap == other.upperDimensionGap
&& dimensionTypeFingerprintSchema == other.dimensionTypeFingerprintSchema
&& dimensionTypeFingerprint.equals(other.dimensionTypeFingerprint);
}
@@ -1,23 +1,16 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.volmlib.util.collection.KList;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
@@ -55,7 +48,7 @@ public final class GenerationFindCatalog {
}
public static boolean hasObjectPlacement(Engine engine, String key) {
return engine.hasObjectPlacement(key) || retained(engine).objects.contains(normalizeObjectKey(key));
return engine.hasObjectPlacement(key) || containsKey(retained(engine).objects, normalizeObjectKey(key));
}
public static KList<String> retainedStructureKeys(Engine engine) {
@@ -63,7 +56,7 @@ public final class GenerationFindCatalog {
}
public static boolean hasRetainedStructurePlacement(Engine engine, String key) {
return retained(engine).structures.contains(normalizeKey(key));
return containsKey(retained(engine).structures, normalizeKey(key));
}
private static RetainedCatalog retained(Engine engine) {
@@ -90,87 +83,48 @@ public final class GenerationFindCatalog {
private static RetainedCatalog loadRetained(GenerationHistory history, GenerationManifest manifest) {
RetainedCatalog catalog = new RetainedCatalog(manifest.activeActivation().activationId());
Set<String> loadedPacks = new HashSet<>();
Long activationId = manifest.activeActivation().parentActivationId();
while (activationId != null) {
GenerationActivation activation = manifest.activation(activationId).orElseThrow();
GenerationEpoch epoch = manifest.epoch(activation.epochId()).orElseThrow();
String dimensionKey = epoch.dimensionContract().dimensionKey();
if (loadedPacks.add(epoch.packFingerprint() + ":" + dimensionKey)) {
try {
loadPack(catalog, history.packRoot(activationId), dimensionKey);
history.forEachRecordedSemantic(semantics -> addRecorded(catalog, semantics));
} catch (IOException error) {
throw new UncheckedIOException("Unable to read retained Iris find catalog for activation "
+ activationId, error);
}
}
activationId = activation.parentActivationId();
throw new UncheckedIOException("Unable to read recorded Iris find catalog", error);
}
return catalog;
}
private static void loadPack(RetainedCatalog catalog, Path packRoot, String dimensionKey) {
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IllegalStateException("Retained Iris dimension is absent: " + dimensionKey);
private static void addRecorded(RetainedCatalog catalog, ChunkGenerationSemantics semantics) {
if (!semantics.sealed() || semantics.activationId() == catalog.activeActivationId) {
return;
}
addStructures(catalog.structures, dimension.getStructures());
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
catalog.biomes.putIfAbsent(normalizeKey(biome.getLoadKey()), biome);
addObjects(catalog.objects, biome.getObjects());
addStructures(catalog.structures, biome.getStructures());
for (String key : semantics.surfaceBiomeKeys()) {
addBiome(catalog, key);
}
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
catalog.regions.putIfAbsent(normalizeKey(region.getLoadKey()), region);
addObjects(catalog.objects, region.getObjects());
addStructures(catalog.structures, region.getStructures());
for (String key : semantics.caveBiomeKeys()) {
addBiome(catalog, key);
}
if (dimension.hasUpperDimension() && !dimensionKey.equals(dimension.getUpperDimension())) {
IrisDimension upper = data.getDimensionLoader().load(dimension.getUpperDimension(), false);
if (upper == null) {
throw new IllegalStateException("Retained Iris upper dimension is absent: "
+ dimension.getUpperDimension());
}
for (IrisBiome biome : upper.getReachableBiomes(() -> data)) {
addObjects(catalog.objects, biome.getSurfaceObjects());
}
for (IrisRegion region : upper.getAllRegions(() -> data)) {
addObjects(catalog.objects, region.getSurfaceObjects());
}
}
} finally {
data.close();
}
}
private static void addStructures(Set<String> structures, Iterable<IrisStructurePlacement> placements) {
for (IrisStructurePlacement placement : placements) {
if (placement == null) {
continue;
}
for (String key : placement.getStructures()) {
for (String key : semantics.regionKeys()) {
String normalized = normalizeKey(key);
if (!normalized.isEmpty()) {
structures.add(normalized);
if (!catalog.regions.containsKey(normalized)) {
IrisRegion region = new IrisRegion().setName(key);
region.setLoadKey(key);
catalog.regions.put(normalized, region);
}
}
for (String key : semantics.objectKeys()) {
catalog.objects.add(normalizeObjectKey(key));
}
for (ChunkGenerationSemantics.StructureOccurrence occurrence : semantics.structures()) {
catalog.structures.add(normalizeKey(occurrence.key()));
}
}
private static void addObjects(Set<String> objects, Iterable<IrisObjectPlacement> placements) {
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
}
for (String key : placement.getPlace()) {
String normalized = normalizeObjectKey(key);
if (!normalized.isEmpty()) {
objects.add(normalized);
}
}
private static void addBiome(RetainedCatalog catalog, String key) {
String normalized = normalizeKey(key);
if (catalog.biomes.containsKey(normalized)) {
return;
}
IrisBiome biome = new IrisBiome().setName(key);
biome.setLoadKey(key);
catalog.biomes.put(normalized, biome);
}
private static <T extends IrisRegistrant> KList<T> merge(Iterable<T> active, Map<String, T> historical) {
@@ -184,16 +138,33 @@ public final class GenerationFindCatalog {
private static <T extends IrisRegistrant> T find(Iterable<T> entries, String key) {
String normalized = normalizeKey(key);
T caseInsensitiveMatch = null;
for (T entry : entries) {
if (normalizeKey(entry.getLoadKey()).equals(normalized)) {
String entryKey = normalizeKey(entry.getLoadKey());
if (entryKey.equals(normalized)) {
return entry;
}
if (caseInsensitiveMatch == null && entryKey.equalsIgnoreCase(normalized)) {
caseInsensitiveMatch = entry;
}
return null;
}
return caseInsensitiveMatch;
}
private static boolean containsKey(Set<String> keys, String key) {
if (keys.contains(key)) {
return true;
}
for (String candidate : keys) {
if (candidate.equalsIgnoreCase(key)) {
return true;
}
}
return false;
}
private static String normalizeKey(String key) {
return key == null ? "" : key.trim().toLowerCase(Locale.ROOT);
return key == null ? "" : key.trim();
}
private static String normalizeObjectKey(String key) {
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
public final class GenerationHistory {
@@ -380,9 +381,44 @@ public final class GenerationHistory {
}
}
LiveCutover beginLiveCutover() {
return new LiveCutover(admission.beginCutover());
}
final class LiveCutover implements AutoCloseable {
private final GenerationAdmission.CutoverLease lease;
private final Thread owner = Thread.currentThread();
private boolean closed;
private LiveCutover(GenerationAdmission.CutoverLease lease) {
this.lease = lease;
}
GenerationActivation promote(BoundarySignatureCapture signatureCapture) throws IOException {
requireOpen();
synchronized (GenerationHistory.this) {
return promotePendingLocked(Objects.requireNonNull(signatureCapture, "signature capture"));
}
}
private void requireOpen() {
if (closed || Thread.currentThread() != owner) {
throw new IllegalStateException("Generation cutover is closed or belongs to another thread.");
}
}
@Override
public void close() {
requireOpen();
closed = true;
lease.close();
}
}
private GenerationActivation promotePendingLocked(
BoundarySignatureCapture signatureCapture
) throws IOException {
WorldChunkInventory inventory = recoverUnstoredClaims();
validateReferencedState();
Optional<GenerationActivation> pending = store.pendingActivation();
if (pending.isEmpty()) {
@@ -390,16 +426,9 @@ public final class GenerationHistory {
}
long outgoingActivationId = store.activeActivation().activationId();
semantics.compactJournals();
WorldChunkInventory inventory = WorldChunkInventory.scan(paths.dimensionRoot());
ownership.assignUnassigned(inventory, outgoingActivationId);
semantics.forEachSealedClaim(
outgoingActivationId,
(chunkX, chunkZ) -> ownership.assign(chunkX, chunkZ, outgoingActivationId)
);
ownership.persist();
requireExplicitOwnership(inventory);
requireExplicitSemanticOwnership(outgoingActivationId);
GenerationBoundary boundary = boundaries.publishOwnership(
pending.get().activationId(),
ownership
@@ -440,6 +469,13 @@ public final class GenerationHistory {
return kernels.current();
}
public synchronized boolean usesCurrentGenerator() throws IOException {
GenerationEpoch active = store.activeEpoch();
return active.kernelVersion().equals(kernels.current())
&& active.kernelImplementationFingerprint().equals(
kernels.requireSupported(kernels.current()).implementationFingerprint());
}
public synchronized GenerationActivation stageCurrentKernel(
int transitionWidthBlocks
) throws IOException {
@@ -455,6 +491,49 @@ public final class GenerationHistory {
);
}
public void prepareCurrentGenerator(int transitionWidthBlocks) throws IOException {
try (GenerationAdmission.CutoverLease ignored = admission.beginStartupCutover()) {
synchronized (this) {
if (store.pendingActivation().isEmpty() && usesCurrentGenerator()) {
recoverUnstoredClaims();
validateReferencedState();
} else {
if (store.pendingActivation().isEmpty()) {
stageCurrentKernel(transitionWidthBlocks);
}
promoteSavedBoundary();
if (!usesCurrentGenerator()) {
stageCurrentKernel(transitionWidthBlocks);
promoteSavedBoundary();
}
}
packs.releaseArchivedPacks(store.manifest());
}
}
}
private void promoteSavedBoundary() throws IOException {
GenerationEpoch.DimensionContract contract = activeEpoch().dimensionContract();
promotePendingLocked(boundary -> new DiskBoundaryCapture(paths.dimensionRoot(), contract.minHeight(), contract.height()));
}
private WorldChunkInventory recoverUnstoredClaims() throws IOException {
long outgoing = store.activeActivation().activationId();
Set<Long> selected = store.pendingActivation()
.map(pending -> Set.of(outgoing, pending.activationId()))
.orElseGet(() -> Set.of(outgoing));
WorldChunkInventory inventory = WorldChunkInventory.scan(paths.dimensionRoot()).filter((chunkX, chunkZ) -> {
if (ownership.isExplicitlyAssigned(chunkX, chunkZ)
&& !selected.contains(ownership.resolve(chunkX, chunkZ, outgoing))) {
return true;
}
return SavedTerrainChunk.hasTerrain(SavedTerrainChunk.readStatus(paths.dimensionRoot(), chunkX, chunkZ));
});
semantics.discardUnstoredClaims(inventory, selected);
ownership.discardUnstoredClaims(inventory, selected);
return inventory;
}
public synchronized Optional<GenerationActivation> pendingActivation() {
return store.pendingActivation();
}
@@ -482,7 +561,7 @@ public final class GenerationHistory {
GenerationAdmission.StageLease lease = admission.enterStage();
try {
synchronized (this) {
GenerationActivation activation = resolveActivation(chunkX, chunkZ);
GenerationActivation activation = store.activeActivation();
GenerationEpoch epoch = requireEpoch(activation.epochId());
return new GenerationStage(
this,
@@ -526,6 +605,10 @@ public final class GenerationHistory {
return semantics.claimAndPersist(requiredUpdate);
}
public synchronized void forEachRecordedSemantic(GenerationSemanticIndex.RecordConsumer consumer) throws IOException {
semantics.forEachRecord(consumer);
}
public synchronized Optional<ChunkGenerationSemantics> semantics(int chunkX, int chunkZ) {
return semantics.get(chunkX, chunkZ);
}
@@ -652,13 +735,11 @@ public final class GenerationHistory {
private void validateReferencedState() throws IOException {
validateRuntimeVersions();
Collection<GenerationEpoch> epochs = store.manifest().epochs();
for (GenerationEpoch epoch : epochs) {
packs.requireExactPack(
epoch.epochId(),
epoch.packFingerprint(),
epoch.packFingerprintVersion()
);
GenerationEpoch active = store.activeEpoch();
packs.requireExactPack(active.epochId(), active.packFingerprint(), active.packFingerprintVersion());
if (store.pendingEpoch().isPresent()) {
GenerationEpoch pending = store.pendingEpoch().orElseThrow();
packs.requireExactPack(pending.epochId(), pending.packFingerprint(), pending.packFingerprintVersion());
}
for (GenerationActivation activation : store.manifest().activations()) {
requireSafeDirectoryIfPresent(paths.activationRoot(activation.activationId()));
@@ -698,14 +779,6 @@ public final class GenerationHistory {
private void validateRuntimeVersions() throws IOException {
for (GenerationEpoch epoch : store.manifest().epochs()) {
kernels.requireSupported(
new GenerationKernelRegistry.Version(
epoch.generatorAbi(),
epoch.rngVersion(),
epoch.seedDerivationVersion()
),
epoch.kernelImplementationFingerprint()
);
GenerationPackFingerprint.requireSupported(epoch.packFingerprintVersion());
}
}
@@ -716,7 +789,6 @@ public final class GenerationHistory {
GenerationBoundary expectedBoundary = active.isInitial() ? null : boundary(activeActivationId);
boolean pendingCutover = store.pendingActivation().isPresent();
int[] assignedCount = new int[1];
WorldChunkInventory[] inventory = new WorldChunkInventory[1];
ownership.forEachAssignment((chunkX, chunkZ, activationId) -> {
assignedCount[0] = Math.addExact(assignedCount[0], 1);
if (store.activation(activationId).isEmpty()) {
@@ -739,14 +811,6 @@ public final class GenerationHistory {
+ chunkX + "," + chunkZ + ": expected activation "
+ activeActivationId + " but found " + activationId + ".");
}
if (inventory[0] == null) {
inventory[0] = WorldChunkInventory.scan(paths.dimensionRoot());
}
if (!inventory[0].contains(chunkX, chunkZ)
&& !semantics.hasSealedClaim(chunkX, chunkZ, activationId)) {
throw new IOException("Pending generation cutover claims an ungenerated chunk "
+ chunkX + "," + chunkZ + " without matching sealed semantics.");
}
});
int expectedCount = expectedBoundary == null ? 0 : expectedBoundary.historicalChunkCount();
@@ -803,24 +867,6 @@ public final class GenerationHistory {
+ " was not durably assigned before generation activation promotion.");
}
private void requireExplicitSemanticOwnership(long activationId) throws IOException {
boolean[] missing = new boolean[1];
long[] firstMissing = new long[1];
semantics.forEachSealedClaim(activationId, (chunkX, chunkZ) -> {
if (!missing[0] && !ownership.isExplicitlyAssigned(chunkX, chunkZ)) {
missing[0] = true;
firstMissing[0] = ChunkGenerationOwnership.packChunk(chunkX, chunkZ);
}
});
if (!missing[0]) {
return;
}
long first = firstMissing[0];
throw new IOException("Generated chunk " + ChunkGenerationOwnership.chunkX(first)
+ "," + ChunkGenerationOwnership.chunkZ(first)
+ " was not durably assigned before generation activation promotion.");
}
private GenerationActivation requireActivation(long activationId) {
return store.activation(activationId).orElseThrow(() -> new IllegalStateException(
"Generation ownership references missing activation " + activationId + "."
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.GenerationTransitionGate;
import art.arcane.iris.engine.object.IrisDimension;
import java.io.IOException;
@@ -19,7 +20,6 @@ import java.util.concurrent.locks.ReentrantLock;
public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
public static final int DEFAULT_TRANSITION_WIDTH_BLOCKS = 256;
public static final int DEFAULT_RUNTIME_CACHE_CAPACITY = 4;
private final IrisEngine engine;
private final GenerationHistory history;
@@ -27,7 +27,6 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final ActivationRuntimeFactory runtimeFactory;
private final LinkedHashMap<Long, RuntimeCacheEntry> bindings;
private final Map<Long, RuntimeRetirement> retiringBindings;
private final int runtimeCacheCapacity;
private final ReentrantLock stateLock;
private final Condition inactive;
private final ThreadLocal<Integer> operationDepth;
@@ -43,18 +42,13 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
IrisEngine engine,
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
ActivationRuntimeFactory runtimeFactory,
int runtimeCacheCapacity
ActivationRuntimeFactory runtimeFactory
) throws IOException {
this.engine = Objects.requireNonNull(engine, "Iris engine");
this.history = Objects.requireNonNull(history, "generation history");
this.signatureSampler = Objects.requireNonNull(signatureSampler, "boundary signature sampler");
this.runtimeFactory = Objects.requireNonNull(runtimeFactory, "activation runtime factory");
if (runtimeCacheCapacity < 1) {
throw new IllegalArgumentException("Generation runtime cache capacity must be positive.");
}
this.runtimeCacheCapacity = runtimeCacheCapacity;
this.bindings = new LinkedHashMap<>(runtimeCacheCapacity, 0.75F, true);
this.bindings = new LinkedHashMap<>();
this.retiringBindings = new LinkedHashMap<>();
this.stateLock = new ReentrantLock(true);
this.inactive = stateLock.newCondition();
@@ -65,7 +59,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
GenerationEpoch epoch = requireEpoch(active);
IrisEngine.GenerationRuntimeBinding base = engine.getActiveGenerationRuntimeBinding();
runtimeFactory.validateBase(engine, history, active, epoch, base);
if (history.usesCurrentGenerator()) {
requireBindingContract(active, epoch, base);
}
RuntimeCacheEntry baseEntry = new RuntimeCacheEntry(active.activationId());
baseEntry.binding = base;
baseEntry.defaultRuntime = true;
@@ -117,8 +113,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
history,
signatureSampler,
transitionWidthBlocks,
new DefaultActivationRuntimeFactory(),
DEFAULT_RUNTIME_CACHE_CAPACITY
new DefaultActivationRuntimeFactory()
);
}
@@ -128,31 +123,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
GenerationBoundarySignatureSampler signatureSampler,
int transitionWidthBlocks,
ActivationRuntimeFactory runtimeFactory
) throws IOException {
return attachAndPromotePending(
engine,
history,
signatureSampler,
transitionWidthBlocks,
runtimeFactory,
DEFAULT_RUNTIME_CACHE_CAPACITY
);
}
static GenerationHistoryRuntimeRouter attachAndPromotePending(
IrisEngine engine,
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
int transitionWidthBlocks,
ActivationRuntimeFactory runtimeFactory,
int runtimeCacheCapacity
) throws IOException {
GenerationHistoryRuntimeRouter router = attach(
engine,
history,
signatureSampler,
runtimeFactory,
runtimeCacheCapacity
runtimeFactory
);
try {
router.promotePendingAndReconcileCurrentKernel(transitionWidthBlocks);
@@ -174,29 +150,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
ActivationRuntimeFactory runtimeFactory
) throws IOException {
return attach(
engine,
history,
signatureSampler,
runtimeFactory,
DEFAULT_RUNTIME_CACHE_CAPACITY
);
}
static GenerationHistoryRuntimeRouter attach(
IrisEngine engine,
GenerationHistory history,
GenerationBoundarySignatureSampler signatureSampler,
ActivationRuntimeFactory runtimeFactory,
int runtimeCacheCapacity
) throws IOException {
return new GenerationHistoryRuntimeRouter(
engine,
history,
signatureSampler,
runtimeFactory,
runtimeCacheCapacity
runtimeFactory
);
}
@@ -208,24 +167,94 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return history;
}
public StudioCutover beginStudioCutover(long timeoutMillis) {
if (!engine.isStudio()) {
throw new IllegalStateException("Live generation cutovers require a Studio world.");
}
if (scopedRoute.get() != null || operationDepth.get() > 0) {
throw new IllegalStateException("Cannot begin a Studio cutover inside another generation operation.");
}
enterOperation();
GenerationTransitionGate.Transition transition = null;
try {
transition = engine.getGenerationSessions().transitionGate().beginTransition(timeoutMillis);
return new StudioCutover(history.beginLiveCutover(), transition);
} catch (Throwable failure) {
if (transition != null) {
transition.close();
}
leaveOperation();
throw failure;
}
}
public final class StudioCutover implements AutoCloseable {
private final GenerationHistory.LiveCutover cutover;
private final GenerationTransitionGate.Transition transition;
private final Thread owner = Thread.currentThread();
private boolean closed;
private StudioCutover(GenerationHistory.LiveCutover cutover, GenerationTransitionGate.Transition transition) {
this.cutover = cutover;
this.transition = transition;
}
public GenerationActivation promotePending() throws IOException {
requireOpen();
GenerationActivation activated = cutover.promote(GenerationHistoryRuntimeRouter.this::captureBoundarySignatures);
try (RuntimeLease lease = acquireRuntime(activated, requireEpoch(activated))) {
engine.setDefaultGenerationRuntime(lease.binding());
retireBindings(pinDefault(lease.entry));
}
return activated;
}
private void requireOpen() {
if (closed || Thread.currentThread() != owner) {
throw new IllegalStateException("Studio cutover is closed or belongs to another thread.");
}
}
@Override
public void close() {
requireOpen();
closed = true;
try {
cutover.close();
} finally {
try {
transition.close();
} finally {
leaveOperation();
}
}
}
}
public Optional<RuntimeOwnership> currentRuntimeOwnership() {
RuntimeRoute current = scopedRoute.get();
return current == null ? Optional.empty() : Optional.of(current.ownership);
}
public RuntimeRoute openRoute(int chunkX, int chunkZ) throws IOException {
enterRouteOperation();
GenerationTransitionGate.Participation participation = engine.getGenerationSessions().transitionGate().enter();
boolean operation = false;
try {
enterRouteOperation();
operation = true;
GenerationHistory.GenerationStage stage = history.openStage(chunkX, chunkZ);
try {
RuntimeLease lease = acquireRuntime(stage.activation(), stage.epoch());
return new RuntimeRoute(this, stage, lease);
return new RuntimeRoute(this, stage, lease, participation);
} catch (Throwable failure) {
stage.close();
throw failure;
}
} catch (Throwable failure) {
if (operation) {
leaveRouteOperation();
}
participation.close();
throw propagate(failure, "Unable to open an Iris generation-history stage.");
}
}
@@ -259,6 +288,15 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return new CoordinateScope(blockX, blockZ, stage.route, stage);
}
public void recordNaturalTerrain(SavedTerrainChunk terrain) {
SavedTerrainChunk captured = Objects.requireNonNull(terrain, "natural terrain");
RuntimeRoute route = scopedRoute.get();
if (route == null || route.chunkX() != captured.chunkX() || route.chunkZ() != captured.chunkZ()) {
throw new IllegalStateException("Natural terrain must be captured inside its chunk generation route.");
}
route.naturalTerrain = captured;
}
public void preloadActiveRuntimes() throws IOException {
enterOperation();
try {
@@ -274,6 +312,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
enterOperation();
try {
GenerationActivation activated = history.promotePending(this::captureBoundarySignatures);
if (!history.usesCurrentGenerator()) {
return activated;
}
GenerationEpoch epoch = requireEpoch(activated);
try (RuntimeLease lease = acquireRuntime(activated, epoch)) {
engine.setDefaultGenerationRuntime(lease.binding());
@@ -288,8 +329,11 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private GenerationActivation promotePendingAndReconcileCurrentKernel(
int transitionWidthBlocks
) throws IOException {
if (history.pendingActivation().isEmpty() && !history.usesCurrentGenerator()) {
history.stageCurrentKernel(transitionWidthBlocks);
}
GenerationActivation activated = promotePending();
while (!history.activeEpoch().kernelVersion().equals(history.currentKernelVersion())) {
if (!history.usesCurrentGenerator()) {
history.stageCurrentKernel(transitionWidthBlocks);
activated = promotePending();
}
@@ -364,8 +408,8 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private TerrainBoundarySignatureStore.SignatureSampler captureBoundarySignatures(
GenerationBoundary boundary
) {
return new BoundaryCapture();
) throws IOException {
return signatureSampler.open(engine);
}
private RuntimeLease acquireRuntime(
@@ -522,7 +566,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private List<RuntimeRetirement> collectEvictionsLocked() {
ArrayList<RuntimeRetirement> retired = new ArrayList<>();
while (bindings.size() > runtimeCacheCapacity) {
while (bindings.size() > 1) {
RuntimeCacheEntry candidate = null;
Iterator<Map.Entry<Long, RuntimeCacheEntry>> iterator = bindings.entrySet().iterator();
while (iterator.hasNext()) {
@@ -576,6 +620,18 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
if (failure != null) {
throw new IllegalStateException("Failed to retire cached generation runtimes.", failure);
}
if (!retired.isEmpty()) {
stateLock.lock();
try {
if (bindings.size() == 1 && retiringBindings.isEmpty()) {
new GenerationPackRepository(history.paths().dimensionRoot()).releaseArchivedPacks(history.manifest());
}
} catch (IOException cleanupFailure) {
throw new IllegalStateException("Unable to release archived generation packs after runtime retirement.", cleanupFailure);
} finally {
stateLock.unlock();
}
}
}
private void requireBindingContract(
@@ -807,49 +863,6 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
}
}
private final class BoundaryCapture implements TerrainBoundarySignatureStore.SignatureSampler {
private RuntimeLease lease;
private IrisEngine.GenerationRuntimeScope scope;
private long activationId;
@Override
public TerrainBoundarySignature sample(int blockX, int blockZ) throws IOException {
GenerationActivation activation = history.resolveActivation(
Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE),
Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE)
);
if (lease == null || activationId != activation.activationId()) {
close();
lease = acquireRuntime(activation, requireEpoch(activation));
try {
scope = engine.openGenerationRuntimeScope(lease.binding());
activationId = activation.activationId();
} catch (Throwable failure) {
close();
throw propagate(failure, "Unable to scope boundary capture.");
}
}
return signatureSampler.sample(engine, blockX, blockZ);
}
@Override
public void close() {
try {
if (scope != null) {
IrisEngine.GenerationRuntimeScope closing = scope;
scope = null;
closing.close();
}
} finally {
if (lease != null) {
RuntimeLease closing = lease;
lease = null;
closing.close();
}
}
}
}
interface ActivationRuntimeFactory {
void validateBase(
IrisEngine engine,
@@ -960,17 +973,21 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final RuntimeLease lease;
private final IrisEngine.GenerationRuntimeBinding binding;
private final RuntimeOwnership ownership;
private final GenerationTransitionGate.Participation participation;
private int activeScopes;
private volatile SavedTerrainChunk naturalTerrain;
private boolean closed;
private RuntimeRoute(
GenerationHistoryRuntimeRouter router,
GenerationHistory.GenerationStage stage,
RuntimeLease lease
RuntimeLease lease,
GenerationTransitionGate.Participation participation
) {
this.router = router;
this.stage = stage;
this.lease = lease;
this.participation = participation;
this.binding = lease.binding();
this.ownership = new RuntimeOwnership(stage.activation().activationId(), binding);
}
@@ -979,6 +996,10 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return stage;
}
public Optional<SavedTerrainChunk> naturalTerrain() {
return Optional.ofNullable(naturalTerrain);
}
public int chunkX() {
return stage.chunkX();
}
@@ -999,15 +1020,29 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return binding.transitionPlan();
}
public void detachThread() {
participation.detachThread();
}
public boolean claimGeneratedSemantics() throws IOException {
return claimGeneratedSemantics((x, y, z) -> true);
}
public boolean claimGeneratedSemantics(GenerationSemanticCapture.CaveSpace caveSpace) throws IOException {
if (router.scopedRoute.get() != this) {
throw new IllegalStateException(
"Generation semantics must be captured inside this route's runtime scope."
);
}
Optional<ChunkGenerationSemantics> recorded = router.history.semantics(chunkX(), chunkZ());
if (recorded.isPresent() && recorded.get().sealed()
|| router.history.resolveActivation(chunkX(), chunkZ()).activationId() != stage.activation().activationId()) {
return false;
}
ChunkGenerationSemantics semantics = GenerationSemanticCapture.capture(
router.engine,
stage
stage,
caveSpace
);
return router.history.claimGeneratedSemantics(stage, semantics);
}
@@ -1021,10 +1056,12 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
}
router.enterRuntimeScope();
RuntimeRoute previousRoute = router.installScopedRoute(this);
GenerationTransitionGate.Participation scopedParticipation = participation.attach();
try {
IrisEngine.GenerationRuntimeScope opened = router.engine.openGenerationRuntimeScope(binding);
return new RuntimeScope(this, opened, Thread.currentThread(), previousRoute);
return new RuntimeScope(this, opened, Thread.currentThread(), previousRoute, scopedParticipation);
} catch (Throwable failure) {
scopedParticipation.close();
try {
router.restoreScopedRoute(this, previousRoute);
} catch (Throwable closeFailure) {
@@ -1077,6 +1114,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
failure.addSuppressed(closeFailure);
}
}
participation.close();
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
@@ -1100,18 +1138,21 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final IrisEngine.GenerationRuntimeScope runtimeScope;
private final Thread owner;
private final RuntimeRoute previousRoute;
private final GenerationTransitionGate.Participation participation;
private boolean closed;
private RuntimeScope(
RuntimeRoute route,
IrisEngine.GenerationRuntimeScope runtimeScope,
Thread owner,
RuntimeRoute previousRoute
RuntimeRoute previousRoute,
GenerationTransitionGate.Participation participation
) {
this.route = route;
this.runtimeScope = runtimeScope;
this.owner = owner;
this.previousRoute = previousRoute;
this.participation = participation;
}
@Override
@@ -1148,6 +1189,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
} catch (Throwable closeFailure) {
failure = appendFailure(failure, closeFailure);
}
participation.close();
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
@@ -3,6 +3,8 @@ package art.arcane.iris.engine.history;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.Strictness;
@@ -27,6 +29,8 @@ import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Clock;
import java.util.Objects;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public final class GenerationHistoryStore {
@@ -97,7 +101,7 @@ public final class GenerationHistoryStore {
requiredClock.millis()
);
DirectorySync requiredDirectorySync = Objects.requireNonNull(directorySync, "directorySync");
writeManifestAtomically(directory, manifestPath, manifest, false, requiredDirectorySync);
writeManifestAtomically(directory, manifestPath, manifest, null, false, requiredDirectorySync);
return new GenerationHistoryStore(directory, manifest, requiredClock, requiredDirectorySync);
}
}
@@ -207,7 +211,7 @@ public final class GenerationHistoryStore {
private void writeUpdatedManifest(GenerationManifest updated) throws IOException {
try {
writeManifestAtomically(generationDirectory, manifestPath, updated, true, directorySync);
writeManifestAtomically(generationDirectory, manifestPath, updated, manifest, true, directorySync);
} catch (IOException error) {
reconcileAfterWriteFailure(error);
throw error;
@@ -233,7 +237,16 @@ public final class GenerationHistoryStore {
if (reader.peek() != JsonToken.END_DOCUMENT) {
throw new IllegalArgumentException("Generation manifest contains trailing content.");
}
return GenerationManifest.fromJson(parsed.getAsJsonObject());
JsonObject index = parsed.getAsJsonObject();
JsonArray references = GenerationManifest.JsonSchema.requireArray(index, "epochs", "generation manifest");
List<GenerationEpoch> epochs = new ArrayList<>(references.size());
for (JsonElement reference : references) {
if (!reference.isJsonPrimitive() || !reference.getAsJsonPrimitive().isString()) {
throw new IOException("Generation epoch reference must be a digest.");
}
epochs.add(readEpoch(manifestPath.getParent(), reference.getAsString()));
}
return GenerationManifest.fromJson(index, epochs);
} catch (JsonParseException | IllegalArgumentException | ArithmeticException exception) {
throw new IOException("Invalid generation manifest: " + manifestPath, exception);
}
@@ -243,12 +256,21 @@ public final class GenerationHistoryStore {
Path generationDirectory,
Path manifestPath,
GenerationManifest manifest,
GenerationManifest previous,
boolean replaceExisting,
DirectorySync directorySync
) throws IOException {
requireSafeParent(generationDirectory);
requireDirectory(generationDirectory);
byte[] content = GSON.toJson(manifest.toJson()).getBytes(StandardCharsets.UTF_8);
if (content.length > MAX_MANIFEST_BYTES) {
throw new IOException("Generation manifest exceeds the supported size: " + manifestPath);
}
for (GenerationEpoch epoch : manifest.epochs()) {
if (previous == null || previous.epoch(epoch.epochId()).isEmpty()) {
publishEpoch(generationDirectory, epoch);
}
}
Path temporary = Files.createTempFile(generationDirectory, ".manifest-", ".tmp");
try {
try (FileChannel channel = FileChannel.open(
@@ -282,6 +304,74 @@ public final class GenerationHistoryStore {
}
}
private static GenerationEpoch readEpoch(Path generationDirectory, String epochId) throws IOException {
Path epochDirectory = epochDirectory(generationDirectory, epochId);
requireDirectory(generationDirectory.resolve("epochs"));
requireDirectory(epochDirectory);
Path metadata = epochDirectory.resolve("epoch.json");
requireRegularFile(metadata);
if (Files.size(metadata) > MAX_MANIFEST_BYTES) {
throw new IOException("Generation epoch metadata exceeds the supported size: " + metadata);
}
try (JsonReader reader = new JsonReader(Files.newBufferedReader(metadata, StandardCharsets.UTF_8))) {
reader.setStrictness(Strictness.STRICT);
JsonElement parsed = JsonParser.parseReader(reader);
if (!parsed.isJsonObject() || reader.peek() != JsonToken.END_DOCUMENT) {
throw new IOException("Invalid generation epoch metadata: " + metadata);
}
GenerationEpoch epoch = GenerationEpoch.fromJson(parsed.getAsJsonObject());
if (!epoch.epochId().equals(epochId)) {
throw new IOException("Generation epoch metadata identity differs from its reference: " + metadata);
}
return epoch;
} catch (JsonParseException | IllegalArgumentException exception) {
throw new IOException("Invalid generation epoch metadata: " + metadata, exception);
}
}
private static void publishEpoch(Path generationDirectory, GenerationEpoch epoch) throws IOException {
Path epochsDirectory = generationDirectory.resolve("epochs");
ensureDirectory(epochsDirectory);
Path epochDirectory = epochDirectory(generationDirectory, epoch.epochId());
ensureDirectory(epochDirectory);
Path metadata = epochDirectory.resolve("epoch.json");
if (Files.exists(metadata, LinkOption.NOFOLLOW_LINKS)) {
if (!readEpoch(generationDirectory, epoch.epochId()).equals(epoch)) {
throw new IOException("Generation epoch metadata is immutable: " + metadata);
}
return;
}
byte[] content = GSON.toJson(epoch.toJson()).getBytes(StandardCharsets.UTF_8);
if (content.length > MAX_MANIFEST_BYTES) {
throw new IOException("Generation epoch metadata exceeds the supported size: " + metadata);
}
Path temporary = Files.createTempFile(epochDirectory, ".epoch-", ".tmp");
try {
try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap(content);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
try {
Files.move(temporary, metadata, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException exception) {
throw new IOException("Generation history requires atomic epoch publication.", exception);
}
forceDirectory(epochDirectory);
} finally {
Files.deleteIfExists(temporary);
}
}
private static Path epochDirectory(Path generationDirectory, String epochId) throws IOException {
if (!epochId.matches("[0-9a-f]{64}")) {
throw new IOException("Generation epoch reference must be a digest.");
}
return generationDirectory.resolve("epochs").resolve(epochId);
}
private static Path normalizeDirectory(Path directory) {
return Objects.requireNonNull(directory, "generationDirectory").toAbsolutePath().normalize();
}
@@ -23,19 +23,19 @@ final class GenerationKernelCatalog {
Catalog catalog = parse(input.readNBytes(MAXIMUM_BYTES + 1));
List<GenerationKernelRegistry.Kernel> kernels = new ArrayList<>();
for (int abi : catalog.abis()) {
GenerationKernelSourceSeal.Descriptor seal = GenerationKernelSourceSeal.load(abi);
GenerationBuildRevision.Descriptor revision = GenerationBuildRevision.load(abi);
Class<? extends GenerationKernelRegistry.RuntimeFactory> factoryType = Class.forName(
seal.factoryClass(), true, GenerationKernelCatalog.class.getClassLoader())
revision.factoryClass(), true, GenerationKernelCatalog.class.getClassLoader())
.asSubclass(GenerationKernelRegistry.RuntimeFactory.class);
Map<GenerationKernelRegistry.AlgorithmVersion, GenerationKernelRegistry.RuntimeFactory> factories = new LinkedHashMap<>();
for (GenerationKernelRegistry.AlgorithmVersion algorithm : seal.algorithms()) {
for (GenerationKernelRegistry.AlgorithmVersion algorithm : revision.algorithms()) {
factories.put(algorithm, factoryType.getDeclaredConstructor().newInstance());
}
kernels.add(new GenerationKernelRegistry.Kernel(abi, seal.fingerprint(), factories));
kernels.add(new GenerationKernelRegistry.Kernel(abi, revision.fingerprint(), factories));
}
return new GenerationKernelRegistry(catalog.current(), kernels);
} catch (IOException | ReflectiveOperationException | IllegalArgumentException failure) {
throw new IllegalStateException("Cannot load the sealed Iris generation kernel catalog.", failure);
throw new IllegalStateException("Cannot load the Iris generation kernel catalog.", failure);
}
}
@@ -20,7 +20,7 @@ import java.nio.file.Path;
final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactory {
static final String IMPLEMENTATION_FINGERPRINT =
GenerationKernelSourceSeal.requireFingerprint(1, GenerationKernelV1.class);
GenerationBuildRevision.requireFingerprint(1, GenerationKernelV1.class);
@Override
public SeedManager createSeedManager(long worldSeed) {
@@ -125,7 +125,7 @@ final class GenerationKernelV1 implements GenerationKernelRegistry.RuntimeFactor
return;
}
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
mode.registerStage((x, z, blocks, biomes, multicore, chunkContext) ->
mode.registerTerrainStage((x, z, blocks, biomes, multicore, chunkContext) ->
dimensionStack.actuate(x, z, blocks, multicore, chunkContext));
IrisCustomModifier stackCustom = new IrisCustomModifier(engine);
mode.registerStage((x, z, blocks, biomes, multicore, chunkContext) ->
@@ -20,7 +20,7 @@ import java.util.Set;
import java.util.TreeMap;
public final class GenerationManifest {
public static final int CURRENT_SCHEMA_VERSION = 1;
public static final int CURRENT_SCHEMA_VERSION = 2;
private final int schemaVersion;
private final NavigableMap<String, GenerationEpoch> epochs;
@@ -189,7 +189,7 @@ public final class GenerationManifest {
JsonArray epochArray = new JsonArray(epochs.size());
for (GenerationEpoch epoch : epochs.values()) {
epochArray.add(epoch.toJson());
epochArray.add(epoch.epochId());
}
json.add("epochs", epochArray);
@@ -207,7 +207,7 @@ public final class GenerationManifest {
return json;
}
static GenerationManifest fromJson(JsonObject json) {
static GenerationManifest fromJson(JsonObject json, List<GenerationEpoch> epochRecords) {
JsonSchema.requireFields(
json,
"generation manifest",
@@ -231,10 +231,17 @@ public final class GenerationManifest {
"generation manifest"
);
if (epochArray.size() != epochRecords.size()) {
throw new IllegalArgumentException("Generation manifest epoch references do not match metadata.");
}
TreeMap<String, GenerationEpoch> epochs = new TreeMap<>();
for (int index = 0; index < epochArray.size(); index++) {
JsonObject epochJson = JsonSchema.requireObject(epochArray.get(index), "epochs[" + index + "]");
GenerationEpoch epoch = GenerationEpoch.fromJson(epochJson);
GenerationEpoch epoch = epochRecords.get(index);
if (!epochArray.get(index).isJsonPrimitive()
|| !epochArray.get(index).getAsJsonPrimitive().isString()
|| !epoch.epochId().equals(epochArray.get(index).getAsString())) {
throw new IllegalArgumentException("Generation manifest epoch reference does not match metadata.");
}
if (epochs.putIfAbsent(epoch.epochId(), epoch) != null) {
throw new IllegalArgumentException("Generation manifest contains duplicate epoch IDs.");
}
@@ -70,35 +70,55 @@ public final class GenerationPackRepository {
int packFingerprintVersion,
Path source
) throws IOException {
Path target = epochsRoot.resolve(epochId);
Path epochDirectory = ensureChildDirectory(epochsRoot, epochId);
Path target = epochDirectory.resolve("pack");
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
requireSafeDirectory(target, "Generation epoch path is not a safe directory");
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
}
Path stage = Files.createTempDirectory(epochsRoot, ".epoch-" + epochId.substring(0, 12) + "-");
boolean published = false;
Path stage = Files.createTempDirectory(epochDirectory, ".pack-");
try {
Path stagedPack = stage.resolve("pack");
copyPackTree(source, stagedPack);
requireFingerprint(stagedPack, packFingerprint, packFingerprintVersion);
Files.delete(stage);
copyPackTree(source, stage);
requireFingerprint(stage, packFingerprint, packFingerprintVersion);
try (AtomicDirectoryPublisher.Publication publication =
AtomicDirectoryPublisher.publishAbsent(stage, target)) {
publication.commit();
} catch (FileAlreadyExistsException race) {
requireSafeDirectory(target, "Generation epoch path is not a safe directory");
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
}
forceDirectory(epochsRoot);
published = true;
forceDirectory(epochDirectory);
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
} finally {
if (!published && Files.exists(stage, LinkOption.NOFOLLOW_LINKS)) {
if (Files.exists(stage, LinkOption.NOFOLLOW_LINKS)) {
AtomicDirectoryPublisher.deleteTree(stage);
}
}
}
public synchronized void releaseArchivedPacks(GenerationManifest manifest) throws IOException {
GenerationManifest retained = Objects.requireNonNull(manifest, "manifest");
String activeEpochId = retained.activeEpoch().epochId();
String pendingEpochId = retained.pendingEpoch().map(GenerationEpoch::epochId).orElse(null);
for (GenerationEpoch epoch : retained.epochs()) {
String epochId = epoch.epochId();
if (epochId.equals(activeEpochId) || epochId.equals(pendingEpochId)) {
continue;
}
try (GenerationPublicationLock ignored = GenerationPublicationLock.acquire(
epochsRoot, ".epoch-" + epochId + ".lock")) {
validateExistingAncestors(epochId);
Path pack = packRoot(epochId);
if (!Files.exists(pack, LinkOption.NOFOLLOW_LINKS)) {
continue;
}
requireSafeDirectory(pack, "Generation epoch pack is unsafe");
AtomicDirectoryPublisher.deleteTree(pack);
forceDirectory(epochRoot(epochId));
}
}
}
public Path requireExactPack(
String epochId,
String packFingerprint,
@@ -182,7 +202,7 @@ public final class GenerationPackRepository {
}
}
private static void copyPackTree(Path source, Path target) throws IOException {
public static void copyPackTree(Path source, Path target) throws IOException {
Path sourcePath = Objects.requireNonNull(source, "source").toAbsolutePath().normalize();
if (Files.isSymbolicLink(sourcePath)) {
throw new IOException("Pack source is a symbolic link: " + sourcePath);

Some files were not shown because too many files have changed in this diff Show More