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