mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Merged terrain generation updates
- Existing worlds retain generated terrain across generator updates - New terrain blends at generation boundaries - Cold generation and Studio startup improvements are integrated - Terrain sampling validates active generator bounds and noise
This commit is contained in:
commit
d10d70a894
250 files changed
+16953
-4448
No files matched your search
+14
@@ -17,6 +17,7 @@ import art.arcane.iris.engine.object.IrisBiome;
|
|||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||||
|
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import art.arcane.iris.util.project.context.IrisContext;
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import art.arcane.volmlib.util.math.RNG;
|
import art.arcane.volmlib.util.math.RNG;
|
||||||
@@ -64,6 +65,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
private final long seed;
|
private final long seed;
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
|
private final BukkitChunkGenerator platformGenerator;
|
||||||
private final Registry<Biome> biomeCustomRegistry;
|
private final Registry<Biome> biomeCustomRegistry;
|
||||||
private final Registry<Biome> biomeRegistry;
|
private final Registry<Biome> biomeRegistry;
|
||||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||||
@@ -76,6 +78,9 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
|
this.platformGenerator = world.getGenerator() instanceof BukkitChunkGenerator generator
|
||||||
|
? generator
|
||||||
|
: null;
|
||||||
this.seed = seed;
|
this.seed = seed;
|
||||||
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
||||||
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
||||||
@@ -353,6 +358,9 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (!isRuntimeAvailable()) {
|
if (!isRuntimeAvailable()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||||
|
return fallbackBiome;
|
||||||
|
}
|
||||||
runtimeBiomeState();
|
runtimeBiomeState();
|
||||||
int quartX = QuartPos.fromBlock(blockX);
|
int quartX = QuartPos.fromBlock(blockX);
|
||||||
int quartZ = QuartPos.fromBlock(blockZ);
|
int quartZ = QuartPos.fromBlock(blockZ);
|
||||||
@@ -405,6 +413,9 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(int x, int y, int z) {
|
private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(int x, int y, int z) {
|
||||||
|
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||||
|
return fallbackBiome;
|
||||||
|
}
|
||||||
if (isGuaranteedSurfaceBiome(y)) {
|
if (isGuaranteedSurfaceBiome(y)) {
|
||||||
return getSurfaceStructureBiomeHolder(x, z);
|
return getSurfaceStructureBiomeHolder(x, z);
|
||||||
}
|
}
|
||||||
@@ -731,6 +742,9 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
IrisDimensionCarvingResolver.State resolverState,
|
IrisDimensionCarvingResolver.State resolverState,
|
||||||
boolean cacheable
|
boolean cacheable
|
||||||
) {
|
) {
|
||||||
|
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||||
|
return fallbackBiome;
|
||||||
|
}
|
||||||
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
|
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
|
||||||
engine.getCacheID(), packNoiseKey(x, y, z));
|
engine.getCacheID(), packNoiseKey(x, y, z));
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
|
|||||||
+133
-18
@@ -1,5 +1,10 @@
|
|||||||
package art.arcane.iris.core.nms.v26_2_R1;
|
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.DimensionStackContext;
|
||||||
import art.arcane.iris.engine.DimensionStackLayout;
|
import art.arcane.iris.engine.DimensionStackLayout;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
@@ -12,7 +17,12 @@ import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
|||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.IrisEngine;
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
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.platform.BukkitChunkGenerator;
|
||||||
|
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||||
@@ -112,7 +122,6 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -122,7 +131,14 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import java.util.function.IntBinaryOperator;
|
import java.util.function.IntBinaryOperator;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
public class IrisChunkGenerator extends CustomChunkGenerator implements LongPredicate {
|
||||||
|
private static final Set<Heightmap.Types> AUTHORING_HEIGHTMAPS = Set.of(
|
||||||
|
Heightmap.Types.WORLD_SURFACE_WG,
|
||||||
|
Heightmap.Types.OCEAN_FLOOR_WG,
|
||||||
|
Heightmap.Types.WORLD_SURFACE,
|
||||||
|
Heightmap.Types.OCEAN_FLOOR,
|
||||||
|
Heightmap.Types.MOTION_BLOCKING,
|
||||||
|
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||||
private static final Runnable NO_OP = () -> {
|
private static final Runnable NO_OP = () -> {
|
||||||
@@ -413,6 +429,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
));
|
));
|
||||||
adjustGeneratedStructures(
|
adjustGeneratedStructures(
|
||||||
registryAccess, access, previousStarts, configuredStarts, templateManager);
|
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 +737,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface");
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
if (!engine.getComplex().allowsMantleChunkWrite(chunkPos.x(), chunkPos.z())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -729,6 +753,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers");
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
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);
|
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -766,6 +793,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope =
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope =
|
||||||
openHistoryRuntimeScope(route);
|
openHistoryRuntimeScope(route);
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
claimGeneratedSemantics(route, filled, engine.getMinHeight());
|
||||||
|
persistNaturalTerrain(filled, route);
|
||||||
primeWorldgenHeightmaps(filled);
|
primeWorldgenHeightmaps(filled);
|
||||||
return filled;
|
return filled;
|
||||||
}
|
}
|
||||||
@@ -783,6 +812,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
completion.completeExceptionally(completionFailure);
|
completion.completeExceptionally(completionFailure);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
lease.detachThread();
|
||||||
|
if (route != null) {
|
||||||
|
route.detachThread();
|
||||||
|
}
|
||||||
return completion;
|
return completion;
|
||||||
} catch (RuntimeException | Error failure) {
|
} catch (RuntimeException | Error failure) {
|
||||||
lease.close();
|
lease.close();
|
||||||
@@ -792,6 +825,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(
|
private static Throwable closeNoisePipelineResources(
|
||||||
Throwable failure,
|
Throwable failure,
|
||||||
GenerationSessionLease lease,
|
GenerationSessionLease lease,
|
||||||
@@ -854,9 +900,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void primeWorldgenHeightmaps(ChunkAccess chunkAccess) {
|
private void primeWorldgenHeightmaps(ChunkAccess chunkAccess) {
|
||||||
|
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||||
|
primeAuthoringHeightmaps(chunkAccess);
|
||||||
|
return;
|
||||||
|
}
|
||||||
WorldgenTerrainHeightmaps.primeTerrain(chunkAccess, worldgenSurfaceHeight(), worldgenFloorHeight());
|
WorldgenTerrainHeightmaps.primeTerrain(chunkAccess, worldgenSurfaceHeight(), worldgenFloorHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void primeAuthoringHeightmaps(ChunkAccess chunkAccess) {
|
||||||
|
Heightmap.primeHeightmaps(chunkAccess, AUTHORING_HEIGHTMAPS);
|
||||||
|
}
|
||||||
|
|
||||||
private IntBinaryOperator worldgenSurfaceHeight() {
|
private IntBinaryOperator worldgenSurfaceHeight() {
|
||||||
return (x, z) -> engine.getHeight(x, z, false) + runtimeMinY + 1;
|
return (x, z) -> engine.getHeight(x, z, false) + runtimeMinY + 1;
|
||||||
}
|
}
|
||||||
@@ -933,19 +987,27 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)) {
|
boolean flatStudioTerrain = platformGenerator != null && platformGenerator.usesFlatStudioTerrain();
|
||||||
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
|
if (!flatStudioTerrain && !ichunkaccess.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) {
|
||||||
// feature-order cycle is reported once and degraded to features-off.
|
|
||||||
importedFeatures.prepare(generatoraccessseed);
|
|
||||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
|
||||||
placeVanillaStructures(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
|
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)
|
||||||
// dimension set importedFeatures.enabled.
|
&& NativeGenerationWriteGuard.allowsDecoration(engine, generatoraccessseed, chunkPos)) {
|
||||||
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
|
if (!flatStudioTerrain) {
|
||||||
|
// 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);
|
||||||
|
if (!flatStudioTerrain) {
|
||||||
|
// 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);
|
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||||
}
|
}
|
||||||
claimGeneratedSemantics(route, "bukkit_nms_biome_decoration");
|
claimGeneratedSemantics(route, ichunkaccess, engine.getMinHeight());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -998,7 +1060,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||||
for (StructureStart start : starts) {
|
for (StructureStart start : starts) {
|
||||||
BoundingBox footprint = start.getBoundingBox();
|
BoundingBox footprint = start.getBoundingBox();
|
||||||
if (!engine.getComplex().allowsNewGenerationFootprint(
|
if (!isHistoricalStructureStart(world, start)
|
||||||
|
&& !engine.getComplex().allowsNewGenerationFootprint(
|
||||||
footprint.minX(),
|
footprint.minX(),
|
||||||
footprint.minZ(),
|
footprint.minZ(),
|
||||||
footprint.maxX(),
|
footprint.maxX(),
|
||||||
@@ -1098,6 +1161,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) {
|
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
|
||||||
if (placementGroups.isEmpty()) {
|
if (placementGroups.isEmpty()) {
|
||||||
return "<no resolved native structures>";
|
return "<no resolved native structures>";
|
||||||
@@ -1219,6 +1296,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
BlockPos blockPos = sectionPos.origin();
|
BlockPos blockPos = sectionPos.origin();
|
||||||
|
|
||||||
primeWorldgenHeightmaps(chunkAccess);
|
primeWorldgenHeightmaps(chunkAccess);
|
||||||
|
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
||||||
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||||
@@ -1272,12 +1352,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) {
|
private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) {
|
||||||
if (chunk.getPersistedStatus().isOrAfter(stage)) {
|
if (chunk.getPersistedStatus().isOrAfter(stage)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ChunkPos chunkPos = chunk.getPos();
|
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(
|
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
||||||
@@ -1310,10 +1397,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||||
|
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||||
|
return platformGenerator.getAuthoringBaseHeight();
|
||||||
|
}
|
||||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||||
i, j, "bukkit_nms_base_height");
|
i, j, "bukkit_nms_base_height");
|
||||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
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());
|
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||||
int height = engine.getDimensionStackContext() == null
|
int height = engine.getDimensionStackContext() == null
|
||||||
? engine.getHeight(i, j, ignoreFluid)
|
? engine.getHeight(i, j, ignoreFluid)
|
||||||
@@ -1327,10 +1421,27 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||||
|
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
|
||||||
|
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||||
|
int floorIndex = platformGenerator.getAuthoringFloorY() - levelheightaccessor.getMinY();
|
||||||
|
BlockState floor = platformGenerator.isJigsawStudioActive() && JigsawStudioGenerator.isLightFloor(i, j)
|
||||||
|
? Blocks.SMOOTH_STONE.defaultBlockState()
|
||||||
|
: Blocks.POLISHED_DEEPSLATE.defaultBlockState();
|
||||||
|
for (int index = 0; index < column.length; index++) {
|
||||||
|
column[index] = index == floorIndex
|
||||||
|
? floor
|
||||||
|
: Blocks.AIR.defaultBlockState();
|
||||||
|
}
|
||||||
|
return new NoiseColumn(levelheightaccessor.getMinY(), column);
|
||||||
|
}
|
||||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||||
i, j, "bukkit_nms_base_column");
|
i, j, "bukkit_nms_base_column");
|
||||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
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;
|
boolean dimensionStack = engine.getDimensionStackContext() != null;
|
||||||
int block = dimensionStack
|
int block = dimensionStack
|
||||||
? Engine.hostHeight(engine, i, j, true)
|
? Engine.hostHeight(engine, i, j, true)
|
||||||
@@ -1412,16 +1523,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
private static void claimGeneratedSemantics(
|
private static void claimGeneratedSemantics(
|
||||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||||
String operation
|
ChunkAccess chunk,
|
||||||
|
int minimumY
|
||||||
) {
|
) {
|
||||||
if (route == null) {
|
if (route == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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) {
|
} catch (IOException failure) {
|
||||||
throw new IllegalStateException("Iris " + operation
|
throw new IllegalStateException("Iris could not durably claim generated chunk semantics.", failure);
|
||||||
+ " could not durably claim generated chunk semantics.", failure);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-381
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.core.nms.v26_2_R1;
|
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.io.MoonriseRegionFileIO;
|
||||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
|
||||||
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
|
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.NativeStructureFactory;
|
||||||
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
import art.arcane.iris.nativegen.NativeStructureGenerationException;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
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.KList;
|
||||||
import art.arcane.volmlib.util.collection.KMap;
|
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.Hunk;
|
||||||
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
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.volmlib.util.nbt.tag.CompoundTag;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
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.BlockPos;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.core.IdMapper;
|
import net.minecraft.core.IdMapper;
|
||||||
@@ -95,7 +85,6 @@ import net.minecraft.tags.TagKey;
|
|||||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||||
import net.minecraft.world.entity.EntityType;
|
import net.minecraft.world.entity.EntityType;
|
||||||
import net.minecraft.world.item.component.CustomData;
|
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.BiomeSource;
|
||||||
import net.minecraft.world.level.biome.Biomes;
|
import net.minecraft.world.level.biome.Biomes;
|
||||||
import net.minecraft.world.level.block.Block;
|
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.ChunkGeneratorStructureState;
|
||||||
import net.minecraft.world.level.chunk.LevelChunk;
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
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.ChunkStatus;
|
||||||
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||||
import net.minecraft.world.level.dimension.LevelStem;
|
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.FallenTreeFeature;
|
||||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||||
import net.minecraft.world.level.levelgen.feature.TreeFeature;
|
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.FlatLayerInfo;
|
||||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
@@ -159,10 +146,8 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@@ -173,7 +158,6 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class NMSBinding implements INMSBinding {
|
public class NMSBinding implements INMSBinding {
|
||||||
@@ -181,11 +165,7 @@ public class NMSBinding implements INMSBinding {
|
|||||||
private volatile DataVersion dataVersion;
|
private volatile DataVersion dataVersion;
|
||||||
private final BlockData AIR = Material.AIR.createBlockData();
|
private final BlockData AIR = Material.AIR.createBlockData();
|
||||||
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
|
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
|
||||||
private final AtomicBoolean injected = new AtomicBoolean();
|
private final NmsWorldLifecycle worldLifecycle = new NmsWorldLifecycle();
|
||||||
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
|
||||||
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
|
||||||
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
|
|
||||||
private boolean pluginClassLoaderCloseDeferred;
|
|
||||||
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
||||||
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
||||||
private final AtomicCache<RegistryAccess> registryAccess = 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
|
@Override
|
||||||
public boolean saveAndUnloadChunk(World world, int x, int z) {
|
public boolean saveAndUnloadChunk(World world, int x, int z) {
|
||||||
try {
|
try {
|
||||||
@@ -1279,9 +1270,9 @@ public class NMSBinding implements INMSBinding {
|
|||||||
: null;
|
: null;
|
||||||
boolean studioBootstrap = platformGenerator != null
|
boolean studioBootstrap = platformGenerator != null
|
||||||
&& platformGenerator.isStudioEntryBootstrapActive();
|
&& platformGenerator.isStudioEntryBootstrapActive();
|
||||||
boolean jigsawStudio = platformGenerator != null
|
boolean authoringStudio = platformGenerator != null
|
||||||
&& platformGenerator.isJigsawStudioActive();
|
&& platformGenerator.isAuthoringStudio();
|
||||||
if (jigsawStudio) {
|
if (authoringStudio) {
|
||||||
requireIrisGenerator(generator);
|
requireIrisGenerator(generator);
|
||||||
if (currentState.possibleStructureSets().isEmpty()) {
|
if (currentState.possibleStructureSets().isEmpty()) {
|
||||||
currentState.ensureStructuresGenerated();
|
currentState.ensureStructuresGenerated();
|
||||||
@@ -1552,170 +1543,37 @@ public class NMSBinding implements INMSBinding {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean injectBukkit() {
|
public boolean injectBukkit() {
|
||||||
synchronized (injected) {
|
return worldLifecycle.injectBukkit();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void ensureServerLevelInjection() {
|
public void ensureServerLevelInjection() {
|
||||||
if (!injected.get())
|
worldLifecycle.ensureServerLevelInjection();
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void uninjectBukkit() {
|
public void uninjectBukkit() {
|
||||||
synchronized (injected) {
|
worldLifecycle.uninjectBukkit();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isServerStopping() {
|
public boolean isServerStopping() {
|
||||||
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
|
return worldLifecycle.isServerStopping();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ServerShutdownBoundary createServerShutdownBoundary() {
|
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
return worldLifecycle.createServerShutdownBoundary();
|
||||||
return new ServerShutdownBoundary(
|
|
||||||
() -> server.hasFullyShutdown,
|
|
||||||
server.getRunningThread()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deferPluginClassLoaderClose() {
|
public void deferPluginClassLoaderClose() {
|
||||||
synchronized (injected) {
|
worldLifecycle.deferPluginClassLoaderClose();
|
||||||
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Agent.retainClassLoader(getClass().getClassLoader());
|
|
||||||
pluginClassLoaderCloseDeferred = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releasePluginClassLoaderClose() {
|
public void releasePluginClassLoaderClose() {
|
||||||
ResettableClassFileTransformer transformer;
|
worldLifecycle.releasePluginClassLoaderClose();
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1761,220 +1619,4 @@ public class NMSBinding implements INMSBinding {
|
|||||||
settings.updateLayers();
|
settings.updateLayers();
|
||||||
return new FlatLevelSource(settings);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -24,6 +24,31 @@ public class CustomBiomeSourceStructureContractTest {
|
|||||||
Bootstrap.bootStrap();
|
Bootstrap.bootStrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authoringBiomesBypassTerrainResolutionBeforeDestinationChunksGenerate() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||||
|
int visibleStart = source.indexOf("private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(");
|
||||||
|
int visibleEnd = source.indexOf("private boolean isBiomeCacheable(", visibleStart);
|
||||||
|
String visible = source.substring(visibleStart, visibleEnd);
|
||||||
|
int constantBiome = visible.indexOf("platformGenerator.usesFlatStudioTerrain()");
|
||||||
|
int terrainResolution = visible.indexOf("resolveVisibleBiomeHolder(");
|
||||||
|
|
||||||
|
assertTrue(source.contains("platformGenerator.usesFlatStudioTerrain()"));
|
||||||
|
assertTrue(constantBiome >= 0);
|
||||||
|
assertTrue(visible.indexOf("return fallbackBiome;", constantBiome) < terrainResolution);
|
||||||
|
assertTrue(terrainResolution > constantBiome);
|
||||||
|
|
||||||
|
int structureStart = source.indexOf("private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(");
|
||||||
|
int structureEnd = source.indexOf("boolean cacheable", structureStart);
|
||||||
|
assertTrue(source.substring(structureStart, structureEnd).contains("platformGenerator.usesFlatStudioTerrain()"));
|
||||||
|
|
||||||
|
String generator = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||||
|
int createBiomes = generator.indexOf("public CompletableFuture<ChunkAccess> createBiomes(");
|
||||||
|
int buildSurface = generator.indexOf("public void buildSurface(", createBiomes);
|
||||||
|
assertTrue(generator.substring(createBiomes, buildSurface)
|
||||||
|
.contains("customBiomeSource.getVisibleNoiseBiomeWithActiveGenerationLease("));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
|
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
|
||||||
|
|||||||
+3
@@ -43,6 +43,9 @@ public final class GenerationHistoryRoutingContractTest {
|
|||||||
assertBefore(noise, "openHistoryRoute(", "requireGenerationLease(");
|
assertBefore(noise, "openHistoryRoute(", "requireGenerationLease(");
|
||||||
assertTrue(count(noise, "openHistoryRuntimeScope(route)") >= 2);
|
assertTrue(count(noise, "openHistoryRuntimeScope(route)") >= 2);
|
||||||
assertBefore(noise, ".thenApply(filled ->", "pipeline.whenComplete(");
|
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)"));
|
assertTrue(noise.contains("closeNoisePipelineResources(\n failure, lease, route, stage)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
@@ -221,6 +221,55 @@ public class IrisChunkGeneratorFailureContractTest {
|
|||||||
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
|
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authoringHeightmapStagesReturnBeforeAnyTerrainHeightResolver() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||||
|
int primeStart = source.indexOf("private void primeWorldgenHeightmaps(");
|
||||||
|
int primeEnd = source.indexOf("private IntBinaryOperator worldgenSurfaceHeight()", primeStart);
|
||||||
|
String prime = source.substring(primeStart, primeEnd);
|
||||||
|
assertBefore(prime, "platformGenerator.usesFlatStudioTerrain()", "worldgenSurfaceHeight()");
|
||||||
|
assertBefore(prime, "primeAuthoringHeightmaps(chunkAccess);", "return;");
|
||||||
|
assertBefore(prime, "return;", "WorldgenTerrainHeightmaps.primeTerrain(");
|
||||||
|
|
||||||
|
int decorationStart = source.indexOf("public void addVanillaDecorations(");
|
||||||
|
int decorationEnd = source.indexOf("public void spawnOriginalMobs(", decorationStart);
|
||||||
|
String decorations = source.substring(decorationStart, decorationEnd);
|
||||||
|
assertBefore(decorations, "primeWorldgenHeightmaps(chunkAccess);", "platformGenerator.usesFlatStudioTerrain()");
|
||||||
|
assertBefore(decorations, "platformGenerator.usesFlatStudioTerrain()", "return;");
|
||||||
|
assertBefore(decorations, "return;", "engine.getHeight(");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authoringDecorationSkipsNativeFeaturesButRetainsHeightmapsAndBukkitPopulators() throws IOException {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
|
||||||
|
String decoration = method(source,
|
||||||
|
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
|
||||||
|
"public BiomeGenerationSettings getBiomeGenerationSettings");
|
||||||
|
int structuresGuard = decoration.indexOf("if (!flatStudioTerrain && !ichunkaccess.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES))");
|
||||||
|
assertTrue(structuresGuard >= 0);
|
||||||
|
int structuresEnd = decoration.indexOf("\n }", structuresGuard);
|
||||||
|
String structures = decoration.substring(structuresGuard, structuresEnd);
|
||||||
|
assertTrue(structures.contains("placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);"));
|
||||||
|
int preparationGuard = decoration.indexOf("if (!flatStudioTerrain)");
|
||||||
|
assertTrue(structuresEnd < preparationGuard);
|
||||||
|
assertTrue(decoration.indexOf("NativeGenerationWriteGuard.allowsDecoration") > structuresEnd);
|
||||||
|
int preparationEnd = decoration.indexOf("\n }", preparationGuard);
|
||||||
|
String preparation = decoration.substring(preparationGuard, preparationEnd);
|
||||||
|
assertTrue(preparation.contains("importedFeatures.prepare(generatoraccessseed);"));
|
||||||
|
|
||||||
|
int placementGuard = decoration.indexOf("if (!flatStudioTerrain)", preparationEnd);
|
||||||
|
int placementEnd = decoration.indexOf("\n }", placementGuard);
|
||||||
|
String placement = decoration.substring(placementGuard, placementEnd);
|
||||||
|
assertTrue(placement.contains("importedFeatures.run(generatoraccessseed, ichunkaccess, this);"));
|
||||||
|
int heightmaps = decoration.indexOf("addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);");
|
||||||
|
int populators = decoration.indexOf("delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);");
|
||||||
|
assertTrue(heightmaps > preparationEnd && heightmaps < placementGuard);
|
||||||
|
assertTrue(populators > placementEnd);
|
||||||
|
assertTrue(decoration.indexOf("claimGeneratedSemantics(route,") > populators);
|
||||||
|
assertTrue(decoration.contains("requireGenerationStage(\"bukkit_nms_biome_decoration\")"));
|
||||||
|
assertTrue(decoration.contains("requireGenerationLease(\"bukkit_nms_biome_decoration\")"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void everyChunkUsesTheProductionGenerationStages() throws IOException {
|
public void everyChunkUsesTheProductionGenerationStages() throws IOException {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")))
|
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")))
|
||||||
|
|||||||
+3
-3
@@ -352,7 +352,7 @@ public class NMSBindingDatapackStructureScopeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void standardPublishesOneDeferredStateWhileJigsawPublishesInitializedEmptyState() throws IOException {
|
public void standardPublishesOneDeferredStateWhileAuthoringPublishesInitializedEmptyState() throws IOException {
|
||||||
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
|
||||||
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
|
||||||
int methodStart = source.indexOf("public DatapackStructureScopeResult scopeDatapackStructures(");
|
int methodStart = source.indexOf("public DatapackStructureScopeResult scopeDatapackStructures(");
|
||||||
@@ -365,8 +365,8 @@ public class NMSBindingDatapackStructureScopeTest {
|
|||||||
String method = source.substring(methodStart, methodEnd);
|
String method = source.substring(methodStart, methodEnd);
|
||||||
int filteredState = method.indexOf("possibleSetsField.set(scopedState, selection.structureSets());");
|
int filteredState = method.indexOf("possibleSetsField.set(scopedState, selection.structureSets());");
|
||||||
int jigsawMode = method.indexOf(
|
int jigsawMode = method.indexOf(
|
||||||
"boolean jigsawStudio = platformGenerator != null");
|
"boolean authoringStudio = platformGenerator != null");
|
||||||
int jigsawOnly = method.indexOf("if (jigsawStudio)");
|
int jigsawOnly = method.indexOf("if (authoringStudio)");
|
||||||
int emptyCreation = method.indexOf("ChunkGeneratorStructureState bootstrapState = createStructureState(");
|
int emptyCreation = method.indexOf("ChunkGeneratorStructureState bootstrapState = createStructureState(");
|
||||||
int emptyFiltering = method.indexOf("bootstrapSetsField.set(bootstrapState, List.of());");
|
int emptyFiltering = method.indexOf("bootstrapSetsField.set(bootstrapState, List.of());");
|
||||||
int emptyInitialization = method.indexOf("bootstrapState.ensureStructuresGenerated();");
|
int emptyInitialization = method.indexOf("bootstrapState.ensureStructuresGenerated();");
|
||||||
|
|||||||
+4
-4
@@ -8,11 +8,11 @@ import java.nio.file.Path;
|
|||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class NMSBindingServerLevelAdviceContractTest {
|
public class NmsWorldLifecycleServerLevelAdviceContractTest {
|
||||||
@Test
|
@Test
|
||||||
public void serverLevelAdviceSupportsCurrentSpigotAndPaperConstructorLayouts() throws Exception {
|
public void serverLevelAdviceSupportsCurrentSpigotAndPaperConstructorLayouts() throws Exception {
|
||||||
String source = Files.readString(
|
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(
|
String transformer = section(
|
||||||
source,
|
source,
|
||||||
"public boolean injectBukkit()",
|
"public boolean injectBukkit()",
|
||||||
@@ -35,7 +35,7 @@ public class NMSBindingServerLevelAdviceContractTest {
|
|||||||
@Test
|
@Test
|
||||||
public void currentSpigotStorageAccessReceivesManagedIdentityBeforeConstruction() throws Exception {
|
public void currentSpigotStorageAccessReceivesManagedIdentityBeforeConstruction() throws Exception {
|
||||||
String source = Files.readString(
|
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(
|
String transformer = section(
|
||||||
source,
|
source,
|
||||||
"public boolean injectBukkit()",
|
"public boolean injectBukkit()",
|
||||||
@@ -68,7 +68,7 @@ public class NMSBindingServerLevelAdviceContractTest {
|
|||||||
@Test
|
@Test
|
||||||
public void ownedWorldPublishesItsManagedIdentityAndRuntimeStemTogether() throws Exception {
|
public void ownedWorldPublishesItsManagedIdentityAndRuntimeStemTogether() throws Exception {
|
||||||
String source = Files.readString(
|
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(
|
String advice = section(
|
||||||
source,
|
source,
|
||||||
"private static class ServerLevelAdvice",
|
"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.assertTrue;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
|
||||||
public class NMSBindingShutdownBoundaryContractTest {
|
public class NmsWorldLifecycleShutdownBoundaryContractTest {
|
||||||
@Test
|
@Test
|
||||||
public void shutdownClassificationUsesTheServerStopFlagInsteadOfPluginEnablement() throws Exception {
|
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");
|
String stopping = section(source, "public boolean isServerStopping()", "public ServerShutdownBoundary createServerShutdownBoundary");
|
||||||
|
|
||||||
assertTrue(stopping.contains("((CraftServer) Bukkit.getServer()).getServer().hasStopped()"));
|
assertTrue(stopping.contains("((CraftServer) Bukkit.getServer()).getServer().hasStopped()"));
|
||||||
@@ -21,10 +21,10 @@ public class NMSBindingShutdownBoundaryContractTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void pluginLoaderGuardUsesSystemAgentIdentityAndIsRemovedAtRelease() throws Exception {
|
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 injection = section(source, "public boolean injectBukkit()", "public void ensureServerLevelInjection()");
|
||||||
String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class ChunkAccessAdvice");
|
String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class LevelStorageAccessAdvice");
|
||||||
String release = section(source, "public void releasePluginClassLoaderClose()", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
String release = section(source, "public void releasePluginClassLoaderClose()", "private static final class PluginClassLoaderInjectionListener");
|
||||||
|
|
||||||
assertTrue(injection.contains("Agent.requireClassLoaderCloseDeferral()"));
|
assertTrue(injection.contains("Agent.requireClassLoaderCloseDeferral()"));
|
||||||
assertTrue(injection.contains("getMethod(\"close\").getDeclaringClass()"));
|
assertTrue(injection.contains("getMethod(\"close\").getDeclaringClass()"));
|
||||||
@@ -40,8 +40,8 @@ public class NMSBindingShutdownBoundaryContractTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
|
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")).resolveSibling("NmsWorldLifecycle.java")).replace("\r\n", "\n");
|
||||||
String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "private static final class PluginClassLoaderInjectionListener");
|
||||||
|
|
||||||
assertTrue(boundary.contains("new ServerShutdownBoundary("));
|
assertTrue(boundary.contains("new ServerShutdownBoundary("));
|
||||||
assertTrue(boundary.contains("server.hasFullyShutdown"));
|
assertTrue(boundary.contains("server.hasFullyShutdown"));
|
||||||
+15
@@ -64,6 +64,21 @@ public class WorldgenTerrainHeightmapsTest {
|
|||||||
Bootstrap.bootStrap();
|
Bootstrap.bootStrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
|
||||||
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|
||||||
|
IrisChunkGenerator.primeAuthoringHeightmaps(chunk);
|
||||||
|
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, LAND_X, LAND_Z));
|
||||||
|
assertEquals(LAND_TERRAIN_TOP + 6,
|
||||||
|
chunk.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, LAND_X, LAND_Z));
|
||||||
|
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
|
||||||
|
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
|
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
|
||||||
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
|
||||||
|
|||||||
+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.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
import art.arcane.iris.core.events.IrisEngineHotloadEvent;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
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.nms.INMS;
|
||||||
import art.arcane.iris.core.project.IrisProject;
|
import art.arcane.iris.core.project.IrisProject;
|
||||||
import art.arcane.iris.core.project.IrisCodeWorkspace;
|
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.IrisToolbelt;
|
||||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.EngineMode;
|
||||||
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
import art.arcane.iris.engine.framework.EnginePlatformHooks;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
@@ -47,6 +51,16 @@ import org.bukkit.World;
|
|||||||
import org.bukkit.WorldBorder;
|
import org.bukkit.WorldBorder;
|
||||||
|
|
||||||
public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
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
|
@Override
|
||||||
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
public KList<NativeStructureVolume> nativeStructureVolumes(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
return INMS.get().nativeStructureVolumes(engine, minX, minZ, maxX, maxZ);
|
return INMS.get().nativeStructureVolumes(engine, minX, minZ, maxX, maxZ);
|
||||||
@@ -54,12 +68,17 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refreshWorkspace(Engine engine) {
|
public void refreshWorkspace(Engine engine) {
|
||||||
new IrisCodeWorkspace(new IrisProject(engine.getData().getDataFolder())).updateWorkspace();
|
new IrisCodeWorkspace(new IrisProject(engine.getPackSource().toFile())).updateWorkspace();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void refreshDatapackWorkspace(Engine engine) {
|
public void refreshDatapackWorkspace(Engine engine) {
|
||||||
DatapackIngestService.refreshWorkspace(engine.getData());
|
IrisData data = IrisData.openRuntime(engine.getPackSource().toFile());
|
||||||
|
try {
|
||||||
|
DatapackIngestService.refreshWorkspace(data);
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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();
|
World world = player.getWorld();
|
||||||
Engine engine = engineFor(world);
|
Engine engine = engineFor(world);
|
||||||
if (engine != null) {
|
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);
|
world.getSeed(), engine.getMinHeight(), engine.getMaxHeight(), true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ plugins {
|
|||||||
alias(libs.plugins.shadow)
|
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()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ plugins {
|
|||||||
alias(libs.plugins.shadow)
|
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()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
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;
|
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.loader.IrisData;
|
||||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.engine.IrisEngine;
|
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);
|
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
|
||||||
|
|
||||||
writeBlocks(chunk, blocks, dimMinY, height);
|
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);
|
writeTerrainHeightmaps(chunk, generationEngine, pos, height);
|
||||||
Heightmap.primeHeightmaps(chunk, EnumSet.of(
|
Heightmap.primeHeightmaps(chunk, EnumSet.of(
|
||||||
Heightmap.Types.MOTION_BLOCKING,
|
Heightmap.Types.MOTION_BLOCKING,
|
||||||
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
||||||
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
||||||
if (route != null) {
|
|
||||||
route.claimGeneratedSemantics();
|
|
||||||
}
|
|
||||||
return chunk;
|
return chunk;
|
||||||
} catch (GenerationSessionException e) {
|
} catch (GenerationSessionException e) {
|
||||||
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
|
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
|
||||||
@@ -1066,6 +1075,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
completion.completeExceptionally(completionFailure);
|
completion.completeExceptionally(completionFailure);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
route.detachThread();
|
||||||
return completion;
|
return completion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1205,6 +1215,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
|
if (chunk.getPersistedStatus().isOrAfter(ChunkStatus.FEATURES)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
|
||||||
if (!allowsRoutedDiscreteGeneration(
|
if (!allowsRoutedDiscreteGeneration(
|
||||||
current,
|
current,
|
||||||
route,
|
route,
|
||||||
@@ -1214,8 +1228,10 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
)) {
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!NativeGenerationWriteGuard.allowsDecoration(current, level, chunkPos)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
importedFeatures.prepare(current);
|
importedFeatures.prepare(current);
|
||||||
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
|
|
||||||
// Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk
|
// 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
|
// step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the
|
||||||
// dimension set importedFeatures.enabled.
|
// dimension set importedFeatures.enabled.
|
||||||
@@ -1257,6 +1273,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
));
|
));
|
||||||
nativeStructures.adjustGeneratedStructures(
|
nativeStructures.adjustGeneratedStructures(
|
||||||
registryAccess, chunk, previousStarts, configuredStarts, current, templateManager);
|
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");
|
current, x, z, "modded_base_height");
|
||||||
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
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
|
int height = current.getDimensionStackContext() == null
|
||||||
? current.getHeight(x, z, ignoreFluid)
|
? current.getHeight(x, z, ignoreFluid)
|
||||||
: Engine.hostHeight(current, x, z, ignoreFluid);
|
: Engine.hostHeight(current, x, z, ignoreFluid);
|
||||||
@@ -1367,12 +1388,16 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||||
int minY = heightAccessor.getMinY();
|
int minY = heightAccessor.getMinY();
|
||||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
|
||||||
Engine current = requireDataQueryEngine("base column");
|
Engine current = requireDataQueryEngine("base column");
|
||||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
current, x, z, "modded_base_column");
|
current, x, z, "modded_base_column");
|
||||||
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
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();
|
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||||
boolean dimensionStack = current.getDimensionStackContext() != null;
|
boolean dimensionStack = current.getDimensionStackContext() != null;
|
||||||
int surface = dimensionStack
|
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(
|
private boolean allowsRoutedDiscreteGeneration(
|
||||||
Engine current,
|
Engine current,
|
||||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||||
@@ -1407,7 +1438,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
if (route == null) {
|
if (route == null) {
|
||||||
return allowsGenerationHistoryBypass(current);
|
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) {
|
private void bindGenerationRuntimeRetirementListener(Engine current) {
|
||||||
|
|||||||
+4
-2
@@ -362,13 +362,15 @@ public final class ModdedForcedDatapack {
|
|||||||
KList<String> presetIds = new KList<>();
|
KList<String> presetIds = new KList<>();
|
||||||
Path worldRoot = worldRootOrNull();
|
Path worldRoot = worldRootOrNull();
|
||||||
Set<PackSelection> legacySelections = unadoptedSelections(worldRoot);
|
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);
|
PackStageContext context = packStageContext(pack.toPath(), legacySelections);
|
||||||
if (stagePack(context, fixer, stagingDirectory, seenBiomes, presetIds)) {
|
if (stagePack(context, fixer, stagingDirectory, seenBiomes, presetIds)) {
|
||||||
packCount++;
|
packCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IrisDatapackCompiler.installRetainedRegistrySources(packRoots, List.of(stagingDirectory.toFile()), fixer);
|
||||||
writePackMeta(stagingDirectory);
|
writePackMeta(stagingDirectory);
|
||||||
// Forge only: FML has no block-drops event Iris can use, so drops are routed through a global loot
|
// 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
|
// modifier. NeoForge does not need one - IrisNeoForgeBootstrap listens to BlockDropsEvent and both
|
||||||
@@ -996,7 +998,7 @@ public final class ModdedForcedDatapack {
|
|||||||
return currentSource;
|
return currentSource;
|
||||||
}
|
}
|
||||||
PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey);
|
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;
|
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.DimensionStackContext;
|
||||||
import art.arcane.iris.engine.DimensionStackLayout;
|
import art.arcane.iris.engine.DimensionStackLayout;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
@@ -341,7 +345,8 @@ final class ModdedNativeStructureStage {
|
|||||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||||
for (StructureStart start : starts) {
|
for (StructureStart start : starts) {
|
||||||
BoundingBox footprint = start.getBoundingBox();
|
BoundingBox footprint = start.getBoundingBox();
|
||||||
if (!current.getComplex().allowsNewGenerationFootprint(
|
if (!isHistoricalStructureStart(current, world, start)
|
||||||
|
&& !current.getComplex().allowsNewGenerationFootprint(
|
||||||
footprint.minX(),
|
footprint.minX(),
|
||||||
footprint.minZ(),
|
footprint.minZ(),
|
||||||
footprint.maxX(),
|
footprint.maxX(),
|
||||||
@@ -574,6 +579,16 @@ final class ModdedNativeStructureStage {
|
|||||||
return blockState;
|
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) {
|
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||||
ChunkPos chunkPos = chunk.getPos();
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
int minX = chunkPos.getMinBlockX();
|
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
|
ModdedGenerationHistoryStorage.ActivePack opened
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
GenerationHistory history = opened.history();
|
GenerationHistory history = opened.history();
|
||||||
|
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||||
|
opened = ModdedGenerationHistoryStorage.resolveActive(history);
|
||||||
long openedActivationId = history.activeActivation().activationId();
|
long openedActivationId = history.activeActivation().activationId();
|
||||||
IrisEngine candidate = buildEngine(level, seed, opened);
|
IrisEngine candidate = buildEngine(level, seed, opened);
|
||||||
boolean ready = false;
|
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.PregenTask;
|
||||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||||
import art.arcane.iris.core.pregenerator.cache.PregenCache;
|
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.core.pregenerator.methods.CachedPregenMethod;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
@@ -64,7 +65,9 @@ public final class ModdedPregenJob {
|
|||||||
ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync);
|
ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync);
|
||||||
PregeneratorMethod method = moddedMethod;
|
PregeneratorMethod method = moddedMethod;
|
||||||
if (cached) {
|
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);
|
ActivePregen active = new ActivePregen(engine, moddedMethod);
|
||||||
ACTIVE.set(active);
|
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.loader.IrisData;
|
||||||
import art.arcane.iris.core.tools.WorldMaintenance;
|
import art.arcane.iris.core.tools.WorldMaintenance;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.EnginePlatformHooks;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -64,6 +68,16 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
|
|||||||
private volatile ExecutorService executor;
|
private volatile ExecutorService executor;
|
||||||
private long lastPollAt;
|
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
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
if (executor != null) {
|
if (executor != null) {
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
"package": "art.arcane.iris.modded.mixin",
|
"package": "art.arcane.iris.modded.mixin",
|
||||||
"compatibilityLevel": "JAVA_21",
|
"compatibilityLevel": "JAVA_21",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
|
"WorldGenerationWriteGuardMixin",
|
||||||
|
"ChunkMapTerrainReceiptAccess",
|
||||||
|
"ChunkTerrainReceiptMixin",
|
||||||
|
"LevelChunkTerrainReceiptMixin",
|
||||||
|
"SerializableTerrainReceiptMixin",
|
||||||
"EntityPersistenceMixin",
|
"EntityPersistenceMixin",
|
||||||
"LivingEntityLootMixin",
|
"LivingEntityLootMixin",
|
||||||
"MobAwarenessMixin",
|
"MobAwarenessMixin",
|
||||||
|
|||||||
+37
@@ -19,6 +19,16 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
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 org.junit.Test;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -39,6 +49,33 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class ModdedForcedDatapackTest {
|
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
|
@Test
|
||||||
public void packScopedIdsCannotCollideAndHaveReadableLabels() {
|
public void packScopedIdsCannotCollideAndHaveReadableLabels() {
|
||||||
String first = ModdedWorldgenIds.presetRef("overworld", "overworld");
|
String first = ModdedWorldgenIds.presetRef("overworld", "overworld");
|
||||||
|
|||||||
+29
-6
@@ -25,7 +25,15 @@ public class ModdedGenerationHistoryRoutingContractTest {
|
|||||||
assertTrue(createHistoryEngine.contains("IrisBoundarySignatureSampler.INSTANCE"));
|
assertTrue(createHistoryEngine.contains("IrisBoundarySignatureSampler.INSTANCE"));
|
||||||
assertTrue(createHistoryEngine.contains("getGenerationTransitionWidthBlocks()"));
|
assertTrue(createHistoryEngine.contains("getGenerationTransitionWidthBlocks()"));
|
||||||
assertTrue(createHistoryEngine.contains("router.preloadActiveRuntimes();"));
|
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(createHistoryEngine.contains("GenerationHistoryRuntimeRouter.attach("));
|
||||||
assertTrue(buildEngine.contains("active.packRoot().toFile()"));
|
assertTrue(buildEngine.contains("active.packRoot().toFile()"));
|
||||||
assertTrue(buildEngine.contains("history.paths().activationMantleRoot(activation.activationId())"));
|
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("generateTerrain(chunk, generationEngine, pos, air, route)"));
|
||||||
assertTrue(fill.contains("return closeRouteOnCompletion(pipeline, route);"));
|
assertTrue(fill.contains("return closeRouteOnCompletion(pipeline, route);"));
|
||||||
assertTrue(terrain.contains("openHistoryRuntimeScope(route)"));
|
assertTrue(terrain.contains("openHistoryRuntimeScope(route)"));
|
||||||
assertBefore(terrain, "generationEngine.generate(", "route.claimGeneratedSemantics();");
|
assertBefore(terrain, "generationEngine.generate(", "writeBlocks(chunk, blocks, dimMinY, height)");
|
||||||
assertBefore(terrain, "route.claimGeneratedSemantics();", "return chunk;");
|
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("pipeline.whenComplete("));
|
||||||
assertTrue(completion.contains("route.close();"));
|
assertTrue(completion.contains("route.close();"));
|
||||||
|
assertBefore(completion, "pipeline.whenComplete(", "route.detachThread()");
|
||||||
|
assertBefore(completion, "route.detachThread()", "return completion;");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -108,7 +122,7 @@ public class ModdedGenerationHistoryRoutingContractTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void discreteStagesResumeIncompleteHistoricalChunksAndRejectTransitionOwnership() throws IOException {
|
public void pendingNativeStagesPreserveSavedStartsAndGuardNewFeatureFootprints() throws IOException {
|
||||||
String generator = source("IrisModdedChunkGenerator.java");
|
String generator = source("IrisModdedChunkGenerator.java");
|
||||||
String discrete = method(generator, "private boolean allowsRoutedDiscreteGeneration(");
|
String discrete = method(generator, "private boolean allowsRoutedDiscreteGeneration(");
|
||||||
String decoration = method(generator, "public void applyBiomeDecoration(");
|
String decoration = method(generator, "public void applyBiomeDecoration(");
|
||||||
@@ -117,19 +131,28 @@ public class ModdedGenerationHistoryRoutingContractTest {
|
|||||||
String nativeStage = source("ModdedNativeStructureStage.java");
|
String nativeStage = source("ModdedNativeStructureStage.java");
|
||||||
String adjust = method(nativeStage, "void adjustGeneratedStructures(");
|
String adjust = method(nativeStage, "void adjustGeneratedStructures(");
|
||||||
String place = method(nativeStage, "void placeVanillaStructures(");
|
String place = method(nativeStage, "void placeVanillaStructures(");
|
||||||
|
String savedStart = method(nativeStage, "private boolean isHistoricalStructureStart(");
|
||||||
|
|
||||||
assertTrue(discrete.contains("chunk.getPersistedStatus().isOrAfter(stage)"));
|
assertTrue(discrete.contains("chunk.getPersistedStatus().isOrAfter(stage)"));
|
||||||
assertFalse(discrete.contains("router.history().activeActivation().activationId()"));
|
assertFalse(discrete.contains("router.history().activeActivation().activationId()"));
|
||||||
assertFalse(discrete.contains("return true;"));
|
assertFalse(discrete.contains("return true;"));
|
||||||
assertTrue(discrete.contains("allowsNewGenerationChunk("));
|
assertTrue(discrete.contains("allowsNewGenerationChunk("));
|
||||||
|
assertTrue(discrete.contains("NativeGenerationWriteGuard.allowsPendingStage(current, chunk, stage)"));
|
||||||
assertTrue(decoration.contains("ChunkStatus.FEATURES"));
|
assertTrue(decoration.contains("ChunkStatus.FEATURES"));
|
||||||
assertTrue(structures.contains("ChunkStatus.STRUCTURE_STARTS"));
|
assertTrue(structures.contains("ChunkStatus.STRUCTURE_STARTS"));
|
||||||
assertTrue(mobs.contains("ChunkStatus.SPAWN"));
|
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(structures, "allowsRoutedDiscreteGeneration(", "super.createStructures(");
|
||||||
assertBefore(mobs, "allowsRoutedDiscreteGeneration(", "NaturalSpawner.spawnMobsForChunkGeneration(");
|
assertBefore(mobs, "allowsRoutedDiscreteGeneration(", "NaturalSpawner.spawnMobsForChunkGeneration(");
|
||||||
assertBefore(adjust, "allowsNewGenerationFootprint(", "recordWorldCheckStructureShift(");
|
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) {
|
private static void assertRuntimeRoute(String source, String signature) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ plugins {
|
|||||||
alias(libs.plugins.shadow)
|
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()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
|
|||||||
+4
-3
@@ -72,7 +72,7 @@ String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${lo
|
|||||||
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")
|
||||||
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
|
||||||
File pluginOutDirectory = layout.projectDirectory.dir('../PluginOuts').asFile
|
File pluginOutDirectory = layout.projectDirectory.dir('../PluginOuts').asFile
|
||||||
long maximumBukkitArtifactBytes = 7_500_000L
|
long maximumBukkitArtifactBytes = 8_000_000L
|
||||||
apply plugin: ApiGenerator
|
apply plugin: ApiGenerator
|
||||||
|
|
||||||
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
|
// Where `buildAll` drops the per-platform jars for a local test server. Use the approved sibling
|
||||||
@@ -113,6 +113,7 @@ nmsBindings.each { key, value ->
|
|||||||
compileOnly(rootProject.libs.annotations)
|
compileOnly(rootProject.libs.annotations)
|
||||||
compileOnly(rootProject.libs.byteBuddy.core)
|
compileOnly(rootProject.libs.byteBuddy.core)
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
|
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||||
testImplementation(project(':core'))
|
testImplementation(project(':core'))
|
||||||
testCompileOnly(volmLibCoordinate) {
|
testCompileOnly(volmLibCoordinate) {
|
||||||
transitive = false
|
transitive = false
|
||||||
@@ -427,7 +428,7 @@ tasks.named('neoforgeJar').configure {
|
|||||||
|
|
||||||
List<String> commonModdedArtifactEntries = [
|
List<String> commonModdedArtifactEntries = [
|
||||||
'META-INF/iris/generation-kernels/catalog.tsv',
|
'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/engine/IrisEngine.class',
|
||||||
'art/arcane/iris/spi/IrisPlatform.class',
|
'art/arcane/iris/spi/IrisPlatform.class',
|
||||||
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.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.
|
// them, so a shadowJar exclude that drops one is silent until a server hits that path.
|
||||||
List<String> requiredBukkitArtifactEntries = [
|
List<String> requiredBukkitArtifactEntries = [
|
||||||
'META-INF/iris/generation-kernels/catalog.tsv',
|
'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',
|
'plugin.yml',
|
||||||
'paper-plugin.yml',
|
'paper-plugin.yml',
|
||||||
'slimjar.dat',
|
'slimjar.dat',
|
||||||
|
|||||||
+27
-28
@@ -24,18 +24,18 @@ import java.util.stream.Stream;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
public final class GenerationKernelSeal {
|
public final class GenerationBuildRevision {
|
||||||
public static final String FORMAT = "iris-generation-kernel-seal-v1";
|
public static final String FORMAT = "iris-generation-build-revision-v1";
|
||||||
private static final Pattern HASH = Pattern.compile("[0-9a-f]{64}");
|
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 static final int MAXIMUM_ENTRIES = 30_000;
|
||||||
|
|
||||||
private GenerationKernelSeal() {
|
private GenerationBuildRevision() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] arguments) throws IOException {
|
public static void main(String[] arguments) throws IOException {
|
||||||
if (arguments.length < 4 || arguments.length % 2 != 0 || !arguments[0].equals("verify-dependencies")) {
|
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<>();
|
Map<String, Path> dependencies = new TreeMap<>();
|
||||||
for (int index = 2; index < arguments.length; index += 2) {
|
for (int index = 2; index < arguments.length; index += 2) {
|
||||||
@@ -46,8 +46,8 @@ public final class GenerationKernelSeal {
|
|||||||
verifyDependencySubset(Path.of(arguments[1]), dependencies);
|
verifyDependencySubset(Path.of(arguments[1]), dependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void verifyDependencySubset(Path seal, Map<String, Path> dependencies) throws IOException {
|
public static void verifyDependencySubset(Path revision, Map<String, Path> dependencies) throws IOException {
|
||||||
SourceManifest expected = read(seal);
|
SourceManifest expected = read(revision);
|
||||||
for (Map.Entry<String, String> dependency : dependencyHashes(dependencies).entrySet()) {
|
for (Map.Entry<String, String> dependency : dependencyHashes(dependencies).entrySet()) {
|
||||||
if (!Objects.equals(expected.dependencies().get(dependency.getKey()), dependency.getValue())) {
|
if (!Objects.equals(expected.dependencies().get(dependency.getKey()), dependency.getValue())) {
|
||||||
throw new IOException("Resolved platform dependency does not match generation ABI " + expected.abi()
|
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]));
|
AlgorithmVersion algorithms = new AlgorithmVersion(positive(current[2]), positive(current[3]));
|
||||||
SourceManifest selected = manifests.get(currentAbi);
|
SourceManifest selected = manifests.get(currentAbi);
|
||||||
if (selected == null || !selected.algorithms().contains(algorithms)) {
|
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<>();
|
TreeSet<Integer> registered = new TreeSet<>();
|
||||||
int previous = 0;
|
int previous = 0;
|
||||||
@@ -92,7 +92,7 @@ public final class GenerationKernelSeal {
|
|||||||
registered.add(abi);
|
registered.add(abi);
|
||||||
}
|
}
|
||||||
if (!registered.equals(manifests.keySet())) {
|
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) {
|
} catch (IllegalArgumentException failure) {
|
||||||
throw new IOException("Invalid generation kernel catalog.", failure);
|
throw new IOException("Invalid generation kernel catalog.", failure);
|
||||||
@@ -116,33 +116,32 @@ public final class GenerationKernelSeal {
|
|||||||
sourceHashes, dependencyHashes(dependencies));
|
sourceHashes, dependencyHashes(dependencies));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void verify(Path repository, Path seal, Map<String, Path> dependencies) throws IOException {
|
public static void verifySnapshot(Path repository, Path revision, Map<String, Path> dependencies) throws IOException {
|
||||||
SourceManifest expected = read(seal);
|
SourceManifest expected = read(revision);
|
||||||
SourceManifest actual = capture(new CaptureOptions(repository, expected.abi(), expected.factoryClass(), expected.algorithms(),
|
SourceManifest actual = capture(new CaptureOptions(repository, expected.abi(), expected.factoryClass(), expected.algorithms(),
|
||||||
expected.roots(), expected.exclusions()), dependencies);
|
expected.roots(), expected.exclusions()), dependencies);
|
||||||
List<String> failures = new ArrayList<>();
|
List<String> failures = new ArrayList<>();
|
||||||
differences("source", expected.sources(), actual.sources(), failures);
|
differences("source", expected.sources(), actual.sources(), failures);
|
||||||
differences("dependency", expected.dependencies(), actual.dependencies(), failures);
|
differences("dependency", expected.dependencies(), actual.dependencies(), failures);
|
||||||
if (!failures.isEmpty()) {
|
if (!failures.isEmpty()) {
|
||||||
throw new IOException("Generation ABI " + expected.abi() + " no longer matches its sealed implementation:\n"
|
throw new IOException("Generation build inputs changed after revision capture:\n"
|
||||||
+ String.join("\n", failures)
|
+ String.join("\n", failures));
|
||||||
+ "\nRetain this kernel's implementation and add a separately versioned kernel for changed generation.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.createDirectories(destination.toAbsolutePath().getParent());
|
||||||
Files.writeString(destination, encode(manifest), StandardCharsets.UTF_8,
|
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 {
|
public static SourceManifest read(Path path) throws IOException {
|
||||||
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > MAXIMUM_SEAL_BYTES) {
|
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > MAXIMUM_REVISION_BYTES) {
|
||||||
throw new IOException("Missing or oversized generation kernel seal: " + path);
|
throw new IOException("Missing or oversized generation build revision: " + path);
|
||||||
}
|
}
|
||||||
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
|
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
|
||||||
if (lines.size() < 4 || !FORMAT.equals(lines.getFirst()) || lines.size() > MAXIMUM_ENTRIES) {
|
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;
|
int abi = 0;
|
||||||
String factory = null;
|
String factory = null;
|
||||||
@@ -155,7 +154,7 @@ public final class GenerationKernelSeal {
|
|||||||
for (int index = 1; index < lines.size(); index++) {
|
for (int index = 1; index < lines.size(); index++) {
|
||||||
String[] entry = lines.get(index).split("\t", -1);
|
String[] entry = lines.get(index).split("\t", -1);
|
||||||
if (entry.length < 2 || entry.length > 3) {
|
if (entry.length < 2 || entry.length > 3) {
|
||||||
throw new IllegalArgumentException("Invalid seal entry.");
|
throw new IllegalArgumentException("Invalid revision entry.");
|
||||||
}
|
}
|
||||||
switch (entry[0]) {
|
switch (entry[0]) {
|
||||||
case "abi" -> {
|
case "abi" -> {
|
||||||
@@ -180,16 +179,16 @@ public final class GenerationKernelSeal {
|
|||||||
case "exclude" -> addPath(exclusions, entry);
|
case "exclude" -> addPath(exclusions, entry);
|
||||||
case "source" -> addHash(sources, entry, true);
|
case "source" -> addHash(sources, entry, true);
|
||||||
case "dependency" -> addHash(dependencies, entry, false);
|
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);
|
SourceManifest manifest = new SourceManifest(abi, factory, algorithms, roots, exclusions, sources, dependencies);
|
||||||
if (!Files.readString(path, StandardCharsets.UTF_8).equals(encode(manifest))) {
|
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;
|
return manifest;
|
||||||
} catch (RuntimeException failure) {
|
} 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) {
|
private static void addHash(Map<String, String> hashes, String[] entry, boolean path) {
|
||||||
if (entry.length != 3 || !HASH.matcher(entry[2]).matches()) {
|
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]);
|
String key = path ? relative(entry[1]) : token(entry[1]);
|
||||||
if (hashes.putIfAbsent(key, entry[2]) != null) {
|
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) {
|
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)) {
|
if (required.isBlank() || required.chars().anyMatch(Character::isWhitespace)) {
|
||||||
throw new IllegalArgumentException("Invalid generation seal token.");
|
throw new IllegalArgumentException("Invalid generation revision token.");
|
||||||
}
|
}
|
||||||
return required;
|
return required;
|
||||||
}
|
}
|
||||||
@@ -442,7 +441,7 @@ public final class GenerationKernelSeal {
|
|||||||
sources = Map.copyOf(sources);
|
sources = Map.copyOf(sources);
|
||||||
dependencies = Map.copyOf(dependencies);
|
dependencies = Map.copyOf(dependencies);
|
||||||
if (sources.isEmpty() || dependencies.isEmpty() || sources.size() + dependencies.size() > MAXIMUM_ENTRIES) {
|
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.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
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.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class GenerationKernelSealTest {
|
public class GenerationBuildRevisionTest {
|
||||||
@Rule
|
@Rule
|
||||||
public final TemporaryFolder temporary = new TemporaryFolder();
|
public final TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void verifiesUnchangedSourceAndCanonicalDependencyContents() throws Exception {
|
public void verifiesUnchangedSourceAndCanonicalDependencyContents() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
|
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
|
||||||
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
|
String original = GenerationBuildRevision.artifactHash(fixture.dependency());
|
||||||
artifact(fixture.dependency(), "same code", "another version", 1_700_000_000_000L);
|
artifact(fixture.dependency(), "same code", "another version", 1_700_000_000_000L);
|
||||||
assertEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
|
assertEquals(original, GenerationBuildRevision.artifactHash(fixture.dependency()));
|
||||||
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
|
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
|
||||||
assertEquals(GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())),
|
assertEquals(GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision())),
|
||||||
GenerationKernelSeal.fingerprint(GenerationKernelSeal.capture(fixture.options(), fixture.dependencies())));
|
GenerationBuildRevision.fingerprint(GenerationBuildRevision.capture(fixture.options(), fixture.dependencies())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -39,120 +39,118 @@ public class GenerationKernelSealTest {
|
|||||||
Path source = fixture.root().resolve("generation/Noise.java");
|
Path source = fixture.root().resolve("generation/Noise.java");
|
||||||
Files.writeString(source, "class Noise { int value = 2; }");
|
Files.writeString(source, "class Noise { int value = 2; }");
|
||||||
assertTrue(assertThrows(IOException.class,
|
assertTrue(assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
|
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()))
|
||||||
.getMessage().contains("source changed"));
|
.getMessage().contains("source changed"));
|
||||||
Files.writeString(source, "class Noise { int value = 1; }");
|
Files.writeString(source, "class Noise { int value = 1; }");
|
||||||
Path added = fixture.root().resolve("generation/Added.java");
|
Path added = fixture.root().resolve("generation/Added.java");
|
||||||
Files.writeString(added, "class Added {}");
|
Files.writeString(added, "class Added {}");
|
||||||
assertTrue(assertThrows(IOException.class,
|
assertTrue(assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
|
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()))
|
||||||
.getMessage().contains("source added"));
|
.getMessage().contains("source added"));
|
||||||
Files.delete(added);
|
Files.delete(added);
|
||||||
Files.delete(source);
|
Files.delete(source);
|
||||||
assertThrows(IOException.class,
|
assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
|
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsChangedAddedAndDeletedDependencies() throws Exception {
|
public void rejectsChangedAddedAndDeletedDependencies() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
|
String original = GenerationBuildRevision.artifactHash(fixture.dependency());
|
||||||
artifact(fixture.dependency(), "changed code", "same version", 0L);
|
artifact(fixture.dependency(), "changed code", "same version", 0L);
|
||||||
assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
|
assertNotEquals(original, GenerationBuildRevision.artifactHash(fixture.dependency()));
|
||||||
assertTrue(assertThrows(IOException.class,
|
assertTrue(assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
|
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()))
|
||||||
.getMessage().contains("dependency changed"));
|
.getMessage().contains("dependency changed"));
|
||||||
artifact(fixture.dependency(), "same code", "same version", 0L);
|
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())));
|
Map.of("math", fixture.dependency(), "new-math", fixture.dependency())));
|
||||||
assertThrows(IOException.class,
|
assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), Map.of()));
|
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), Map.of()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsMultiReleaseManifestChanges() throws Exception {
|
public void rejectsMultiReleaseManifestChanges() throws Exception {
|
||||||
Fixture fixture = fixture();
|
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);
|
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,
|
assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
|
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rejectsAgentEntrypointAndCapabilityChanges() throws Exception {
|
public void rejectsAgentEntrypointAndCapabilityChanges() throws Exception {
|
||||||
Fixture fixture = fixture();
|
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",
|
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")) {
|
"Can-Redefine-Classes", "Can-Retransform-Classes", "Can-Set-Native-Method-Prefix")) {
|
||||||
artifact(fixture.dependency(), "same code", "same version\r\n" + attribute + ": changed", 0L);
|
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,
|
assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
|
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sealsExplicitAgentBuildInputsAndExecutableSources() throws Exception {
|
public void capturesExplicitAgentBuildInputsAndExecutableSources() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
Path agentSource = fixture.root().resolve("agent/src/main/java/Installer.java");
|
Path agentSource = fixture.root().resolve("agent/src/main/java/Installer.java");
|
||||||
Path agentBuild = fixture.root().resolve("agent/build.gradle");
|
Path agentBuild = fixture.root().resolve("agent/build.gradle");
|
||||||
Files.createDirectories(agentSource.getParent());
|
Files.createDirectories(agentSource.getParent());
|
||||||
Files.writeString(agentSource, "class Installer {}");
|
Files.writeString(agentSource, "class Installer {}");
|
||||||
Files.writeString(agentBuild, "plugins { id 'java' }");
|
Files.writeString(agentBuild, "plugins { id 'java' }");
|
||||||
GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions(
|
GenerationBuildRevision.CaptureOptions options = new GenerationBuildRevision.CaptureOptions(
|
||||||
fixture.root(), 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)),
|
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"));
|
List.of("generation", "agent/build.gradle", "agent/src/main/java"), List.of("generation/kernels"));
|
||||||
Path seal = fixture.root().resolve("agent.seal");
|
Path revision = fixture.root().resolve("agent.revision");
|
||||||
GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, fixture.dependencies()));
|
GenerationBuildRevision.write(revision, GenerationBuildRevision.capture(options, fixture.dependencies()));
|
||||||
GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies());
|
GenerationBuildRevision.verifySnapshot(fixture.root(), revision, fixture.dependencies());
|
||||||
Files.writeString(agentBuild, "plugins { id 'application' }");
|
Files.writeString(agentBuild, "plugins { id 'application' }");
|
||||||
assertThrows(IOException.class,
|
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(agentBuild, "plugins { id 'java' }");
|
||||||
Files.writeString(agentSource, "class Installer { boolean defers; }");
|
Files.writeString(agentSource, "class Installer { boolean defers; }");
|
||||||
assertThrows(IOException.class,
|
assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies()));
|
() -> GenerationBuildRevision.verifySnapshot(fixture.root(), revision, fixture.dependencies()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void checksTheActualPlatformDependencySubset() throws Exception {
|
public void checksTheActualPlatformDependencySubset() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies());
|
GenerationBuildRevision.verifyDependencySubset(fixture.revision(), fixture.dependencies());
|
||||||
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(),
|
assertThrows(IOException.class, () -> GenerationBuildRevision.verifyDependencySubset(fixture.revision(),
|
||||||
Map.of("unknown-library", fixture.dependency())));
|
Map.of("unknown-library", fixture.dependency())));
|
||||||
artifact(fixture.dependency(), "platform override", "same version", 0L);
|
artifact(fixture.dependency(), "platform override", "same version", 0L);
|
||||||
assertThrows(IOException.class,
|
assertThrows(IOException.class,
|
||||||
() -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies()));
|
() -> GenerationBuildRevision.verifyDependencySubset(fixture.revision(), fixture.dependencies()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void catalogCannotSelectAnUnsealedAlgorithmTuple() throws Exception {
|
public void catalogRequiresAPackagedAlgorithmTuple() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
Path catalog = fixture.root().resolve("catalog.tsv");
|
Path catalog = fixture.root().resolve("catalog.tsv");
|
||||||
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\n");
|
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()));
|
Map<Integer, GenerationBuildRevision.SourceManifest> manifests = Map.of(1, GenerationBuildRevision.read(fixture.revision()));
|
||||||
GenerationKernelSeal.verifyCatalog(catalog, manifests);
|
GenerationBuildRevision.verifyCatalog(catalog, manifests);
|
||||||
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t2\t1\nkernel\t1\n");
|
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");
|
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
|
@Test
|
||||||
public void permitsSeparatelyScopedFutureKernelsWithoutChangingRetainedSeal() throws Exception {
|
public void recapturesChangedGenerationWithoutChangingAbi() throws Exception {
|
||||||
Fixture fixture = fixture();
|
Fixture fixture = fixture();
|
||||||
Path future = fixture.root().resolve("generation/kernels/v2/NewMath.java");
|
String original = GenerationBuildRevision.fingerprint(GenerationBuildRevision.read(fixture.revision()));
|
||||||
Files.createDirectories(future.getParent());
|
Files.writeString(fixture.root().resolve("generation/Noise.java"), "class Noise { int value = 9; }");
|
||||||
Files.writeString(future, "class NewMath {}");
|
artifact(fixture.dependency(), "new generation code", "new version", 0L);
|
||||||
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
|
GenerationBuildRevision.SourceManifest next = GenerationBuildRevision.capture(fixture.options(), fixture.dependencies());
|
||||||
GenerationKernelSeal.SourceManifest next = GenerationKernelSeal.capture(new GenerationKernelSeal.CaptureOptions(
|
GenerationBuildRevision.write(fixture.revision(), next);
|
||||||
fixture.root(), 2, "example.KernelV2", List.of(new GenerationKernelSeal.AlgorithmVersion(2, 3)),
|
GenerationBuildRevision.verifySnapshot(fixture.root(), fixture.revision(), fixture.dependencies());
|
||||||
List.of("generation/kernels/v2"), List.of()), fixture.dependencies());
|
assertEquals(1, next.abi());
|
||||||
assertNotEquals(GenerationKernelSeal.fingerprint(next),
|
assertNotEquals(original, GenerationBuildRevision.fingerprint(next));
|
||||||
GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())));
|
assertEquals(next, GenerationBuildRevision.read(fixture.revision()));
|
||||||
assertThrows(IOException.class,
|
|
||||||
() -> GenerationKernelSeal.writeNew(fixture.seal(), next));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Fixture fixture() throws Exception {
|
private Fixture fixture() throws Exception {
|
||||||
@@ -161,13 +159,13 @@ public class GenerationKernelSealTest {
|
|||||||
Files.writeString(root.resolve("generation/Noise.java"), "class Noise { int value = 1; }");
|
Files.writeString(root.resolve("generation/Noise.java"), "class Noise { int value = 1; }");
|
||||||
Path dependency = root.resolve("math.jar");
|
Path dependency = root.resolve("math.jar");
|
||||||
artifact(dependency, "same code", "same version", 0L);
|
artifact(dependency, "same code", "same version", 0L);
|
||||||
GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions(
|
GenerationBuildRevision.CaptureOptions options = new GenerationBuildRevision.CaptureOptions(
|
||||||
root, 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)),
|
root, 1, "example.KernelV1", List.of(new GenerationBuildRevision.AlgorithmVersion(1, 1)),
|
||||||
List.of("generation"), List.of("generation/kernels"));
|
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);
|
Map<String, Path> dependencies = Map.of("math", dependency);
|
||||||
GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, dependencies));
|
GenerationBuildRevision.write(revision, GenerationBuildRevision.capture(options, dependencies));
|
||||||
return new Fixture(root, seal, dependency, dependencies, options);
|
return new Fixture(root, revision, dependency, dependencies, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void artifact(Path path, String content, String version, long timestamp) throws IOException {
|
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,
|
private record Fixture(Path root, Path revision, Path dependency, Map<String, Path> dependencies,
|
||||||
GenerationKernelSeal.CaptureOptions options) {
|
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"
|
writeEntry(output, "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\r\nMulti-Release: true\r\n\r\n"
|
||||||
.getBytes(StandardCharsets.UTF_8));
|
.getBytes(StandardCharsets.UTF_8));
|
||||||
writeEntry(output, "JarCompactorTest.class", classResource.readAllBytes());
|
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));
|
writeEntry(output, "example/caf\u00e9.txt", "unicode name".getBytes(StandardCharsets.UTF_8));
|
||||||
for (int index = 0; index < 100; index++) {
|
for (int index = 0; index < 100; index++) {
|
||||||
writeEntry(output, "example/item-" + index + ".txt", ("value-" + index).getBytes(StandardCharsets.UTF_8));
|
writeEntry(output, "example/item-" + index + ".txt", ("value-" + index).getBytes(StandardCharsets.UTF_8));
|
||||||
|
|||||||
+1
-1
@@ -522,4 +522,4 @@ tasks.named('check').configure {
|
|||||||
dependsOn('bukkitPurityRatchet')
|
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.KList;
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
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.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -53,7 +58,7 @@ import java.util.regex.Pattern;
|
|||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class IrisDatapackCompiler {
|
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 INPUT_BUFFER_BYTES = 64 * 1024;
|
||||||
private static final int WORLD_PACK_SCAN_DEPTH = 8;
|
private static final int WORLD_PACK_SCAN_DEPTH = 8;
|
||||||
private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
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());
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +228,14 @@ public final class IrisDatapackCompiler {
|
|||||||
data.close();
|
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);
|
addBiomeTagRequirements(requirements, biomeTags);
|
||||||
return Map.copyOf(requirements);
|
return Map.copyOf(requirements);
|
||||||
}
|
}
|
||||||
@@ -494,11 +510,111 @@ public final class IrisDatapackCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight);
|
IrisDimension.writeShared(datapackRoots, height, adjustVanillaHeight);
|
||||||
|
installRetainedRegistrySources(packRoots, datapackRoots, fixer);
|
||||||
installLevelStemBindings(normalizedBindings, dimensions, datapackRoots);
|
installLevelStemBindings(normalizedBindings, dimensions, datapackRoots);
|
||||||
validateOutputs(datapackRoots, dimensionCount, normalizedBindings.size());
|
validateOutputs(datapackRoots, dimensionCount, normalizedBindings.size());
|
||||||
return new CompilationResult(packCount, dimensionCount, countBiomes(biomes));
|
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(
|
private static void installLevelStemBindings(
|
||||||
List<IrisGeneratorBinding> bindings,
|
List<IrisGeneratorBinding> bindings,
|
||||||
Map<String, List<DimensionCandidate>> dimensions,
|
Map<String, List<DimensionCandidate>> dimensions,
|
||||||
@@ -648,11 +764,9 @@ public final class IrisDatapackCompiler {
|
|||||||
&& (Files.exists(generationRoot, LinkOption.NOFOLLOW_LINKS)
|
&& (Files.exists(generationRoot, LinkOption.NOFOLLOW_LINKS)
|
||||||
|| Files.isSymbolicLink(generationRoot))) {
|
|| Files.isSymbolicLink(generationRoot))) {
|
||||||
GenerationHistory history = GenerationHistory.open(worldRoot);
|
GenerationHistory history = GenerationHistory.open(worldRoot);
|
||||||
for (GenerationEpoch epoch : history.manifest().epochs()) {
|
candidates.add(history.activePackRoot());
|
||||||
Path retainedPack = history.paths().packRoot(epoch.epochId());
|
if (history.pendingActivation().isPresent()) {
|
||||||
if (hasDimensions(retainedPack)) {
|
candidates.add(history.packRoot(history.pendingActivation().orElseThrow().activationId()));
|
||||||
candidates.add(retainedPack);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,25 +10,50 @@ public final class IrisStartupValidation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized void begin() {
|
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() {
|
public static synchronized void disable() {
|
||||||
snapshot = Snapshot.disabled();
|
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() {
|
public static synchronized void beginDatapackValidation() {
|
||||||
if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) {
|
if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) {
|
||||||
return;
|
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() {
|
public static synchronized void markDatapacksReady() {
|
||||||
if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) {
|
if (!snapshot.enforced() || snapshot.datapacks() == ValidationState.RESTART_REQUIRED) {
|
||||||
return;
|
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) {
|
public static synchronized void markDatapacksInvalid(String failure) {
|
||||||
@@ -40,7 +65,7 @@ public final class IrisStartupValidation {
|
|||||||
ValidationState.INVALID,
|
ValidationState.INVALID,
|
||||||
snapshot.packs(),
|
snapshot.packs(),
|
||||||
List.of(normalizeFailure(failure, "External datapack validation failed.")),
|
List.of(normalizeFailure(failure, "External datapack validation failed.")),
|
||||||
snapshot.packFailures());
|
snapshot.packFailures(), snapshot.runtime(), snapshot.runtimeFailure());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized void requireRestart(String reason) {
|
public static synchronized void requireRestart(String reason) {
|
||||||
@@ -52,14 +77,14 @@ public final class IrisStartupValidation {
|
|||||||
ValidationState.RESTART_REQUIRED,
|
ValidationState.RESTART_REQUIRED,
|
||||||
snapshot.packs(),
|
snapshot.packs(),
|
||||||
List.of(normalizeFailure(reason, "A restart is required to load validated datapacks.")),
|
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() {
|
public static synchronized void markPacksReady() {
|
||||||
if (!snapshot.enforced()) {
|
if (!snapshot.enforced()) {
|
||||||
return;
|
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) {
|
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."))
|
.map(failure -> normalizeFailure(failure, "Iris dimension-pack validation failed."))
|
||||||
.toList();
|
.toList();
|
||||||
snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.INVALID,
|
snapshot = new Snapshot(true, snapshot.datapacks(), ValidationState.INVALID,
|
||||||
snapshot.datapackFailures(), normalized);
|
snapshot.datapackFailures(), normalized, snapshot.runtime(), snapshot.runtimeFailure());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isReady() {
|
public static boolean isReady() {
|
||||||
@@ -92,6 +117,7 @@ public final class IrisStartupValidation {
|
|||||||
Snapshot current = snapshot;
|
Snapshot current = snapshot;
|
||||||
if (force
|
if (force
|
||||||
&& current.enforced()
|
&& current.enforced()
|
||||||
|
&& isRuntimeReady(current)
|
||||||
&& current.datapacks() == ValidationState.RESTART_REQUIRED
|
&& current.datapacks() == ValidationState.RESTART_REQUIRED
|
||||||
&& current.packs() == ValidationState.READY) {
|
&& current.packs() == ValidationState.READY) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -103,6 +129,11 @@ public final class IrisStartupValidation {
|
|||||||
if (!current.enforced() || isReady(current)) {
|
if (!current.enforced() || isReady(current)) {
|
||||||
return Optional.empty();
|
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) {
|
if (current.datapacks() == ValidationState.RESTART_REQUIRED) {
|
||||||
return Optional.of(firstFailure(current.datapackFailures(),
|
return Optional.of(firstFailure(current.datapackFailures(),
|
||||||
"Iris installed validated datapacks and requires a restart before the server is safe."));
|
"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() {
|
public static void requireWorldReplacementStagingReady() {
|
||||||
Snapshot current = snapshot;
|
Snapshot current = snapshot;
|
||||||
if (current.enforced()
|
if (current.enforced()
|
||||||
|
&& isRuntimeReady(current)
|
||||||
&& current.datapacks() == ValidationState.RESTART_REQUIRED
|
&& current.datapacks() == ValidationState.RESTART_REQUIRED
|
||||||
&& current.packs() == ValidationState.READY) {
|
&& current.packs() == ValidationState.READY) {
|
||||||
return;
|
return;
|
||||||
@@ -150,9 +182,14 @@ public final class IrisStartupValidation {
|
|||||||
return failures == null || failures.isEmpty() ? fallback : failures.getFirst();
|
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) {
|
private static boolean isReady(Snapshot current) {
|
||||||
return !current.enforced()
|
return !current.enforced()
|
||||||
|| current.datapacks() == ValidationState.READY
|
|| isRuntimeReady(current)
|
||||||
|
&& current.datapacks() == ValidationState.READY
|
||||||
&& current.packs() == ValidationState.READY;
|
&& current.packs() == ValidationState.READY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +206,9 @@ public final class IrisStartupValidation {
|
|||||||
ValidationState datapacks,
|
ValidationState datapacks,
|
||||||
ValidationState packs,
|
ValidationState packs,
|
||||||
List<String> datapackFailures,
|
List<String> datapackFailures,
|
||||||
List<String> packFailures
|
List<String> packFailures,
|
||||||
|
ValidationState runtime,
|
||||||
|
String runtimeFailure
|
||||||
) {
|
) {
|
||||||
Snapshot {
|
Snapshot {
|
||||||
datapackFailures = List.copyOf(datapackFailures);
|
datapackFailures = List.copyOf(datapackFailures);
|
||||||
@@ -177,7 +216,7 @@ public final class IrisStartupValidation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Snapshot disabled() {
|
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) {
|
private ImageMapStudioGUI(Engine engine) {
|
||||||
this.engine = Objects.requireNonNull(engine, "Image Map Studio 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.dimensionKey = engine.getDimension().getLoadKey();
|
||||||
this.boundary = engine.getDimension().getWorldBoundary() == null
|
this.boundary = engine.getDimension().getWorldBoundary() == null
|
||||||
? null
|
? null
|
||||||
@@ -163,7 +163,6 @@ public final class ImageMapStudioGUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void reloadActiveEngine(Engine engine) {
|
static void reloadActiveEngine(Engine engine) {
|
||||||
engine.getData().getImageMapLoader().clearCache();
|
|
||||||
engine.hotloadSilently();
|
engine.hotloadSilently();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,9 +430,14 @@ public final class ImageMapStudioGUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refreshPresets() {
|
private void refreshPresets() {
|
||||||
String[] keys = engine.getData().getImageMapLoader().getPossibleKeys();
|
IrisData data = IrisData.openRuntime(packFolder);
|
||||||
Arrays.sort(keys);
|
try {
|
||||||
presetPicker.setModel(new DefaultComboBoxModel<>(keys));
|
String[] keys = data.getImageMapLoader().getPossibleKeys();
|
||||||
|
Arrays.sort(keys);
|
||||||
|
presetPicker.setModel(new DefaultComboBoxModel<>(keys));
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadPreset(String key) {
|
private void loadPreset(String key) {
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ final class BukkitPublicBackend implements WorldLifecycleBackend {
|
|||||||
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
|
WorldLifecycleStaging.stageStemGenerator(request.worldName(), request.generator());
|
||||||
}
|
}
|
||||||
|
|
||||||
INMS.get().ensureServerLevelInjection();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
INMS.get().ensureServerLevelInjection();
|
||||||
World world = creator.createWorld();
|
World world = creator.createWorld();
|
||||||
return CompletableFuture.completedFuture(world);
|
return CompletableFuture.completedFuture(world);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
|||||||
@@ -189,6 +189,26 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
return Optional.ofNullable(dataLoaders.get(dataFolder));
|
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) {
|
public String customBiomeResourceKey(IrisDimension dimension, IrisBiomeCustom customBiome) {
|
||||||
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||||
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
|
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package art.arcane.iris.core.nms;
|
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.core.datapack.DatapackStructureScopeIndex;
|
||||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||||
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
|
import art.arcane.iris.core.lifecycle.WorldLifecycleCaller;
|
||||||
@@ -57,9 +59,19 @@ import org.bukkit.inventory.ItemStack;
|
|||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
public interface INMSBinding {
|
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() {
|
default PlatformGenerationRegistry generationRegistry() {
|
||||||
throw new UnsupportedOperationException(
|
throw new UnsupportedOperationException(
|
||||||
"The active NMS binding does not expose generation registry definitions."
|
"The active NMS binding does not expose generation registry definitions."
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ final class PackStructurePlacementValidator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (hasNativeStructures) {
|
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(
|
validateNativeStructures(
|
||||||
placementPath, nativeStructures,
|
placementPath, nativeStructures,
|
||||||
registries.structures(), registries.jigsaws(),
|
registries.structures(), registries.jigsaws(),
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ public class IrisPregenerator {
|
|||||||
// fires — leaving a phantom job that suppresses spawns and blocks future pregens.
|
// fires — leaving a phantom job that suppresses spawns and blocks future pregens.
|
||||||
try {
|
try {
|
||||||
init();
|
init();
|
||||||
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
|
totalChunks.set(task.chunkCount());
|
||||||
long startedAt = M.ms();
|
long startedAt = M.ms();
|
||||||
startTime.set(startedAt);
|
startTime.set(startedAt);
|
||||||
rateTracker.reset(startedAt, generated.get());
|
rateTracker.reset(startedAt, generated.get());
|
||||||
@@ -211,7 +211,6 @@ public class IrisPregenerator {
|
|||||||
historyLastCached.set(cached.get());
|
historyLastCached.set(cached.get());
|
||||||
historyLastMillis.set(startedAt);
|
historyLastMillis.set(startedAt);
|
||||||
ticker.start();
|
ticker.start();
|
||||||
checkRegions();
|
|
||||||
int[] regionBounds = task.regionBounds();
|
int[] regionBounds = task.regionBounds();
|
||||||
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
|
generator.onRegionBounds(regionBounds[0], regionBounds[1], regionBounds[2], regionBounds[3]);
|
||||||
generator.onPregenStart(task.getCenter().getX(), task.getCenter().getZ());
|
generator.onPregenStart(task.getCenter().getX(), task.getCenter().getZ());
|
||||||
@@ -280,10 +279,6 @@ public class IrisPregenerator {
|
|||||||
+ "] chunks/s";
|
+ "] chunks/s";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkRegions() {
|
|
||||||
task.iterateRegions(this::checkRegion);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
private void init() {
|
||||||
generator.init();
|
generator.init();
|
||||||
}
|
}
|
||||||
@@ -413,9 +408,6 @@ public class IrisPregenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generatedRegions.add(pos);
|
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();
|
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() {
|
public long getFailedChunks() {
|
||||||
return failed.get();
|
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.core.IrisSettings;
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
import art.arcane.iris.engine.IrisComplex;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||||
import art.arcane.iris.util.project.stream.ProceduralStream;
|
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.CachedDoubleStream2D;
|
||||||
import art.arcane.iris.util.project.stream.utility.CachedStream2D;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public final class PregenPerformanceProfile {
|
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() {
|
private PregenPerformanceProfile() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +37,6 @@ public final class PregenPerformanceProfile {
|
|||||||
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
|
IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance();
|
||||||
int previousNoiseCacheSize = performance.getNoiseCacheSize();
|
int previousNoiseCacheSize = performance.getNoiseCacheSize();
|
||||||
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
|
int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, 4_096);
|
||||||
boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast");
|
|
||||||
boolean changed = false;
|
boolean changed = false;
|
||||||
|
|
||||||
if (targetNoiseCacheSize != previousNoiseCacheSize) {
|
if (targetNoiseCacheSize != previousNoiseCacheSize) {
|
||||||
@@ -52,73 +44,41 @@ public final class PregenPerformanceProfile {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fastCacheEnabledBefore) {
|
|
||||||
System.setProperty("iris.cache.fast", "true");
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(Runtime.getRuntime().availableProcessors()))) {
|
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(Runtime.getRuntime().availableProcessors()))) {
|
||||||
changed = true;
|
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;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void apply(Engine engine) {
|
public static void apply(Engine engine) {
|
||||||
apply();
|
apply();
|
||||||
if (!prepareNoiseCacheInPlace(engine)) {
|
if (engine != null) {
|
||||||
engine.hotloadComplex();
|
prepareNoiseCacheInPlace(engine);
|
||||||
logApplied();
|
IrisLogging.info("Pregen profile applied: noiseCacheSize="
|
||||||
|
+ IrisSettings.get().getPerformance().getNoiseCacheSize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void applyToGenerator(PlatformChunkGenerator generator) {
|
public static void applyToGenerator(PlatformChunkGenerator generator) {
|
||||||
apply();
|
apply(generator == null ? null : generator.getEngine());
|
||||||
Engine engine = generator == null ? null : generator.getEngine();
|
|
||||||
if (!prepareNoiseCacheInPlace(engine)) {
|
|
||||||
generator.hotloadComplexAsync(HOTLOAD_ACQUISITION_TIMEOUT_SECONDS, TimeUnit.SECONDS).join();
|
|
||||||
logApplied();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int pregenBurstParallelism(int availableProcessors) {
|
static int pregenBurstParallelism(int availableProcessors) {
|
||||||
return Math.max(4, availableProcessors * 2);
|
return Math.max(4, availableProcessors * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean prepareNoiseCacheInPlace(Engine engine) {
|
private static void prepareNoiseCacheInPlace(Engine engine) {
|
||||||
if (engine == null) {
|
IrisComplex complex = Objects.requireNonNull(engine.getComplex(), "Pregeneration requires an active biome complex");
|
||||||
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;
|
|
||||||
}
|
|
||||||
int configuredChunks = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
int configuredChunks = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
||||||
if (!resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks)) {
|
resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks);
|
||||||
return false;
|
resizeNoiseCache(complex.getRawHeightStream(), configuredChunks);
|
||||||
}
|
|
||||||
return resizeNoiseCache(complex.getHeightStream(), configuredChunks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean resizeNoiseCache(ProceduralStream<Double> stream, int configuredChunks) {
|
private static void resizeNoiseCache(ProceduralStream<Double> stream, int configuredChunks) {
|
||||||
if (stream instanceof CachedDoubleStream2D cachedStream) {
|
if (!(stream instanceof CachedDoubleStream2D cachedStream)) {
|
||||||
cachedStream.setMaximumChunks(configuredChunks);
|
throw new IllegalStateException("Pregeneration requires a mutable double terrain cache");
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if (!(stream instanceof MeteredCache cache)) {
|
cachedStream.setMaximumChunks(configuredChunks);
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,6 +215,11 @@ public class PregenTask {
|
|||||||
return new int[]{bound.minX(), bound.minZ(), bound.maxX(), bound.maxZ()};
|
return new int[]{bound.minX(), bound.minZ(), bound.maxX(), bound.maxZ()};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long chunkCount() {
|
||||||
|
Bound bound = bounds.chunk();
|
||||||
|
return (long) bound.sizeX() * bound.sizeZ();
|
||||||
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface InterleavedChunkConsumer {
|
public interface InterleavedChunkConsumer {
|
||||||
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
|
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
|
||||||
|
|||||||
+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.PregenTask;
|
||||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||||
import art.arcane.iris.core.pregenerator.cache.PregenCache;
|
import art.arcane.iris.core.pregenerator.cache.PregenCache;
|
||||||
|
import art.arcane.iris.core.pregenerator.cache.PregenSavedChunkStatus;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
|
|
||||||
public class CachedPregenMethod implements PregeneratorMethod {
|
public class CachedPregenMethod implements PregeneratorMethod {
|
||||||
private final PregeneratorMethod method;
|
private final PregeneratorMethod method;
|
||||||
private final PregenCache cache;
|
private final PregenCache cache;
|
||||||
private final PregenTask task;
|
private final PregenTask task;
|
||||||
|
private final PregenSavedChunkStatus savedStatus;
|
||||||
private volatile PregenListener wrappedSource;
|
private volatile PregenListener wrappedSource;
|
||||||
private volatile PregenListener wrappedListener;
|
private volatile PregenListener wrappedListener;
|
||||||
|
|
||||||
public CachedPregenMethod(PregeneratorMethod method, PregenCache cache, PregenTask task) {
|
public CachedPregenMethod(Configuration configuration) {
|
||||||
this.method = method;
|
this.method = configuration.method();
|
||||||
this.cache = cache.sync();
|
this.cache = configuration.cache().sync();
|
||||||
this.task = task;
|
this.task = configuration.task();
|
||||||
|
this.savedStatus = configuration.savedStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -26,7 +29,6 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
cache.write();
|
|
||||||
method.close();
|
method.close();
|
||||||
cache.write();
|
cache.write();
|
||||||
}
|
}
|
||||||
@@ -39,12 +41,12 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsRegions(int x, int z, PregenListener listener) {
|
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
|
@Override
|
||||||
public String getMethod(int x, int z) {
|
public String getMethod(int x, int z) {
|
||||||
if (cache.isRegionCached(x, z)) {
|
if (isSavedRegionCached(x, z)) {
|
||||||
return "Cached";
|
return "Cached";
|
||||||
}
|
}
|
||||||
return method.getMethod(x, z);
|
return method.getMethod(x, z);
|
||||||
@@ -52,7 +54,7 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateRegion(int x, int z, PregenListener listener) {
|
public void generateRegion(int x, int z, PregenListener listener) {
|
||||||
if (cache.isRegionCached(x, z)) {
|
if (isSavedRegionCached(x, z)) {
|
||||||
listener.onRegionGenerated(x, z);
|
listener.onRegionGenerated(x, z);
|
||||||
task.iterateChunks(x, z, (cX, cZ) -> {
|
task.iterateChunks(x, z, (cX, cZ) -> {
|
||||||
listener.onChunkGenerated(cX, cZ, true);
|
listener.onChunkGenerated(cX, cZ, true);
|
||||||
@@ -60,13 +62,16 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
method.generateRegion(x, z, listener);
|
if (!method.supportsRegions(x, z, listener)) {
|
||||||
cache.cacheRegion(x, z);
|
task.iterateChunks(x, z, (chunkX, chunkZ) -> generateChunk(chunkX, chunkZ, listener));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
method.generateRegion(x, z, cachingListener(listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generateChunk(int x, int z, PregenListener listener) {
|
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.onChunkGenerated(x, z, true);
|
||||||
listener.onChunkCleaned(x, z);
|
listener.onChunkCleaned(x, z);
|
||||||
return;
|
return;
|
||||||
@@ -74,6 +79,20 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
|||||||
method.generateChunk(x, z, cachingListener(listener));
|
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) {
|
private PregenListener cachingListener(PregenListener listener) {
|
||||||
PregenListener source = wrappedSource;
|
PregenListener source = wrappedSource;
|
||||||
PregenListener wrapped = wrappedListener;
|
PregenListener wrapped = wrappedListener;
|
||||||
@@ -185,6 +204,10 @@ public class CachedPregenMethod implements PregeneratorMethod {
|
|||||||
method.onRegionSubmitted(regionX, regionZ);
|
method.onRegionSubmitted(regionX, regionZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record Configuration(PregeneratorMethod method, PregenCache cache, PregenTask task,
|
||||||
|
PregenSavedChunkStatus savedStatus) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mantle getMantle() {
|
public Mantle getMantle() {
|
||||||
return method.getMantle();
|
return method.getMantle();
|
||||||
|
|||||||
@@ -952,7 +952,7 @@ public final class StudioOpenCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean prepareGeneratorState() {
|
public boolean prepareGeneratorState() {
|
||||||
return this != JIGSAW;
|
return this == STANDARD || this == FORCED_STANDARD;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisCreator.DatapackPreparation datapackPreparation() {
|
public IrisCreator.DatapackPreparation datapackPreparation() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.core.safeguard;
|
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.Diagnostic;
|
||||||
import art.arcane.iris.core.safeguard.task.Task;
|
import art.arcane.iris.core.safeguard.task.Task;
|
||||||
import art.arcane.iris.core.safeguard.task.Tasks;
|
import art.arcane.iris.core.safeguard.task.Tasks;
|
||||||
@@ -24,6 +25,7 @@ public final class IrisSafeguard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void execute() {
|
public static void execute() {
|
||||||
|
IrisStartupValidation.beginRuntimeValidation();
|
||||||
List<Task> tasks = Tasks.getTasks();
|
List<Task> tasks = Tasks.getTasks();
|
||||||
LinkedHashMap<Task, ValueWithDiagnostics<Mode>> resultValues = new LinkedHashMap<>(tasks.size());
|
LinkedHashMap<Task, ValueWithDiagnostics<Mode>> resultValues = new LinkedHashMap<>(tasks.size());
|
||||||
LinkedHashMap<String, String> contextValues = new LinkedHashMap<>(tasks.size());
|
LinkedHashMap<String, String> contextValues = new LinkedHashMap<>(tasks.size());
|
||||||
@@ -36,9 +38,13 @@ public final class IrisSafeguard {
|
|||||||
try {
|
try {
|
||||||
result = task.run();
|
result = task.run();
|
||||||
} catch (Throwable e) {
|
} 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);
|
IrisLogging.reportError(e);
|
||||||
result = new ValueWithDiagnostics<>(
|
result = new ValueWithDiagnostics<>(
|
||||||
Mode.WARNING,
|
injectionFailure ? Mode.UNSTABLE : Mode.WARNING,
|
||||||
new Diagnostic(Diagnostic.Logger.ERROR, "Error while running task " + task.getId(), e)
|
new Diagnostic(Diagnostic.Logger.ERROR, "Error while running task " + task.getId(), e)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.core.safeguard.task;
|
package art.arcane.iris.core.safeguard.task;
|
||||||
|
|
||||||
import art.arcane.iris.BuildConstants;
|
import art.arcane.iris.BuildConstants;
|
||||||
|
import art.arcane.iris.core.IrisStartupValidation;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
import art.arcane.iris.core.IrisWorlds;
|
import art.arcane.iris.core.IrisWorlds;
|
||||||
import art.arcane.iris.core.IrisWorldStorage;
|
import art.arcane.iris.core.IrisWorldStorage;
|
||||||
@@ -119,18 +120,22 @@ public final class Tasks {
|
|||||||
|
|
||||||
private static final Task INJECTION = Task.of("injection", () -> {
|
private static final Task INJECTION = Task.of("injection", () -> {
|
||||||
if (!Agent.install()) {
|
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,
|
return withDiagnostics(Mode.UNSTABLE,
|
||||||
Diagnostic.Logger.ERROR.create("Java Agent"),
|
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("- Add -javaagent:" + Agent.AGENT_JAR.getPath() + " before -jar in your startup command and restart the server."),
|
||||||
Diagnostic.Logger.ERROR.create("- or add the jvm argument -javaagent:" + Agent.AGENT_JAR.getPath()));
|
Diagnostic.Logger.ERROR.create("- Dynamic attachment requires -XX:+EnableDynamicAgentLoading and a host that permits JVM attachment."));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!INMS.get().injectBukkit()) {
|
if (!INMS.get().injectBukkit()) {
|
||||||
|
IrisStartupValidation.markRuntimeInvalid("Iris runtime injection failed. Resolve the startup errors and restart the server.");
|
||||||
return withDiagnostics(Mode.UNSTABLE,
|
return withDiagnostics(Mode.UNSTABLE,
|
||||||
Diagnostic.Logger.ERROR.create("Code Injection"),
|
Diagnostic.Logger.ERROR.create("Code Injection"),
|
||||||
Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support"));
|
Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IrisStartupValidation.markRuntimeReady();
|
||||||
return withDiagnostics(Mode.STABLE);
|
return withDiagnostics(Mode.STABLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public class ObjectStudioSaveService implements IrisService {
|
|||||||
|
|
||||||
Map<String, File> objectsDirs = new ConcurrentHashMap<>();
|
Map<String, File> objectsDirs = new ConcurrentHashMap<>();
|
||||||
for (Map.Entry<String, IrisData> e : sources.entrySet()) {
|
for (Map.Entry<String, IrisData> e : sources.entrySet()) {
|
||||||
File dir = resolveObjectsDir(e.getValue());
|
File dir = resolveObjectsDir(e.getValue(), engine);
|
||||||
if (dir != null) {
|
if (dir != null) {
|
||||||
objectsDirs.put(e.getKey(), dir);
|
objectsDirs.put(e.getKey(), dir);
|
||||||
}
|
}
|
||||||
@@ -200,9 +200,13 @@ public class ObjectStudioSaveService implements IrisService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static File resolveObjectsDir(IrisData data) {
|
static File resolveObjectsDir(IrisData data, Engine engine) {
|
||||||
File root = data.getDataFolder();
|
File root = data.getDataFolder();
|
||||||
if (root == null) return null;
|
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");
|
File objects = new File(root, "objects");
|
||||||
if (!objects.exists()) {
|
if (!objects.exists()) {
|
||||||
objects.mkdirs();
|
objects.mkdirs();
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ package art.arcane.iris.core.structure;
|
|||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
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 com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -38,8 +36,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
|
|
||||||
public final class StructureIndexService {
|
public final class StructureIndexService {
|
||||||
private static final String INDEX_PATH = ".iris/structure-index.json";
|
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 static final Set<String> GENERATED = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
private StructureIndexService() {
|
private StructureIndexService() {
|
||||||
@@ -66,7 +62,6 @@ public final class StructureIndexService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static File write(IrisData data) {
|
public static File write(IrisData data) {
|
||||||
removeLegacyIndex(data);
|
|
||||||
List<String> structures = IrisPlatforms.get().structureHooks().structureKeys();
|
List<String> structures = IrisPlatforms.get().structureHooks().structureKeys();
|
||||||
List<String> sets = IrisPlatforms.get().structureHooks().structureSetKeys();
|
List<String> sets = IrisPlatforms.get().structureHooks().structureSetKeys();
|
||||||
|
|
||||||
@@ -93,7 +88,6 @@ public final class StructureIndexService {
|
|||||||
List<String> iris = new ArrayList<>();
|
List<String> iris = new ArrayList<>();
|
||||||
if (data.getStructureLoader() != null) {
|
if (data.getStructureLoader() != null) {
|
||||||
Collections.addAll(iris, data.getStructureLoader().getPossibleKeys());
|
Collections.addAll(iris, data.getStructureLoader().getPossibleKeys());
|
||||||
iris.removeIf(LEGACY_INDEX_KEY::equals);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Collections.sort(vanilla);
|
Collections.sort(vanilla);
|
||||||
@@ -138,22 +132,4 @@ public final class StructureIndexService {
|
|||||||
return new File(dataFolder, INDEX_PATH);
|
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";
|
private String name = "irisworld";
|
||||||
/**
|
/**
|
||||||
* Studio mode makes the engine hotloadable and uses the dimension in
|
* Studio mode snapshots edits from your Iris/packs folder into generation history.
|
||||||
* your Iris/packs folder instead of copying the dimension files into
|
* Existing chunks retain their generation while new chunks use accepted edits.
|
||||||
* the world itself. Studio worlds are deleted when they are unloaded.
|
* Studio worlds are deleted when they are unloaded.
|
||||||
*/
|
*/
|
||||||
private boolean studio = false;
|
private boolean studio = false;
|
||||||
/**
|
/**
|
||||||
@@ -260,18 +260,15 @@ public class IrisCreator {
|
|||||||
+ "Restart the server, then run the command again.");
|
+ "Restart the server, then run the command again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisDimension installedDimension = resolvedDimension;
|
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
|
||||||
if (!studio() || benchmark) {
|
StudioSVC studioService = IrisServices.get(StudioSVC.class);
|
||||||
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
|
IrisDimension installedDimension = benchmark && !studio
|
||||||
StudioSVC studioService = IrisServices.get(StudioSVC.class);
|
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
|
||||||
installedDimension = benchmark
|
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
|
||||||
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
|
if (installedDimension == null) {
|
||||||
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
|
throw new IrisException("Failed to install dimension pack for " + dimension());
|
||||||
if (installedDimension == null) {
|
|
||||||
throw new IrisException("Failed to install dimension pack for " + dimension());
|
|
||||||
}
|
|
||||||
dimension = installedDimension.getLoadKey();
|
|
||||||
}
|
}
|
||||||
|
dimension = installedDimension.getLoadKey();
|
||||||
if (studio()) {
|
if (studio()) {
|
||||||
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
|
IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen());
|
||||||
IrisLogging.debug("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
|
IrisLogging.debug("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT)
|
||||||
@@ -285,6 +282,7 @@ public class IrisCreator {
|
|||||||
long generatorPrepareStart = System.nanoTime();
|
long generatorPrepareStart = System.nanoTime();
|
||||||
WorldCreator wc = new IrisWorldCreator()
|
WorldCreator wc = new IrisWorldCreator()
|
||||||
.dimension(installedDimension)
|
.dimension(installedDimension)
|
||||||
|
.studioPackSource(resolvedDimension.getLoader().getDataFolder())
|
||||||
.name(name)
|
.name(name)
|
||||||
.seed(seed)
|
.seed(seed)
|
||||||
.studio(studio)
|
.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.PregenTask;
|
||||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||||
import art.arcane.iris.core.pregenerator.cache.PregenCache;
|
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.PackDirectoryResolver;
|
||||||
import art.arcane.iris.core.pack.PackDownloader;
|
import art.arcane.iris.core.pack.PackDownloader;
|
||||||
import art.arcane.iris.core.project.IrisProject;
|
import art.arcane.iris.core.project.IrisProject;
|
||||||
@@ -316,7 +317,8 @@ public class IrisToolbelt {
|
|||||||
useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA;
|
useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA;
|
||||||
}
|
}
|
||||||
PregeneratorMethod activeMethod = useCachedWrapper
|
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;
|
: method;
|
||||||
return new PregeneratorJob(new PregeneratorJob.Configuration(
|
return new PregeneratorJob(new PregeneratorJob.Configuration(
|
||||||
task,
|
task,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import org.bukkit.generator.ChunkGenerator;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class IrisWorldCreator {
|
public class IrisWorldCreator {
|
||||||
private String name;
|
private String name;
|
||||||
@@ -41,6 +42,7 @@ public class IrisWorldCreator {
|
|||||||
private IrisDimension dimension;
|
private IrisDimension dimension;
|
||||||
private long seed = 1337;
|
private long seed = 1337;
|
||||||
private boolean persistent;
|
private boolean persistent;
|
||||||
|
private File studioPackSource;
|
||||||
|
|
||||||
public IrisWorldCreator() {
|
public IrisWorldCreator() {
|
||||||
|
|
||||||
@@ -83,6 +85,11 @@ public class IrisWorldCreator {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IrisWorldCreator studioPackSource(File source) {
|
||||||
|
this.studioPackSource = Objects.requireNonNull(source, "Studio authoring source");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public WorldCreator create() {
|
public WorldCreator create() {
|
||||||
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
||||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(name);
|
NamespacedKey worldKey = IrisWorldStorage.keyFromName(name);
|
||||||
@@ -102,13 +109,13 @@ public class IrisWorldCreator {
|
|||||||
.seed(seed)
|
.seed(seed)
|
||||||
.worldFolder(worldFolder)
|
.worldFolder(worldFolder)
|
||||||
.build();
|
.build();
|
||||||
GenerationHistory generationHistory = persistent
|
GenerationHistory generationHistory = persistent || studio
|
||||||
? requireGenerationHistory(w.worldFolder(), seed)
|
? requireGenerationHistory(w.worldFolder(), seed)
|
||||||
: null;
|
: null;
|
||||||
File packRoot = generationHistory != null
|
File packRoot = studio
|
||||||
|
? Objects.requireNonNull(studioPackSource, "Studio authoring source")
|
||||||
|
: generationHistory != null
|
||||||
? requireActivePack(generationHistory)
|
? requireActivePack(generationHistory)
|
||||||
: studio
|
|
||||||
? dim.getLoader().getDataFolder()
|
|
||||||
: new File(w.worldFolder(), "iris/pack");
|
: new File(w.worldFolder(), "iris/pack");
|
||||||
ChunkGenerator g = new BukkitChunkGenerator(
|
ChunkGenerator g = new BukkitChunkGenerator(
|
||||||
w,
|
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;
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
||||||
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
|
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
|
||||||
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
|
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.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.StructureReachability;
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
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.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.util.project.context.IrisContext;
|
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;
|
import static art.arcane.iris.engine.EngineShutdownSequence.runCleanup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs the two supported live reload transitions for an {@link IrisEngine}: a full pack hotload
|
* Reloads mutable engine runtimes and publishes immutable Studio generation activations.
|
||||||
* and a biome-complex-only rebuild. Both run entirely under the engine lifecycle lock, seal and
|
* Studio transitions drain sessions outside the lifecycle lock before freezing chunk ownership.
|
||||||
* drain generation before swapping anything, and roll the previous runtime back on failure.
|
* Failures before publication preserve the previous runtime; durable publication failures stop generation.
|
||||||
*/
|
*/
|
||||||
final class EngineHotloader {
|
final class EngineHotloader {
|
||||||
private final IrisEngine engine;
|
private final IrisEngine engine;
|
||||||
|
private final Object studioTransitionLock = new Object();
|
||||||
|
|
||||||
EngineHotloader(IrisEngine engine) {
|
EngineHotloader(IrisEngine engine) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
@@ -51,6 +57,10 @@ final class EngineHotloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void hotloadComplex() {
|
void hotloadComplex() {
|
||||||
|
if (engine.isStudio() && engine.getGenerationHistoryRuntimeRouter().isPresent()) {
|
||||||
|
hotloadSilently();
|
||||||
|
return;
|
||||||
|
}
|
||||||
synchronized (engine.lifecycleLock) {
|
synchronized (engine.lifecycleLock) {
|
||||||
engine.requireRunning("rebuild the biome complex");
|
engine.requireRunning("rebuild the biome complex");
|
||||||
requireMutableGenerationRuntime("rebuild the biome complex");
|
requireMutableGenerationRuntime("rebuild the biome complex");
|
||||||
@@ -114,6 +124,11 @@ final class EngineHotloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void hotloadSilently() {
|
void hotloadSilently() {
|
||||||
|
GenerationHistoryRuntimeRouter router = engine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||||
|
if (engine.isStudio() && router != null) {
|
||||||
|
hotloadStudio(router);
|
||||||
|
return;
|
||||||
|
}
|
||||||
synchronized (engine.lifecycleLock) {
|
synchronized (engine.lifecycleLock) {
|
||||||
engine.requireRunning("hotload");
|
engine.requireRunning("hotload");
|
||||||
requireMutableGenerationRuntime("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) {
|
private void requireMutableGenerationRuntime(String operation) {
|
||||||
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
|
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
|
||||||
throw new IllegalStateException("Cannot " + operation
|
throw new IllegalStateException("Cannot " + operation
|
||||||
@@ -210,7 +314,7 @@ final class EngineHotloader {
|
|||||||
if (protocolServer == null) {
|
if (protocolServer == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String packKey = engine.getData().getDataFolder().getName();
|
String packKey = engine.getPackSource().getFileName().toString();
|
||||||
protocolServer.broadcastStudioHotload(packKey, 0, failed, message);
|
protocolServer.broadcastStudioHotload(packKey, 0, failed, message);
|
||||||
} catch (Throwable broadcastFailure) {
|
} catch (Throwable broadcastFailure) {
|
||||||
IrisLogging.error("Iris studio hotload broadcast failed: " + broadcastFailure.getClass().getSimpleName()
|
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());
|
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) {
|
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
|
||||||
schedulePackHash(engineRuntime.generation());
|
schedulePackHash(engineRuntime.generation());
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.engine;
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
|
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||||
@@ -25,6 +26,7 @@ import art.arcane.iris.engine.data.cache.Cache;
|
|||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.history.GenerationBlend;
|
import art.arcane.iris.engine.history.GenerationBlend;
|
||||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
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.HydrologyColumnLayer;
|
||||||
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
|
||||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||||
@@ -38,6 +40,9 @@ import art.arcane.iris.engine.object.IrisBiome;
|
|||||||
import art.arcane.iris.engine.object.IrisDecorationPart;
|
import art.arcane.iris.engine.object.IrisDecorationPart;
|
||||||
import art.arcane.iris.engine.object.IrisDecorator;
|
import art.arcane.iris.engine.object.IrisDecorator;
|
||||||
import art.arcane.iris.engine.object.IrisGenerator;
|
import art.arcane.iris.engine.object.IrisGenerator;
|
||||||
|
import art.arcane.iris.engine.object.IrisGeneratorStyle;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import art.arcane.iris.engine.object.IrisInterpolator;
|
import art.arcane.iris.engine.object.IrisInterpolator;
|
||||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
@@ -67,6 +72,7 @@ import lombok.Getter;
|
|||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -79,6 +85,7 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalInt;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
@@ -133,6 +140,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
private ProceduralStream<IrisBiome> seaBiomeStream;
|
private ProceduralStream<IrisBiome> seaBiomeStream;
|
||||||
private ProceduralStream<IrisBiome> shoreBiomeStream;
|
private ProceduralStream<IrisBiome> shoreBiomeStream;
|
||||||
private ProceduralStream<IrisBiome> baseBiomeStream;
|
private ProceduralStream<IrisBiome> baseBiomeStream;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private final transient boolean reuseNaturalBaseBiome;
|
||||||
private ProceduralStream<UUID> baseBiomeIDStream;
|
private ProceduralStream<UUID> baseBiomeIDStream;
|
||||||
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
|
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
|
||||||
private ProceduralStream<IrisBiome> unblendedNaturalTrueBiomeStream;
|
private ProceduralStream<IrisBiome> unblendedNaturalTrueBiomeStream;
|
||||||
@@ -140,6 +149,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
|
private ProceduralStream<PlatformBiome> trueBiomeDerivativeStream;
|
||||||
private ProceduralStream<Double> naturalHeightStream;
|
private ProceduralStream<Double> naturalHeightStream;
|
||||||
private ProceduralStream<Double> unblendedNaturalHeightStream;
|
private ProceduralStream<Double> unblendedNaturalHeightStream;
|
||||||
|
private final ResolvedTerrainProvider resolvedTerrain;
|
||||||
|
private ProceduralStream<Double> placementHeightStream;
|
||||||
private ProceduralStream<Double> heightStream;
|
private ProceduralStream<Double> heightStream;
|
||||||
private ProceduralStream<Integer> roundedHeighteightStream;
|
private ProceduralStream<Integer> roundedHeighteightStream;
|
||||||
private ProceduralStream<Double> maxHeightStream;
|
private ProceduralStream<Double> maxHeightStream;
|
||||||
@@ -188,6 +199,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
|
|
||||||
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan) {
|
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan) {
|
||||||
this.transitionGenerationPlan = transitionGenerationPlan;
|
this.transitionGenerationPlan = transitionGenerationPlan;
|
||||||
|
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
|
||||||
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize());
|
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize());
|
||||||
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
|
IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE);
|
||||||
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
|
UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes());
|
||||||
@@ -228,6 +240,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
|
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
|
||||||
}
|
}
|
||||||
|
reuseNaturalBaseBiome = hasFixedNaturalBiomeNoise(engine.getDimension(), generatorBiomes);
|
||||||
inferredBiomeStreams = compileInferredBiomeStreams(
|
inferredBiomeStreams = compileInferredBiomeStreams(
|
||||||
preparedRegions,
|
preparedRegions,
|
||||||
(region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome)
|
(region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome)
|
||||||
@@ -314,11 +327,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
(x, z) -> sampleUnblendedNaturalTerrainHeight(engine, x, z),
|
(x, z) -> sampleUnblendedNaturalTerrainHeight(engine, x, z),
|
||||||
Interpolated.DOUBLE
|
Interpolated.DOUBLE
|
||||||
).cache2DDouble("unblendedNaturalHeightStream", engine, cacheSize);
|
).cache2DDouble("unblendedNaturalHeightStream", engine, cacheSize);
|
||||||
naturalHeightStream = transitionGenerationPlan == null ? unblendedNaturalHeightStream
|
naturalHeightStream = unblendedNaturalHeightStream;
|
||||||
: unblendedNaturalHeightStream.convertAware2D((newHeight, x, z) -> blendNaturalTerrainHeight(
|
|
||||||
newHeight,
|
|
||||||
transitionGenerationPlan.terrainSampleAt(blockCoordinate(x), blockCoordinate(z))))
|
|
||||||
.cache2DDouble("naturalHeightStream", engine, cacheSize);
|
|
||||||
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
naturalTrueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||||
b -> focusBiome))
|
b -> focusBiome))
|
||||||
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
|
.cache2D("naturalTrueBiomeStream-focus", engine, cacheSize) : naturalHeightStream
|
||||||
@@ -355,10 +364,11 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
|
heightStream = ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z))
|
||||||
.cache2DDouble("heightStream", engine, cacheSize);
|
.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();
|
.round();
|
||||||
slopeStream = heightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
slopeStream = placementHeightStream.contextInjecting(engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
.slope(3).cache2DDouble("slopeStream", engine, cacheSize);
|
.slope(3);
|
||||||
naturalSlopeStream = naturalHeightStream.slope(3).cache2DDouble("naturalSlopeStream", engine, cacheSize);
|
naturalSlopeStream = naturalHeightStream.slope(3).cache2DDouble("naturalSlopeStream", engine, cacheSize);
|
||||||
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
trueBiomeStream = focusBiome != null ? ProceduralStream.of((x, y) -> focusBiome, Interpolated.of(a -> 0D,
|
||||||
b -> focusBiome))
|
b -> focusBiome))
|
||||||
@@ -480,14 +490,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
|
private double sampleNaturalTerrainHeight(Engine engine, double x, double z) {
|
||||||
double newHeight = sampleUnblendedNaturalTerrainHeight(engine, x, z);
|
return sampleUnblendedNaturalTerrainHeight(engine, x, z);
|
||||||
if (transitionGenerationPlan == null) {
|
|
||||||
return newHeight;
|
|
||||||
}
|
|
||||||
TransitionGenerationPlan.TerrainSample sample = transitionGenerationPlan.terrainSampleAt(
|
|
||||||
blockCoordinate(x),
|
|
||||||
blockCoordinate(z));
|
|
||||||
return blendNaturalTerrainHeight(newHeight, sample);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double sampleUnblendedNaturalTerrainHeight(Engine engine, double x, double z) {
|
private double sampleUnblendedNaturalTerrainHeight(Engine engine, double x, double z) {
|
||||||
@@ -495,20 +498,6 @@ public class IrisComplex implements DataProvider {
|
|||||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
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) {
|
static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
|
||||||
double deltaX = easternHeight - naturalHeight;
|
double deltaX = easternHeight - naturalHeight;
|
||||||
double deltaZ = southernHeight - naturalHeight;
|
double deltaZ = southernHeight - naturalHeight;
|
||||||
@@ -519,16 +508,19 @@ public class IrisComplex implements DataProvider {
|
|||||||
InferredType inferredType,
|
InferredType inferredType,
|
||||||
IrisRegion region,
|
IrisRegion region,
|
||||||
double naturalHeight,
|
double naturalHeight,
|
||||||
double x,
|
int x,
|
||||||
double z
|
int z
|
||||||
) {
|
) {
|
||||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
if (mapped != null) {
|
if (mapped != null) {
|
||||||
return mapped;
|
return mapped;
|
||||||
}
|
}
|
||||||
IrisBiome baseBiome = implode(sampleInferredBiome(region, inferredType, x, z), x, z);
|
IrisContext context = IrisContext.get();
|
||||||
|
IrisBiome baseBiome = reuseNaturalBaseBiome && (context == null || context.getChunkContext() == null)
|
||||||
|
? baseBiomeStream.get(x, z)
|
||||||
|
: implode(sampleInferredBiome(region, inferredType, x, z), (double) x, (double) z);
|
||||||
IrisBiome resolved = resolveNaturalSurfaceBiome(naturalHeight, baseBiome, region, x, z);
|
IrisBiome resolved = resolveNaturalSurfaceBiome(naturalHeight, baseBiome, region, x, z);
|
||||||
return resolved == baseBiome ? baseBiome : implode(resolved, x, z);
|
return resolved == baseBiome ? baseBiome : implode(resolved, (double) x, (double) z);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome resolveNaturalSurfaceBiome(
|
private IrisBiome resolveNaturalSurfaceBiome(
|
||||||
@@ -808,6 +800,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
int naturalHeight,
|
int naturalHeight,
|
||||||
double hydrologyWeight
|
double hydrologyWeight
|
||||||
) {
|
) {
|
||||||
|
if (layer.feature().type().isUnderground() || layer.feature().type().isDeepFluid()) {
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
return new HydrologyColumnLayer(
|
return new HydrologyColumnLayer(
|
||||||
layer.feature(),
|
layer.feature(),
|
||||||
GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight),
|
GenerationBlend.interpolateHeight(naturalHeight, layer.bedY(), hydrologyWeight),
|
||||||
@@ -847,6 +842,42 @@ public class IrisComplex implements DataProvider {
|
|||||||
|| hydrologyRuntime.isPlanned(x, z);
|
|| 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) {
|
public boolean isHistoricalChunk(int chunkX, int chunkZ) {
|
||||||
return transitionGenerationPlan != null
|
return transitionGenerationPlan != null
|
||||||
&& transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
|
&& transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
|
||||||
@@ -1370,9 +1401,25 @@ public class IrisComplex implements DataProvider {
|
|||||||
if (generators.length == 0) {
|
if (generators.length == 0) {
|
||||||
return 0D;
|
return 0D;
|
||||||
}
|
}
|
||||||
|
if (!Double.isFinite(low) || !Double.isFinite(high)) {
|
||||||
|
throw new IllegalStateException("Terrain generator bounds were not finite at " + x + "," + z
|
||||||
|
+ " (generator=" + generators[0].getLoadKey() + ", bounds=" + low + ".." + high + ").");
|
||||||
|
}
|
||||||
double height = 0D;
|
double height = 0D;
|
||||||
|
if (low == high) {
|
||||||
|
for (int i = 0; i < generators.length; i++) {
|
||||||
|
height += low;
|
||||||
|
}
|
||||||
|
return height / generators.length;
|
||||||
|
}
|
||||||
for (IrisGenerator generator : generators) {
|
for (IrisGenerator generator : generators) {
|
||||||
height += M.lerp(low, high, generator.getHeight(x, z, seed));
|
double noise = generator.getHeight(x, z, seed);
|
||||||
|
if (!Double.isFinite(noise)) {
|
||||||
|
throw new IllegalStateException("Terrain generator noise was not finite at " + x + "," + z
|
||||||
|
+ " (generator=" + generator.getLoadKey() + ", noise=" + noise
|
||||||
|
+ ", bounds=" + low + ".." + high + ").");
|
||||||
|
}
|
||||||
|
height += M.lerp(low, high, noise);
|
||||||
}
|
}
|
||||||
return height / generators.length;
|
return height / generators.length;
|
||||||
}
|
}
|
||||||
@@ -1455,9 +1502,34 @@ public class IrisComplex implements DataProvider {
|
|||||||
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
|
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
|
||||||
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
|
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
|
||||||
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
|
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
|
||||||
|
if (biome == b && hasFixedNoise(b.getChildStyle())) {
|
||||||
|
return biome;
|
||||||
|
}
|
||||||
return implode(biome, x, z, max - 1);
|
return implode(biome, x, z, max - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean hasFixedNaturalBiomeNoise(IrisDimension dimension, Iterable<IrisBiome> biomes) {
|
||||||
|
if (!hasFixedNoise(dimension.getRegionStyle()) || !hasFixedNoise(dimension.getContinentalStyle())
|
||||||
|
|| !hasFixedNoise(dimension.getLandBiomeStyle()) || !hasFixedNoise(dimension.getSeaBiomeStyle())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (IrisBiome biome : biomes) {
|
||||||
|
if (!biome.getChildren().isEmpty() && !hasFixedNoise(biome.getChildStyle())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasFixedNoise(IrisGeneratorStyle style) {
|
||||||
|
for (IrisGeneratorStyle current = style; current != null; current = current.getFracture()) {
|
||||||
|
if (current.getExpression() != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private ChildSelectionPlan resolveChildSelectionPlan(IrisBiome biome) {
|
private ChildSelectionPlan resolveChildSelectionPlan(IrisBiome biome) {
|
||||||
ChildSelectionPlan cachedPlan = childSelectionPlans.get(biome);
|
ChildSelectionPlan cachedPlan = childSelectionPlans.get(biome);
|
||||||
if (cachedPlan != null) {
|
if (cachedPlan != null) {
|
||||||
@@ -1517,14 +1589,15 @@ public class IrisComplex implements DataProvider {
|
|||||||
/**
|
/**
|
||||||
* Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator
|
* Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator
|
||||||
* bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
|
* bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
|
||||||
* resets both, giving each pass the same empty-scratch semantics a fresh allocation had.
|
* resets generator bounds while retaining bounded coordinate samples for the same biome stream.
|
||||||
* <p>
|
* <p>
|
||||||
* Single threaded and non reentrant by contract, matching the thread local sample caches in
|
* Single threaded and non reentrant by contract, matching the thread local sample caches in
|
||||||
* IrisInterpolation that this provider is invoked through. If a nested pass ever does appear,
|
* IrisInterpolation that this provider is invoked through. If a nested pass ever does appear,
|
||||||
* {@link #isInUse()} makes the caller fall back to a freshly allocated sampler.
|
* {@link #isInUse()} makes the caller fall back to a freshly allocated sampler.
|
||||||
*/
|
*/
|
||||||
private static final class BoundsSampler implements NoiseBoundsProvider {
|
private static final class BoundsSampler implements NoiseBoundsProvider {
|
||||||
private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64);
|
private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(4096);
|
||||||
|
private WeakReference<ProceduralStream<IrisBiome>> sampleSource = new WeakReference<>(null);
|
||||||
private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
|
private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
|
||||||
private IrisComplex complex;
|
private IrisComplex complex;
|
||||||
private Engine engine;
|
private Engine engine;
|
||||||
@@ -1542,7 +1615,10 @@ public class IrisComplex implements DataProvider {
|
|||||||
this.generators = generators;
|
this.generators = generators;
|
||||||
this.cachedBounds = cachedBounds;
|
this.cachedBounds = cachedBounds;
|
||||||
this.inUse = true;
|
this.inUse = true;
|
||||||
sampleCache.clear();
|
if (sampleSource.get() != complex.baseBiomeStream) {
|
||||||
|
sampleCache.clear();
|
||||||
|
sampleSource = new WeakReference<>(complex.baseBiomeStream);
|
||||||
|
}
|
||||||
|
|
||||||
if (!localBounds.isEmpty()) {
|
if (!localBounds.isEmpty()) {
|
||||||
localBounds.clear();
|
localBounds.clear();
|
||||||
@@ -1585,73 +1661,40 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open addressed biome memo keyed on the packed coordinate bits, replacing a linear scan that was
|
* Bounded biome memo keyed on the packed coordinate bits, replacing a linear scan that was
|
||||||
* quadratic in the number of columns a wide starcast touches. Single threaded by contract.
|
* quadratic in the number of columns a wide starcast touches. Single threaded by contract.
|
||||||
*/
|
*/
|
||||||
private static final class CoordinateBiomeCache {
|
private static final class CoordinateBiomeCache {
|
||||||
private long[] xBits;
|
private final long[] xBits;
|
||||||
private long[] zBits;
|
private final long[] zBits;
|
||||||
private IrisBiome[] values;
|
private final IrisBiome[] values;
|
||||||
private byte[] states;
|
private final int mask;
|
||||||
private int mask;
|
|
||||||
private int resizeThreshold;
|
|
||||||
private int size;
|
|
||||||
|
|
||||||
private CoordinateBiomeCache(int initialCapacity) {
|
private CoordinateBiomeCache(int capacity) {
|
||||||
int minimumCapacity = Math.max(8, initialCapacity);
|
xBits = new long[capacity];
|
||||||
int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity);
|
zBits = new long[capacity];
|
||||||
xBits = new long[tableSize];
|
values = new IrisBiome[capacity];
|
||||||
zBits = new long[tableSize];
|
mask = capacity - 1;
|
||||||
values = new IrisBiome[tableSize];
|
|
||||||
states = new byte[tableSize];
|
|
||||||
mask = tableSize - 1;
|
|
||||||
resizeThreshold = Math.max(1, (tableSize * 3) >> 2);
|
|
||||||
size = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clear() {
|
private void clear() {
|
||||||
if (size == 0) {
|
Arrays.fill(values, null);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Arrays.fill(states, (byte) 0);
|
|
||||||
size = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome get(double x, double z) {
|
private IrisBiome get(double x, double z) {
|
||||||
int slot = findSlot(Double.doubleToLongBits(x), Double.doubleToLongBits(z));
|
long xb = Double.doubleToLongBits(x);
|
||||||
return states[slot] == 0 ? null : values[slot];
|
long zb = Double.doubleToLongBits(z);
|
||||||
|
int slot = mix(xb, zb) & mask;
|
||||||
|
return xBits[slot] == xb && zBits[slot] == zb ? values[slot] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void put(double x, double z, IrisBiome biome) {
|
private void put(double x, double z, IrisBiome biome) {
|
||||||
long xb = Double.doubleToLongBits(x);
|
long xb = Double.doubleToLongBits(x);
|
||||||
long zb = Double.doubleToLongBits(z);
|
long zb = Double.doubleToLongBits(z);
|
||||||
int slot = findSlot(xb, zb);
|
int slot = mix(xb, zb) & mask;
|
||||||
boolean occupied = states[slot] != 0;
|
|
||||||
xBits[slot] = xb;
|
xBits[slot] = xb;
|
||||||
zBits[slot] = zb;
|
zBits[slot] = zb;
|
||||||
values[slot] = biome;
|
values[slot] = biome;
|
||||||
states[slot] = 1;
|
|
||||||
|
|
||||||
if (occupied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
size++;
|
|
||||||
if (size >= resizeThreshold) {
|
|
||||||
grow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int findSlot(long xb, long zb) {
|
|
||||||
int slot = mix(xb, zb) & mask;
|
|
||||||
while (states[slot] != 0) {
|
|
||||||
if (xBits[slot] == xb && zBits[slot] == zb) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
slot = (slot + 1) & mask;
|
|
||||||
}
|
|
||||||
return slot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int mix(long xb, long zb) {
|
private int mix(long xb, long zb) {
|
||||||
@@ -1662,49 +1705,6 @@ public class IrisComplex implements DataProvider {
|
|||||||
hash ^= (hash >>> 33);
|
hash ^= (hash >>> 33);
|
||||||
return (int) hash;
|
return (int) hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void grow() {
|
|
||||||
long[] previousXBits = xBits;
|
|
||||||
long[] previousZBits = zBits;
|
|
||||||
IrisBiome[] previousValues = values;
|
|
||||||
byte[] previousStates = states;
|
|
||||||
|
|
||||||
int nextLength = previousXBits.length << 1;
|
|
||||||
xBits = new long[nextLength];
|
|
||||||
zBits = new long[nextLength];
|
|
||||||
values = new IrisBiome[nextLength];
|
|
||||||
states = new byte[nextLength];
|
|
||||||
mask = nextLength - 1;
|
|
||||||
resizeThreshold = Math.max(1, (nextLength * 3) >> 2);
|
|
||||||
size = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < previousStates.length; i++) {
|
|
||||||
if (previousStates[i] == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int slot = findSlot(previousXBits[i], previousZBits[i]);
|
|
||||||
xBits[slot] = previousXBits[i];
|
|
||||||
zBits[slot] = previousZBits[i];
|
|
||||||
values[slot] = previousValues[i];
|
|
||||||
states[slot] = 1;
|
|
||||||
size++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int tableSizeFor(int value) {
|
|
||||||
int n = value - 1;
|
|
||||||
n |= n >>> 1;
|
|
||||||
n |= n >>> 2;
|
|
||||||
n |= n >>> 4;
|
|
||||||
n |= n >>> 8;
|
|
||||||
n |= n >>> 16;
|
|
||||||
int tableSize = n + 1;
|
|
||||||
if (tableSize < 8) {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
return tableSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class ChildSelectionPlan {
|
static final class ChildSelectionPlan {
|
||||||
@@ -1783,6 +1783,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
|
resolvedTerrain.clear();
|
||||||
if (hydrologyRuntime != null) {
|
if (hydrologyRuntime != null) {
|
||||||
hydrologyRuntime.close();
|
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.GenerationBoundarySignatureSampler;
|
||||||
import art.arcane.iris.engine.history.GenerationHistory;
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
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.GenerationKernelRegistry;
|
||||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||||
import art.arcane.iris.engine.framework.SeedManager;
|
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.IrisDimensionCarvingResolver;
|
||||||
import art.arcane.iris.engine.object.IrisEngineData;
|
import art.arcane.iris.engine.object.IrisEngineData;
|
||||||
import art.arcane.iris.engine.object.IrisRegion;
|
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.PackValidationCache;
|
||||||
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.IrisLogging;
|
||||||
import com.google.common.util.concurrent.AtomicDouble;
|
import com.google.common.util.concurrent.AtomicDouble;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
@@ -78,20 +76,17 @@ import lombok.Data;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
@@ -118,6 +113,11 @@ public class IrisEngine implements Engine {
|
|||||||
private final EngineMetrics metrics;
|
private final EngineMetrics metrics;
|
||||||
private final CompletableFuture<Void> generationCacheWarm;
|
private final CompletableFuture<Void> generationCacheWarm;
|
||||||
private final boolean studio;
|
private final boolean studio;
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
private volatile Path studioGenerationSource;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
boolean studioGenerationTransition;
|
||||||
private final AtomicRollingSequence wallClock;
|
private final AtomicRollingSequence wallClock;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
@@ -163,6 +163,9 @@ public class IrisEngine implements Engine {
|
|||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
final EngineMetricsReport metricsReport = new EngineMetricsReport(this);
|
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 AtomicBoolean cleaning;
|
||||||
private final ChronoLatch cleanLatch;
|
private final ChronoLatch cleanLatch;
|
||||||
private final SeedManager seedManager;
|
private final SeedManager seedManager;
|
||||||
@@ -267,7 +270,7 @@ public class IrisEngine implements Engine {
|
|||||||
this.initialKernelVersion = Objects.requireNonNull(kernelVersion, "generation kernel version");
|
this.initialKernelVersion = Objects.requireNonNull(kernelVersion, "generation kernel version");
|
||||||
this.initialTransitionPlan = transitionPlan;
|
this.initialTransitionPlan = transitionPlan;
|
||||||
this.platformHooks = IrisServices.get(EnginePlatformHooks.class);
|
this.platformHooks = IrisServices.get(EnginePlatformHooks.class);
|
||||||
this.generationSessions = new GenerationSessionManager();
|
this.generationSessions = new GenerationSessionManager(requiredMode.studio());
|
||||||
this.closing = new AtomicBoolean(true);
|
this.closing = new AtomicBoolean(true);
|
||||||
this.nativeStructureVolumeQueriesEnabled = new AtomicBoolean(!requiredMode.studio());
|
this.nativeStructureVolumeQueriesEnabled = new AtomicBoolean(!requiredMode.studio());
|
||||||
this.lifecycleState = LifecycleState.INITIALIZING;
|
this.lifecycleState = LifecycleState.INITIALIZING;
|
||||||
@@ -313,7 +316,7 @@ public class IrisEngine implements Engine {
|
|||||||
String studioCacheIdentity = currentStudioCacheIdentity();
|
String studioCacheIdentity = currentStudioCacheIdentity();
|
||||||
getData().loadPrefetch(this);
|
getData().loadPrefetch(this);
|
||||||
IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
|
IrisLogging.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
|
||||||
logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
|
diagnostics.logStudioInitializationPhase("load_prefetch", phaseStartedAt, false);
|
||||||
try {
|
try {
|
||||||
StructureIndexService.writeOnce(getData());
|
StructureIndexService.writeOnce(getData());
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
@@ -326,8 +329,8 @@ public class IrisEngine implements Engine {
|
|||||||
enableStableStudioCache(initialRuntime, studioCacheIdentity);
|
enableStableStudioCache(initialRuntime, studioCacheIdentity);
|
||||||
runtimeBuilder.publishRuntime(initialRuntime, null);
|
runtimeBuilder.publishRuntime(initialRuntime, null);
|
||||||
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
|
IrisLogging.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms");
|
||||||
logPackCompatSummary();
|
diagnostics.logPackCompatSummary();
|
||||||
logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
|
diagnostics.logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
|
||||||
phaseStartedAt = System.nanoTime();
|
phaseStartedAt = System.nanoTime();
|
||||||
if (requiredMode.warmGenerationCaches()) {
|
if (requiredMode.warmGenerationCaches()) {
|
||||||
if (requiredMode.studio()) {
|
if (requiredMode.studio()) {
|
||||||
@@ -337,7 +340,7 @@ public class IrisEngine implements Engine {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
generationCacheWarm.complete(null);
|
generationCacheWarm.complete(null);
|
||||||
logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
|
diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
|
||||||
}
|
}
|
||||||
EngineTickRegistry.registerTicking(this);
|
EngineTickRegistry.registerTicking(this);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
@@ -347,38 +350,6 @@ public class IrisEngine implements Engine {
|
|||||||
IrisLogging.debug("Engine Initialized " + getCacheID());
|
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() {
|
public void awaitGenerationCacheWarm() {
|
||||||
if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) {
|
if (generationCacheWarm.isDone() && !generationCacheWarm.isCompletedExceptionally()) {
|
||||||
return;
|
return;
|
||||||
@@ -394,18 +365,6 @@ public class IrisEngine implements Engine {
|
|||||||
return !generationCacheWarm.isDone();
|
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() {
|
private String currentStudioCacheIdentity() {
|
||||||
if (initializationMode != InitializationMode.STUDIO || !IrisPlatforms.isBound()) {
|
if (initializationMode != InitializationMode.STUDIO || !IrisPlatforms.isBound()) {
|
||||||
return "";
|
return "";
|
||||||
@@ -447,6 +406,36 @@ public class IrisEngine implements Engine {
|
|||||||
return packRoot.resolve(".iris").resolve("studio-hydrology");
|
return packRoot.resolve(".iris").resolve("studio-hydrology");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void startStudioEntryHydrology(int blockX, int blockZ) {
|
||||||
|
GenerationRuntimeBinding binding = getActiveGenerationRuntimeBinding();
|
||||||
|
if (!backgroundTasks.scheduleTrackedTask(() -> prepareStudioEntryHydrology(binding, blockX, blockZ))) {
|
||||||
|
throw new IllegalStateException("Iris background task admission closed before Studio entry preparation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareStudioEntryHydrology(GenerationRuntimeBinding binding, int blockX, int blockZ) {
|
||||||
|
if (closing.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GenerationSessionLease lease;
|
||||||
|
try {
|
||||||
|
lease = acquireGenerationLease("studio_entry_hydrology");
|
||||||
|
} catch (GenerationSessionException failure) {
|
||||||
|
if (closing.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Studio entry hydrology preparation could not acquire its generation session.", failure);
|
||||||
|
}
|
||||||
|
try (lease;
|
||||||
|
GenerationRuntimeScope runtimeScope = generationRuntimeScopes.open(binding);
|
||||||
|
IrisContext.Scope context = IrisContext.open(this, lease.sessionId(), null)) {
|
||||||
|
getComplex().getHydrologyRuntime().prepareChunkColumns(blockX, blockZ);
|
||||||
|
} catch (Exception failure) {
|
||||||
|
throw new IllegalStateException("Studio entry hydrology preparation failed at "
|
||||||
|
+ blockX + "," + blockZ + ".", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void startGenerationCacheWarm(long phaseStartedAtNanos) {
|
private void startGenerationCacheWarm(long phaseStartedAtNanos) {
|
||||||
if (!backgroundTasks.scheduleTrackedTask(() -> warmGenerationCaches(phaseStartedAtNanos))) {
|
if (!backgroundTasks.scheduleTrackedTask(() -> warmGenerationCaches(phaseStartedAtNanos))) {
|
||||||
throw new IllegalStateException("Iris background task admission closed before generation cache warming.");
|
throw new IllegalStateException("Iris background task admission closed before generation cache warming.");
|
||||||
@@ -469,7 +458,7 @@ public class IrisEngine implements Engine {
|
|||||||
throw new IllegalStateException("Generation cache warming failed.", failure);
|
throw new IllegalStateException("Generation cache warming failed.", failure);
|
||||||
} finally {
|
} finally {
|
||||||
IrisLogging.debug("[IrisEngine timing] cache warm total=" + (M.ms() - startedAtMillis) + "ms");
|
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 +564,9 @@ public class IrisEngine implements Engine {
|
|||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
@Override
|
@Override
|
||||||
public IrisBiome getCaveBiome(int x, int z) {
|
public IrisBiome getCaveBiome(int x, int z) {
|
||||||
|
if (usesScopedNaturalTerrain()) {
|
||||||
|
return Engine.super.getCaveBiome(x, z);
|
||||||
|
}
|
||||||
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave biome")) {
|
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave biome")) {
|
||||||
return Engine.super.getCaveBiome(x, z);
|
return Engine.super.getCaveBiome(x, z);
|
||||||
@@ -595,6 +587,9 @@ public class IrisEngine implements Engine {
|
|||||||
int z,
|
int z,
|
||||||
IrisDimensionCarvingResolver.State state
|
IrisDimensionCarvingResolver.State state
|
||||||
) {
|
) {
|
||||||
|
if (usesScopedNaturalTerrain()) {
|
||||||
|
return Engine.super.getCaveBiome(x, y, z, state);
|
||||||
|
}
|
||||||
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical cave biome")) {
|
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical cave biome")) {
|
||||||
return Engine.super.getCaveBiome(x, y, z, state);
|
return Engine.super.getCaveBiome(x, y, z, state);
|
||||||
@@ -604,6 +599,9 @@ public class IrisEngine implements Engine {
|
|||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
@Override
|
@Override
|
||||||
public IrisBiome getSurfaceBiome(int x, int z) {
|
public IrisBiome getSurfaceBiome(int x, int z) {
|
||||||
|
if (usesScopedNaturalTerrain()) {
|
||||||
|
return Engine.super.getSurfaceBiome(x, z);
|
||||||
|
}
|
||||||
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome")) {
|
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome")) {
|
||||||
return Engine.super.getSurfaceBiome(x, z);
|
return Engine.super.getSurfaceBiome(x, z);
|
||||||
@@ -637,6 +635,10 @@ public class IrisEngine implements Engine {
|
|||||||
@ChunkCoordinates
|
@ChunkCoordinates
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getObjectsAt(int x, int z) {
|
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 =
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
openGenerationHistoryCoordinateScopeUnchecked(x << 4, z << 4, "resolve chunk objects")) {
|
openGenerationHistoryCoordinateScopeUnchecked(x << 4, z << 4, "resolve chunk objects")) {
|
||||||
return getMantle().getObjectComponent().guess(x, z);
|
return getMantle().getObjectComponent().guess(x, z);
|
||||||
@@ -646,6 +648,15 @@ public class IrisEngine implements Engine {
|
|||||||
@ChunkCoordinates
|
@ChunkCoordinates
|
||||||
@Override
|
@Override
|
||||||
public Set<Pair<String, BlockPos>> getPOIsAt(int chunkX, int chunkZ) {
|
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 =
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
openGenerationHistoryCoordinateScopeUnchecked(
|
openGenerationHistoryCoordinateScopeUnchecked(
|
||||||
chunkX << 4, chunkZ << 4, "resolve chunk points of interest")) {
|
chunkX << 4, chunkZ << 4, "resolve chunk points of interest")) {
|
||||||
@@ -654,11 +665,19 @@ public class IrisEngine implements Engine {
|
|||||||
chunkX,
|
chunkX,
|
||||||
chunkZ,
|
chunkZ,
|
||||||
MatterStructurePOI.class,
|
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;
|
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) {
|
private void warmupChunk(int x, int z) {
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
for (int j = 0; j < 16; j++) {
|
for (int j = 0; j < 16; j++) {
|
||||||
@@ -675,6 +694,30 @@ public class IrisEngine implements Engine {
|
|||||||
hotloader.hotload();
|
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() {
|
public void hotloadComplex() {
|
||||||
hotloader.hotloadComplex();
|
hotloader.hotloadComplex();
|
||||||
}
|
}
|
||||||
@@ -827,6 +870,10 @@ public class IrisEngine implements Engine {
|
|||||||
return generationRuntimeScopes.current() != null;
|
return generationRuntimeScopes.current() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean usesScopedNaturalTerrain() {
|
||||||
|
return hasGenerationRuntimeScope() && getComplex().isNaturalTerrainContext();
|
||||||
|
}
|
||||||
|
|
||||||
private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScopeUnchecked(
|
private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScopeUnchecked(
|
||||||
int blockX,
|
int blockX,
|
||||||
int blockZ,
|
int blockZ,
|
||||||
@@ -956,7 +1003,7 @@ public class IrisEngine implements Engine {
|
|||||||
EngineTarget requiredTarget = Objects.requireNonNull(runtimeTarget, "detached generation target");
|
EngineTarget requiredTarget = Objects.requireNonNull(runtimeTarget, "detached generation target");
|
||||||
Path requiredMantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
|
Path requiredMantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
|
||||||
synchronized (lifecycleLock) {
|
synchronized (lifecycleLock) {
|
||||||
requireRunning("build a detached generation runtime");
|
requireGenerationRuntimeMutation("build a detached generation runtime");
|
||||||
GenerationRuntime active = runtime.generation();
|
GenerationRuntime active = runtime.generation();
|
||||||
if (requiredTarget.getWorld() != active.target().getWorld()) {
|
if (requiredTarget.getWorld() != active.target().getWorld()) {
|
||||||
throw new IllegalArgumentException("Detached generation runtime must target this Iris world.");
|
throw new IllegalArgumentException("Detached generation runtime must target this Iris world.");
|
||||||
@@ -1066,7 +1113,7 @@ public class IrisEngine implements Engine {
|
|||||||
public void setDefaultGenerationRuntime(GenerationRuntimeBinding binding) {
|
public void setDefaultGenerationRuntime(GenerationRuntimeBinding binding) {
|
||||||
GenerationRuntimeBinding required = Objects.requireNonNull(binding, "generation runtime binding");
|
GenerationRuntimeBinding required = Objects.requireNonNull(binding, "generation runtime binding");
|
||||||
synchronized (lifecycleLock) {
|
synchronized (lifecycleLock) {
|
||||||
requireRunning("select the default generation runtime");
|
requireGenerationRuntimeMutation("select the default generation runtime");
|
||||||
if (required.engine != this) {
|
if (required.engine != this) {
|
||||||
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
|
throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine.");
|
||||||
}
|
}
|
||||||
@@ -1439,6 +1486,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() {
|
private boolean EngineSafe() {
|
||||||
// Todo: this has potential if done right
|
// Todo: this has potential if done right
|
||||||
int EngineMCVersion = getEngineData().getStatistics().getMCVersion();
|
int EngineMCVersion = getEngineData().getStatistics().getMCVersion();
|
||||||
@@ -1466,6 +1521,7 @@ public class IrisEngine implements Engine {
|
|||||||
public enum InitializationMode {
|
public enum InitializationMode {
|
||||||
RUNTIME(false, true),
|
RUNTIME(false, true),
|
||||||
STUDIO(true, true),
|
STUDIO(true, true),
|
||||||
|
OBJECT_STUDIO(true, false),
|
||||||
JIGSAW_STUDIO(true, false);
|
JIGSAW_STUDIO(true, false);
|
||||||
|
|
||||||
private final boolean studio;
|
private final boolean studio;
|
||||||
|
|||||||
@@ -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.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
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.image.IrisImageMapRuntime;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -33,16 +31,12 @@ import art.arcane.iris.util.project.stream.ProceduralStream;
|
|||||||
public class UpperDimensionContext implements DataProvider {
|
public class UpperDimensionContext implements DataProvider {
|
||||||
private final DimensionTerrainContext terrainContext;
|
private final DimensionTerrainContext terrainContext;
|
||||||
private final ProceduralStream<Double> lowerHeightStream;
|
private final ProceduralStream<Double> lowerHeightStream;
|
||||||
private final ProceduralStream<Double> unblendedLowerHeightStream;
|
|
||||||
private final TransitionGenerationPlan transitionPlan;
|
|
||||||
private final CeilingLayout ceilingLayout;
|
private final CeilingLayout ceilingLayout;
|
||||||
|
|
||||||
private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) {
|
private UpperDimensionContext(DimensionTerrainContext terrainContext, Engine engine) {
|
||||||
this.terrainContext = terrainContext;
|
this.terrainContext = terrainContext;
|
||||||
IrisComplex complex = engine.getComplex();
|
IrisComplex complex = engine.getComplex();
|
||||||
lowerHeightStream = complex.getHeightStream();
|
lowerHeightStream = complex.getHeightStream();
|
||||||
unblendedLowerHeightStream = complex.getUnblendedNaturalHeightStream();
|
|
||||||
transitionPlan = complex.getTransitionGenerationPlan();
|
|
||||||
ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap());
|
ceilingLayout = new CeilingLayout(engine.getHeight(), engine.getDimension().getUpperDimensionGap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,18 +97,6 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
0D,
|
0D,
|
||||||
Math.min(ceilingLayout.height() - 1D, terrainContext.getNormalTerrainHeight(x, z))
|
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(
|
int lowerSurfaceY = (int) Math.min(
|
||||||
ceilingLayout.height(),
|
ceilingLayout.height(),
|
||||||
Math.round(lowerHeightStream.getDouble(x, z))
|
Math.round(lowerHeightStream.getDouble(x, z))
|
||||||
@@ -122,14 +104,6 @@ public class UpperDimensionContext implements DataProvider {
|
|||||||
return effectiveSurfaceY(depth, ceilingLayout, lowerSurfaceY);
|
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) {
|
static int effectiveSurfaceY(double depth, CeilingLayout layout, int lowerSurfaceY) {
|
||||||
long mirroredSurface = layout.height() - 1L - Math.round(depth);
|
long mirroredSurface = layout.height() - 1L - Math.round(depth);
|
||||||
long gapSurface = (long) lowerSurfaceY + layout.minimumGap();
|
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.collection.KMap;
|
||||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
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.math.RNG;
|
||||||
import art.arcane.volmlib.util.matter.Matter;
|
import art.arcane.volmlib.util.matter.Matter;
|
||||||
import art.arcane.volmlib.util.matter.MatterBiomeInject;
|
import art.arcane.volmlib.util.matter.MatterBiomeInject;
|
||||||
@@ -65,7 +66,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
int depth = h.getDepth();
|
int depth = h.getDepth();
|
||||||
int height = h.getHeight();
|
int height = h.getHeight();
|
||||||
Engine engine = getEngine();
|
Engine engine = getEngine();
|
||||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
Mantle<Matter> mantle = context.isSpeculativeTerrain() ? null : engine.getMantle().getMantle();
|
||||||
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
|
||||||
IrisComplex complex = context.getComplex();
|
IrisComplex complex = context.getComplex();
|
||||||
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
|
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
|
||||||
@@ -74,21 +75,6 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
for (int zf = 0; zf < depth; zf++) {
|
for (int zf = 0; zf < depth; zf++) {
|
||||||
int worldX = x + xf;
|
int worldX = x + xf;
|
||||||
int worldZ = z + zf;
|
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);
|
IrisBiome biome = biomeCache.get(xf, zf);
|
||||||
ResolvedBiome resolved = resolve(biomeKey(
|
ResolvedBiome resolved = resolve(biomeKey(
|
||||||
biome,
|
biome,
|
||||||
@@ -117,11 +103,39 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
context.getDimensionStackLayout(xf, zf)
|
context.getDimensionStackLayout(xf, zf)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
writeHistoricalColumn(h, mantle, xf, zf, worldX, worldZ, height, engine, complex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
engine.getMetrics().getBiome().put(p.getMilliseconds());
|
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(
|
private void writeHistoricalColumn(
|
||||||
Hunk<PlatformBiome> output,
|
Hunk<PlatformBiome> output,
|
||||||
Mantle<Matter> mantle,
|
Mantle<Matter> mantle,
|
||||||
@@ -137,14 +151,17 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
if (transitionPlan == null) {
|
if (transitionPlan == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TransitionGenerationPlan.TerrainSample sample = transitionPlan.terrainSampleAt(worldX, worldZ);
|
TransitionGenerationPlan.TerrainSample terrainSample = transitionPlan.terrainSampleAt(worldX, worldZ);
|
||||||
|
if (terrainSample.newEpochWeight() == 1D) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
int minimumWorldY = engine.getMinHeight();
|
int minimumWorldY = engine.getMinHeight();
|
||||||
String activeKey = sample.historicalPhysicalBiomeKeyAt(minimumWorldY).orElse(null);
|
String activeKey = transitionPlan.historicalPhysicalBiomeKeyAt(worldX, minimumWorldY, worldZ, terrainSample).orElse(null);
|
||||||
int rangeStart = 0;
|
int rangeStart = 0;
|
||||||
for (int internalY = 1; internalY <= height; internalY++) {
|
for (int internalY = 1; internalY <= height; internalY++) {
|
||||||
String nextKey = internalY == height
|
String nextKey = internalY == height
|
||||||
? null
|
? null
|
||||||
: sample.historicalPhysicalBiomeKeyAt(minimumWorldY + internalY).orElse(null);
|
: transitionPlan.historicalPhysicalBiomeKeyAt(worldX, minimumWorldY + internalY, worldZ, terrainSample).orElse(null);
|
||||||
if (Objects.equals(activeKey, nextKey)) {
|
if (Objects.equals(activeKey, nextKey)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -179,7 +196,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
if (resolved.biome() != null) {
|
if (resolved.biome() != null) {
|
||||||
output.set(localX, 0, localZ, localX, height - 1, localZ, resolved.biome());
|
output.set(localX, 0, localZ, localX, height - 1, localZ, resolved.biome());
|
||||||
}
|
}
|
||||||
mantle.set(worldX, 0, worldZ, resolved.matter());
|
if (mantle != null) {
|
||||||
|
mantle.set(worldX, 0, worldZ, resolved.matter());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyDimensionStackBiomes(
|
private void applyDimensionStackBiomes(
|
||||||
@@ -256,7 +275,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
if (minimumY > maximumY) {
|
if (minimumY > maximumY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY);
|
if (mantle != null) {
|
||||||
|
clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY);
|
||||||
|
}
|
||||||
if (resolved.biome() != null) {
|
if (resolved.biome() != null) {
|
||||||
output.set(
|
output.set(
|
||||||
localX,
|
localX,
|
||||||
@@ -268,7 +289,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
|
|||||||
resolved.biome()
|
resolved.biome()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (resolved.matter() != null) {
|
if (mantle != null && resolved.matter() != null) {
|
||||||
mantle.set(worldX, minimumY, worldZ, resolved.matter());
|
mantle.set(worldX, minimumY, worldZ, resolved.matter());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-10
@@ -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.context.ChunkContext;
|
||||||
import art.arcane.iris.util.project.hunk.Hunk;
|
import art.arcane.iris.util.project.hunk.Hunk;
|
||||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
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.collection.KList;
|
||||||
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
import art.arcane.volmlib.util.documentation.BlockCoordinates;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||||
@@ -81,8 +82,8 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
|
|||||||
if (getEngine().getDimensionStackContext() == null) {
|
if (getEngine().getDimensionStackContext() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle()
|
MantleChunk<Matter> mantleChunk = context.isSpeculativeTerrain() ? null
|
||||||
.getChunk(x >> 4, z >> 4).use();
|
: getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
|
||||||
try {
|
try {
|
||||||
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
|
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
|
||||||
for (int localX = 0; localX < blocks.getWidth(); localX++) {
|
for (int localX = 0; localX < blocks.getWidth(); localX++) {
|
||||||
@@ -100,7 +101,26 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
mantleChunk.release();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,17 +249,46 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Pla
|
|||||||
}
|
}
|
||||||
|
|
||||||
void clear(int x, int y, int z) {
|
void clear(int x, int y, int z) {
|
||||||
int sectionIndex = y >> 4;
|
if (chunk == null) {
|
||||||
List<MatterSlice<?>> slices = slicesBySection[sectionIndex];
|
return;
|
||||||
if (slices == null) {
|
|
||||||
slices = resolveSlices(sectionIndex);
|
|
||||||
slicesBySection[sectionIndex] = slices;
|
|
||||||
}
|
}
|
||||||
for (MatterSlice<?> slice : slices) {
|
synchronized (chunk) {
|
||||||
slice.set(x, y & 15, z, null);
|
int sectionIndex = y >> 4;
|
||||||
|
captureNaturalFacts(sectionIndex, x, y & 15, z);
|
||||||
|
List<MatterSlice<?>> slices = slicesBySection[sectionIndex];
|
||||||
|
if (slices == null) {
|
||||||
|
slices = resolveSlices(sectionIndex);
|
||||||
|
slicesBySection[sectionIndex] = slices;
|
||||||
|
}
|
||||||
|
for (MatterSlice<?> slice : slices) {
|
||||||
|
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) {
|
private List<MatterSlice<?>> resolveSlices(int sectionIndex) {
|
||||||
Matter section = chunk.get(sectionIndex);
|
Matter section = chunk.get(sectionIndex);
|
||||||
if (section == null) {
|
if (section == null) {
|
||||||
|
|||||||
+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
|
@Override
|
||||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
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) {
|
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)) {
|
if (height != Math.round(localFluidHeight)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
|
ProceduralStream<Double> heightStream = getComplex().getHeightStream();
|
||||||
ProceduralStream<Double> fluidStream = getComplex().getRiverWaterSurfaceStream();
|
if (Math.round(heightStream.get(realX1, realZ)) >= getEngine().getMantle().getFluidHeight(realX1, realZ)
|
||||||
if (Math.round(heightStream.get(realX1, realZ)) >= Math.round(fluidStream.get(realX1, realZ))
|
&& Math.round(heightStream.get(realX_1, realZ)) >= getEngine().getMantle().getFluidHeight(realX_1, realZ)
|
||||||
&& Math.round(heightStream.get(realX_1, realZ)) >= Math.round(fluidStream.get(realX_1, realZ))
|
&& Math.round(heightStream.get(realX, realZ1)) >= getEngine().getMantle().getFluidHeight(realX, realZ1)
|
||||||
&& Math.round(heightStream.get(realX, realZ1)) >= Math.round(fluidStream.get(realX, realZ1))
|
&& Math.round(heightStream.get(realX, realZ_1)) >= getEngine().getMantle().getFluidHeight(realX, realZ_1)) {
|
||||||
&& Math.round(heightStream.get(realX, realZ_1)) >= Math.round(fluidStream.get(realX, realZ_1))) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
|
|||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
|
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) {
|
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) {
|
if (inferredType == InferredType.SHORE && height < fluidHeight) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package art.arcane.iris.engine.framework;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.engine.framework.render.RenderType;
|
import art.arcane.iris.engine.framework.render.RenderType;
|
||||||
import art.arcane.iris.engine.framework.render.Renderer;
|
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 org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalInt;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
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) {
|
if (context != null && context.getEngine() == this && context.getGenerationSessionId() != 0L) {
|
||||||
return generationSessions.continueSession(operation, context.getGenerationSessionId());
|
return generationSessions.continueSession(operation, context.getGenerationSessionId());
|
||||||
}
|
}
|
||||||
if (isClosing() || isClosed()) {
|
return generationSessions.acquireForEngine(this, operation);
|
||||||
throw new GenerationSessionException("Generation session rejected new work for " + operation
|
|
||||||
+ " while the Iris engine is closing.", isClosed());
|
|
||||||
}
|
|
||||||
|
|
||||||
return generationSessions.acquire(operation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default long getGenerationSessionId() {
|
default long getGenerationSessionId() {
|
||||||
@@ -238,6 +237,8 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
return getTarget().getData();
|
return getTarget().getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Path getPackSource();
|
||||||
|
|
||||||
default IrisWorld getWorld() {
|
default IrisWorld getWorld() {
|
||||||
return getTarget().getWorld();
|
return getTarget().getWorld();
|
||||||
}
|
}
|
||||||
@@ -448,6 +449,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default int getHeight(int x, int z, boolean ignoreFluid) {
|
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();
|
DimensionStackContext dimensionStackContext = getDimensionStackContext();
|
||||||
if (dimensionStackContext != null) {
|
if (dimensionStackContext != null) {
|
||||||
return ignoreFluid
|
return ignoreFluid
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.framework;
|
|||||||
import art.arcane.iris.util.project.context.IrisContext;
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public final class EngineLifecycleTasks {
|
public final class EngineLifecycleTasks {
|
||||||
@@ -13,10 +14,16 @@ public final class EngineLifecycleTasks {
|
|||||||
Objects.requireNonNull(engine);
|
Objects.requireNonNull(engine);
|
||||||
Objects.requireNonNull(operation);
|
Objects.requireNonNull(operation);
|
||||||
Objects.requireNonNull(task);
|
Objects.requireNonNull(task);
|
||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
|
try {
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
Optional<GenerationSessionLease> admitted = tryAcquire(engine, operation);
|
||||||
task.run();
|
if (admitted.isEmpty()) {
|
||||||
return true;
|
return false;
|
||||||
|
}
|
||||||
|
try (GenerationSessionLease lease = admitted.get();
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
task.run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} catch (GenerationSessionException exception) {
|
} catch (GenerationSessionException exception) {
|
||||||
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
|
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -27,9 +34,17 @@ public final class EngineLifecycleTasks {
|
|||||||
|
|
||||||
public static <T> T call(Engine engine, String operation, Supplier<T> task, T unavailable) {
|
public static <T> T call(Engine engine, String operation, Supplier<T> task, T unavailable) {
|
||||||
Objects.requireNonNull(task);
|
Objects.requireNonNull(task);
|
||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease(operation);
|
Objects.requireNonNull(engine);
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
Objects.requireNonNull(operation);
|
||||||
return task.get();
|
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) {
|
} catch (GenerationSessionException exception) {
|
||||||
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
|
if (engine.isClosing() || engine.isClosed() || exception.isExpectedTeardown()) {
|
||||||
return unavailable;
|
return unavailable;
|
||||||
@@ -37,4 +52,11 @@ public final class EngineLifecycleTasks {
|
|||||||
throw new IllegalStateException("Iris lifecycle rejected " + operation + ".", exception);
|
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;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
import art.arcane.iris.engine.IrisComplex;
|
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.IrisEngine;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
import art.arcane.iris.util.project.context.ChunkContext;
|
import art.arcane.iris.util.project.context.ChunkContext;
|
||||||
@@ -31,6 +33,21 @@ import art.arcane.iris.spi.PlatformBiome;
|
|||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
|
||||||
public interface EngineMode extends Staged {
|
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();
|
void close();
|
||||||
|
|
||||||
Engine getEngine();
|
Engine getEngine();
|
||||||
@@ -159,8 +176,12 @@ public interface EngineMode extends Staged {
|
|||||||
return getEngine().getMantle();
|
return getEngine().getMantle();
|
||||||
}
|
}
|
||||||
|
|
||||||
default void generateMatter(int x, int z, boolean multicore, ChunkContext context) {
|
default void generateTerrainMatter(int x, int z, boolean multicore, ChunkContext context) {
|
||||||
getMantle().generateMatter(x, z, multicore, 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
|
@BlockCoordinates
|
||||||
@@ -180,6 +201,9 @@ public interface EngineMode extends Staged {
|
|||||||
|
|
||||||
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
EngineStage[] stages = getStages().toArray(new EngineStage[0]);
|
||||||
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) {
|
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) {
|
for (EngineStage i : stages) {
|
||||||
i.generate(x, z, blocks, biomes, multicore, ctx);
|
i.generate(x, z, blocks, biomes, multicore, ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,23 @@
|
|||||||
package art.arcane.iris.engine.framework;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
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.iris.engine.mantle.MantleComponent;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
|
||||||
public interface EnginePlatformHooks {
|
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
|
* 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.
|
* 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;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
public final class GenerationSessionLease implements AutoCloseable {
|
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 manager;
|
||||||
private final GenerationSessionManager.GenerationSessionState state;
|
private final GenerationSessionManager.GenerationSessionState state;
|
||||||
private final long sessionId;
|
private final long sessionId;
|
||||||
|
private final GenerationTransitionGate.Participation participation;
|
||||||
private boolean released;
|
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.manager = manager;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
|
this.participation = participation;
|
||||||
this.released = false;
|
this.released = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +26,12 @@ public final class GenerationSessionLease implements AutoCloseable {
|
|||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void detachThread() {
|
||||||
|
if (participation != null) {
|
||||||
|
participation.detachThread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
if (released || state == null) {
|
if (released || state == null) {
|
||||||
@@ -30,6 +39,10 @@ public final class GenerationSessionLease implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
released = true;
|
released = true;
|
||||||
manager.releaseLease(state);
|
try {
|
||||||
|
manager.releaseLease(state);
|
||||||
|
} finally {
|
||||||
|
participation.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,94 @@
|
|||||||
package art.arcane.iris.engine.framework;
|
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.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
|
||||||
public final class GenerationSessionManager {
|
public final class GenerationSessionManager {
|
||||||
private final AtomicLong sessionSequence;
|
private final AtomicLong sessionSequence;
|
||||||
private final AtomicReference<GenerationSessionState> current;
|
private final AtomicReference<GenerationSessionState> current;
|
||||||
private final Object drainMonitor;
|
private final Object drainMonitor;
|
||||||
|
private final GenerationTransitionGate transitionGate;
|
||||||
|
|
||||||
public GenerationSessionManager() {
|
public GenerationSessionManager() {
|
||||||
|
this(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenerationSessionManager(boolean studioTransitions) {
|
||||||
|
this.transitionGate = new GenerationTransitionGate(studioTransitions);
|
||||||
this.sessionSequence = new AtomicLong(0L);
|
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.current = new AtomicReference<>(new GenerationSessionState(nextSessionId(), new AtomicBoolean(true), new AtomicInteger(0), new AtomicBoolean(false), new AtomicReference<>(null)));
|
||||||
this.drainMonitor = new Object();
|
this.drainMonitor = new Object();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GenerationSessionLease acquire(String operation) throws GenerationSessionException {
|
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) {
|
while (true) {
|
||||||
GenerationSessionState state = current.get();
|
GenerationSessionState state = current.get();
|
||||||
if (state == null || !state.accepting().get()) {
|
if (state == null || !state.accepting().get()) {
|
||||||
@@ -34,11 +106,23 @@ public final class GenerationSessionManager {
|
|||||||
throw rejected(operation, state);
|
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 {
|
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) {
|
while (true) {
|
||||||
GenerationSessionState state = current.get();
|
GenerationSessionState state = current.get();
|
||||||
if (state == null || state.sessionId() != sessionId) {
|
if (state == null || state.sessionId() != sessionId) {
|
||||||
@@ -51,7 +135,7 @@ public final class GenerationSessionManager {
|
|||||||
continue;
|
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;
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.iris.engine.actuator.IrisTransitionGeometryActuator;
|
||||||
|
|
||||||
public abstract class IrisEngineMode implements EngineMode {
|
public abstract class IrisEngineMode implements EngineMode {
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
|
private final EngineStage transitionStage;
|
||||||
private final KList<EngineStage> stages;
|
private final KList<EngineStage> stages;
|
||||||
|
private final KList<EngineStage> terrainStages;
|
||||||
private boolean closed;
|
private boolean closed;
|
||||||
|
|
||||||
public IrisEngineMode(Engine engine) {
|
public IrisEngineMode(Engine engine) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
|
this.transitionStage = new IrisTransitionGeometryActuator(engine);
|
||||||
this.stages = new KList<>();
|
this.stages = new KList<>();
|
||||||
|
this.terrainStages = new KList<>();
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +44,9 @@ public abstract class IrisEngineMode implements EngineMode {
|
|||||||
|
|
||||||
closed = true;
|
closed = true;
|
||||||
dump();
|
dump();
|
||||||
|
terrainStages.forEach(EngineStage::close);
|
||||||
|
terrainStages.clear();
|
||||||
|
transitionStage.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -51,6 +59,21 @@ public abstract class IrisEngineMode implements EngineMode {
|
|||||||
return stages;
|
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
|
@Override
|
||||||
public void registerStage(EngineStage stage) {
|
public void registerStage(EngineStage stage) {
|
||||||
stages.add(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.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
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 {
|
public int persist() throws IOException {
|
||||||
lock.writeLock().lock();
|
lock.writeLock().lock();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public final class ChunkGenerationSemantics {
|
public final class ChunkGenerationSemantics {
|
||||||
static final int MAX_KEY_BYTES = 1_024;
|
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_RIVER_FEATURES = 16_384;
|
||||||
static final int MAX_STRUCTURES = 4_096;
|
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
|
private static final Comparator<StructureOccurrence> STRUCTURE_COMPARATOR = Comparator
|
||||||
.comparing(StructureOccurrence::key)
|
.comparing(StructureOccurrence::key)
|
||||||
.thenComparingInt(occurrence -> occurrence.position().x())
|
.thenComparingInt(occurrence -> occurrence.position().x())
|
||||||
.thenComparingInt(occurrence -> occurrence.position().z())
|
.thenComparingInt(occurrence -> occurrence.position().z())
|
||||||
.thenComparingInt(occurrence -> occurrence.position().y());
|
.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
|
private static final Comparator<RiverFeatureOccurrence> RIVER_FEATURE_COMPARATOR = Comparator
|
||||||
.comparing(RiverFeatureOccurrence::profileKey)
|
.comparing(RiverFeatureOccurrence::profileKey)
|
||||||
.thenComparing(RiverFeatureOccurrence::type)
|
.thenComparing(RiverFeatureOccurrence::type)
|
||||||
@@ -43,6 +44,7 @@ public final class ChunkGenerationSemantics {
|
|||||||
private final Set<String> objectKeys;
|
private final Set<String> objectKeys;
|
||||||
private final Set<RiverFeatureOccurrence> riverFeatures;
|
private final Set<RiverFeatureOccurrence> riverFeatures;
|
||||||
private final Set<StructureOccurrence> structures;
|
private final Set<StructureOccurrence> structures;
|
||||||
|
private final Set<PointOfInterest> pointsOfInterest;
|
||||||
|
|
||||||
private ChunkGenerationSemantics(Builder builder) {
|
private ChunkGenerationSemantics(Builder builder) {
|
||||||
chunkX = builder.chunkX;
|
chunkX = builder.chunkX;
|
||||||
@@ -56,6 +58,7 @@ public final class ChunkGenerationSemantics {
|
|||||||
objectKeys = immutableKeys(builder.objectKeys, "object");
|
objectKeys = immutableKeys(builder.objectKeys, "object");
|
||||||
riverFeatures = immutableRiverFeatures(builder.riverFeatures);
|
riverFeatures = immutableRiverFeatures(builder.riverFeatures);
|
||||||
structures = immutableStructures(builder.structures);
|
structures = immutableStructures(builder.structures);
|
||||||
|
pointsOfInterest = immutablePoints(builder.pointsOfInterest);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder builder(int chunkX, int chunkZ, long activationId) {
|
public static Builder builder(int chunkX, int chunkZ, long activationId) {
|
||||||
@@ -102,6 +105,10 @@ public final class ChunkGenerationSemantics {
|
|||||||
return riverFeatures;
|
return riverFeatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<PointOfInterest> pointsOfInterest() {
|
||||||
|
return pointsOfInterest;
|
||||||
|
}
|
||||||
|
|
||||||
public Set<StructureOccurrence> structures() {
|
public Set<StructureOccurrence> structures() {
|
||||||
return structures;
|
return structures;
|
||||||
}
|
}
|
||||||
@@ -129,7 +136,8 @@ public final class ChunkGenerationSemantics {
|
|||||||
|| !riverProfileKeys.containsAll(requiredUpdate.riverProfileKeys)
|
|| !riverProfileKeys.containsAll(requiredUpdate.riverProfileKeys)
|
||||||
|| !objectKeys.containsAll(requiredUpdate.objectKeys)
|
|| !objectKeys.containsAll(requiredUpdate.objectKeys)
|
||||||
|| !riverFeatures.containsAll(requiredUpdate.riverFeatures)
|
|| !riverFeatures.containsAll(requiredUpdate.riverFeatures)
|
||||||
|| !structures.containsAll(requiredUpdate.structures);
|
|| !structures.containsAll(requiredUpdate.structures)
|
||||||
|
|| !pointsOfInterest.containsAll(requiredUpdate.pointsOfInterest);
|
||||||
if (sealed && addsFacts) {
|
if (sealed && addsFacts) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"Chunk " + chunkX + "," + chunkZ + " has sealed generation semantics"
|
"Chunk " + chunkX + "," + chunkZ + " has sealed generation semantics"
|
||||||
@@ -153,7 +161,9 @@ public final class ChunkGenerationSemantics {
|
|||||||
.addRiverFeatures(riverFeatures)
|
.addRiverFeatures(riverFeatures)
|
||||||
.addRiverFeatures(requiredUpdate.riverFeatures)
|
.addRiverFeatures(requiredUpdate.riverFeatures)
|
||||||
.addStructures(structures)
|
.addStructures(structures)
|
||||||
.addStructures(requiredUpdate.structures);
|
.addStructures(requiredUpdate.structures)
|
||||||
|
.addPointsOfInterest(pointsOfInterest)
|
||||||
|
.addPointsOfInterest(requiredUpdate.pointsOfInterest);
|
||||||
if (sealed || requiredUpdate.sealed) {
|
if (sealed || requiredUpdate.sealed) {
|
||||||
merged.seal();
|
merged.seal();
|
||||||
}
|
}
|
||||||
@@ -178,7 +188,8 @@ public final class ChunkGenerationSemantics {
|
|||||||
&& riverProfileKeys.equals(semantics.riverProfileKeys)
|
&& riverProfileKeys.equals(semantics.riverProfileKeys)
|
||||||
&& objectKeys.equals(semantics.objectKeys)
|
&& objectKeys.equals(semantics.objectKeys)
|
||||||
&& riverFeatures.equals(semantics.riverFeatures)
|
&& riverFeatures.equals(semantics.riverFeatures)
|
||||||
&& structures.equals(semantics.structures);
|
&& structures.equals(semantics.structures)
|
||||||
|
&& pointsOfInterest.equals(semantics.pointsOfInterest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -194,7 +205,8 @@ public final class ChunkGenerationSemantics {
|
|||||||
riverProfileKeys,
|
riverProfileKeys,
|
||||||
objectKeys,
|
objectKeys,
|
||||||
riverFeatures,
|
riverFeatures,
|
||||||
structures
|
structures,
|
||||||
|
pointsOfInterest
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,11 +242,11 @@ public final class ChunkGenerationSemantics {
|
|||||||
if (!requiredKey.equals(requiredKey.trim())) {
|
if (!requiredKey.equals(requiredKey.trim())) {
|
||||||
throw new IllegalArgumentException("Generation semantic resource keys must not contain surrounding whitespace: " + requiredKey);
|
throw new IllegalArgumentException("Generation semantic resource keys must not contain surrounding whitespace: " + requiredKey);
|
||||||
}
|
}
|
||||||
if (!requiredKey.equals(requiredKey.toLowerCase(Locale.ROOT))) {
|
for (int index = 0; index < requiredKey.length(); index++) {
|
||||||
throw new IllegalArgumentException("Generation semantic resource keys must be lowercase: " + requiredKey);
|
char character = requiredKey.charAt(index);
|
||||||
}
|
if (character == '\\' || Character.isISOControl(character)) {
|
||||||
if (!RESOURCE_KEY_PATTERN.matcher(requiredKey).matches()) {
|
throw new IllegalArgumentException("Generation semantic resource key contains an invalid character");
|
||||||
throw new IllegalArgumentException("Generation semantic resource key is not normalized: " + requiredKey);
|
}
|
||||||
}
|
}
|
||||||
if (requiredKey.getBytes(StandardCharsets.UTF_8).length > MAX_KEY_BYTES) {
|
if (requiredKey.getBytes(StandardCharsets.UTF_8).length > MAX_KEY_BYTES) {
|
||||||
throw new IllegalArgumentException("Generation semantic resource key is too long");
|
throw new IllegalArgumentException("Generation semantic resource key is too long");
|
||||||
@@ -283,6 +295,28 @@ public final class ChunkGenerationSemantics {
|
|||||||
return Collections.unmodifiableSet(validated);
|
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) {
|
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<String> objectKeys;
|
||||||
private final Set<RiverFeatureOccurrence> riverFeatures;
|
private final Set<RiverFeatureOccurrence> riverFeatures;
|
||||||
private final Set<StructureOccurrence> structures;
|
private final Set<StructureOccurrence> structures;
|
||||||
|
private final Set<PointOfInterest> pointsOfInterest;
|
||||||
private boolean sealed;
|
private boolean sealed;
|
||||||
|
|
||||||
private Builder(int chunkX, int chunkZ, long activationId) {
|
private Builder(int chunkX, int chunkZ, long activationId) {
|
||||||
@@ -333,6 +368,7 @@ public final class ChunkGenerationSemantics {
|
|||||||
objectKeys = new TreeSet<>();
|
objectKeys = new TreeSet<>();
|
||||||
riverFeatures = new TreeSet<>(RIVER_FEATURE_COMPARATOR);
|
riverFeatures = new TreeSet<>(RIVER_FEATURE_COMPARATOR);
|
||||||
structures = new TreeSet<>(STRUCTURE_COMPARATOR);
|
structures = new TreeSet<>(STRUCTURE_COMPARATOR);
|
||||||
|
pointsOfInterest = new TreeSet<>(POI_COMPARATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder addSurfaceBiome(String key) {
|
public Builder addSurfaceBiome(String key) {
|
||||||
@@ -450,6 +486,22 @@ public final class ChunkGenerationSemantics {
|
|||||||
return this;
|
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() {
|
public Builder seal() {
|
||||||
sealed = true;
|
sealed = true;
|
||||||
return this;
|
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);
|
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) {
|
private static void validateWeight(double weight) {
|
||||||
if (!Double.isFinite(weight) || weight < 0D || weight > 1D) {
|
if (!Double.isFinite(weight) || weight < 0D || weight > 1D) {
|
||||||
throw new IllegalArgumentException("Blend weight must be finite and between zero and one");
|
throw new IllegalArgumentException("Blend weight must be finite and between zero and one");
|
||||||
|
|||||||
+4
@@ -7,4 +7,8 @@ import java.io.IOException;
|
|||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface GenerationBoundarySignatureSampler {
|
public interface GenerationBoundarySignatureSampler {
|
||||||
TerrainBoundarySignature sample(IrisEngine engine, int blockX, int blockZ) throws IOException;
|
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
|
@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);
|
long regionKey = packRegion(chunkX >> 5, chunkZ >> 5);
|
||||||
String hash = shardHashes.get(regionKey);
|
String hash = shardHashes.get(regionKey);
|
||||||
if (hash == null) {
|
if (hash == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
byte[] mask = load(regionKey, hash);
|
byte[] mask;
|
||||||
|
synchronized (this) {
|
||||||
|
mask = load(regionKey, hash);
|
||||||
|
}
|
||||||
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
|
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
|
||||||
| Math.floorMod(chunkX, REGION_SIDE);
|
| Math.floorMod(chunkX, REGION_SIDE);
|
||||||
return (mask[localIndex >>> 3] & (1 << (localIndex & 7))) != 0;
|
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.Objects;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
final class GenerationKernelSourceSeal {
|
final class GenerationBuildRevision {
|
||||||
private static final int MAXIMUM_BYTES = 8 * 1024 * 1024;
|
private static final int MAXIMUM_BYTES = 8 * 1024 * 1024;
|
||||||
private static final Pattern SHA256 = Pattern.compile("[0-9a-f]{64}");
|
private static final Pattern SHA256 = Pattern.compile("[0-9a-f]{64}");
|
||||||
|
|
||||||
private GenerationKernelSourceSeal() {
|
private GenerationBuildRevision() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static String requireFingerprint(int abi, Class<?> factory) {
|
static String requireFingerprint(int abi, Class<?> factory) {
|
||||||
Descriptor descriptor = load(abi);
|
Descriptor descriptor = load(abi);
|
||||||
if (!descriptor.factoryClass().equals(factory.getName())) {
|
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();
|
return descriptor.fingerprint();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Descriptor load(int abi) {
|
static Descriptor load(int abi) {
|
||||||
String resource = "/META-INF/iris/generation-kernels/abi-" + abi + ".seal";
|
String resource = "/META-INF/iris/generation-kernels/abi-" + abi + ".revision";
|
||||||
try (InputStream input = GenerationKernelSourceSeal.class.getResourceAsStream(resource)) {
|
try (InputStream input = GenerationBuildRevision.class.getResourceAsStream(resource)) {
|
||||||
if (input == null) {
|
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);
|
byte[] bytes = input.readNBytes(MAXIMUM_BYTES + 1);
|
||||||
String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\n", -1);
|
String[] lines = new String(bytes, StandardCharsets.UTF_8).split("\n", -1);
|
||||||
if (lines.length < 4 || !lines[2].startsWith("factory\t")) {
|
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());
|
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_$]*)+")) {
|
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);
|
return new Descriptor(abi, factoryClass, List.copyOf(algorithms), fingerprint);
|
||||||
} catch (IOException failure) {
|
} 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(factoryClass, "factoryClass");
|
||||||
Objects.requireNonNull(bytes, "bytes");
|
Objects.requireNonNull(bytes, "bytes");
|
||||||
if (abi < 1 || bytes.length == 0 || bytes.length > MAXIMUM_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 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
|
if (!source.startsWith(prefix) || !source.endsWith("\n") || source.indexOf('\r') >= 0
|
||||||
|| !source.contains("\nsource\t") || !source.contains("\ndependency\t")) {
|
|| !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()));
|
requireCanonicalEntries(source.substring(prefix.length()));
|
||||||
try {
|
try {
|
||||||
@@ -79,7 +79,7 @@ final class GenerationKernelSourceSeal {
|
|||||||
private static void requireCanonicalEntries(String source) throws IOException {
|
private static void requireCanonicalEntries(String source) throws IOException {
|
||||||
String[] entries = source.split("\n", -1);
|
String[] entries = source.split("\n", -1);
|
||||||
if (entries.length > 30_000) {
|
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;
|
int previousType = -1;
|
||||||
String previousKey = "";
|
String previousKey = "";
|
||||||
@@ -92,7 +92,7 @@ final class GenerationKernelSourceSeal {
|
|||||||
if (fields[0].equals("algorithm")) {
|
if (fields[0].equals("algorithm")) {
|
||||||
if (previousType != -1 || fields.length != 3 || !fields[1].matches("[1-9][0-9]{0,8}")
|
if (previousType != -1 || fields.length != 3 || !fields[1].matches("[1-9][0-9]{0,8}")
|
||||||
|| !fields[2].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 rng = Integer.parseInt(fields[1]);
|
||||||
int seed = Integer.parseInt(fields[2]);
|
int seed = Integer.parseInt(fields[2]);
|
||||||
@@ -116,14 +116,14 @@ final class GenerationKernelSourceSeal {
|
|||||||
|| fields[1].chars().anyMatch(Character::isWhitespace)
|
|| fields[1].chars().anyMatch(Character::isWhitespace)
|
||||||
|| (type == previousType && fields[1].compareTo(previousKey) <= 0)
|
|| (type == previousType && fields[1].compareTo(previousKey) <= 0)
|
||||||
|| (type >= 2 && !SHA256.matcher(fields[2]).matches())) {
|
|| (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;
|
previousType = type;
|
||||||
previousKey = fields[1];
|
previousKey = fields[1];
|
||||||
hasScope |= type == 0;
|
hasScope |= type == 0;
|
||||||
}
|
}
|
||||||
if (!hasScope || !hasAlgorithm) {
|
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)
|
return dimensionKey.equals(other.dimensionKey)
|
||||||
&& dimensionTypeKey.equals(other.dimensionTypeKey)
|
&& dimensionTypeKey.equals(other.dimensionTypeKey)
|
||||||
&& environment.equals(other.environment)
|
&& environment.equals(other.environment)
|
||||||
&& generationMode.equals(other.generationMode)
|
|
||||||
&& internalFluidHeight == other.internalFluidHeight
|
|
||||||
&& minHeight == other.minHeight
|
&& minHeight == other.minHeight
|
||||||
&& height == other.height
|
&& height == other.height
|
||||||
&& logicalHeight == other.logicalHeight
|
&& logicalHeight == other.logicalHeight
|
||||||
&& Double.compare(coordinateScale, other.coordinateScale) == 0
|
&& Double.compare(coordinateScale, other.coordinateScale) == 0
|
||||||
&& upperTerrainEnabled == other.upperTerrainEnabled
|
|
||||||
&& upperDimensionKey.equals(other.upperDimensionKey)
|
|
||||||
&& upperDimensionGap == other.upperDimensionGap
|
|
||||||
&& dimensionTypeFingerprintSchema == other.dimensionTypeFingerprintSchema
|
&& dimensionTypeFingerprintSchema == other.dimensionTypeFingerprintSchema
|
||||||
&& dimensionTypeFingerprint.equals(other.dimensionTypeFingerprint);
|
&& dimensionTypeFingerprint.equals(other.dimensionTypeFingerprint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
package art.arcane.iris.engine.history;
|
package art.arcane.iris.engine.history;
|
||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
|
||||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||||
import art.arcane.iris.engine.IrisEngine;
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
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.IrisRegion;
|
||||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
@@ -55,7 +48,7 @@ public final class GenerationFindCatalog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasObjectPlacement(Engine engine, String key) {
|
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) {
|
public static KList<String> retainedStructureKeys(Engine engine) {
|
||||||
@@ -63,7 +56,7 @@ public final class GenerationFindCatalog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasRetainedStructurePlacement(Engine engine, String key) {
|
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) {
|
private static RetainedCatalog retained(Engine engine) {
|
||||||
@@ -90,87 +83,48 @@ public final class GenerationFindCatalog {
|
|||||||
|
|
||||||
private static RetainedCatalog loadRetained(GenerationHistory history, GenerationManifest manifest) {
|
private static RetainedCatalog loadRetained(GenerationHistory history, GenerationManifest manifest) {
|
||||||
RetainedCatalog catalog = new RetainedCatalog(manifest.activeActivation().activationId());
|
RetainedCatalog catalog = new RetainedCatalog(manifest.activeActivation().activationId());
|
||||||
Set<String> loadedPacks = new HashSet<>();
|
try {
|
||||||
Long activationId = manifest.activeActivation().parentActivationId();
|
history.forEachRecordedSemantic(semantics -> addRecorded(catalog, semantics));
|
||||||
while (activationId != null) {
|
} catch (IOException error) {
|
||||||
GenerationActivation activation = manifest.activation(activationId).orElseThrow();
|
throw new UncheckedIOException("Unable to read recorded Iris find catalog", error);
|
||||||
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);
|
|
||||||
} catch (IOException error) {
|
|
||||||
throw new UncheckedIOException("Unable to read retained Iris find catalog for activation "
|
|
||||||
+ activationId, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activationId = activation.parentActivationId();
|
|
||||||
}
|
}
|
||||||
return catalog;
|
return catalog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void loadPack(RetainedCatalog catalog, Path packRoot, String dimensionKey) {
|
private static void addRecorded(RetainedCatalog catalog, ChunkGenerationSemantics semantics) {
|
||||||
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
|
if (!semantics.sealed() || semantics.activationId() == catalog.activeActivationId) {
|
||||||
try {
|
return;
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
}
|
||||||
if (dimension == null) {
|
for (String key : semantics.surfaceBiomeKeys()) {
|
||||||
throw new IllegalStateException("Retained Iris dimension is absent: " + dimensionKey);
|
addBiome(catalog, key);
|
||||||
|
}
|
||||||
|
for (String key : semantics.caveBiomeKeys()) {
|
||||||
|
addBiome(catalog, key);
|
||||||
|
}
|
||||||
|
for (String key : semantics.regionKeys()) {
|
||||||
|
String normalized = normalizeKey(key);
|
||||||
|
if (!catalog.regions.containsKey(normalized)) {
|
||||||
|
IrisRegion region = new IrisRegion().setName(key);
|
||||||
|
region.setLoadKey(key);
|
||||||
|
catalog.regions.put(normalized, region);
|
||||||
}
|
}
|
||||||
addStructures(catalog.structures, dimension.getStructures());
|
}
|
||||||
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
|
for (String key : semantics.objectKeys()) {
|
||||||
catalog.biomes.putIfAbsent(normalizeKey(biome.getLoadKey()), biome);
|
catalog.objects.add(normalizeObjectKey(key));
|
||||||
addObjects(catalog.objects, biome.getObjects());
|
}
|
||||||
addStructures(catalog.structures, biome.getStructures());
|
for (ChunkGenerationSemantics.StructureOccurrence occurrence : semantics.structures()) {
|
||||||
}
|
catalog.structures.add(normalizeKey(occurrence.key()));
|
||||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
|
||||||
catalog.regions.putIfAbsent(normalizeKey(region.getLoadKey()), region);
|
|
||||||
addObjects(catalog.objects, region.getObjects());
|
|
||||||
addStructures(catalog.structures, region.getStructures());
|
|
||||||
}
|
|
||||||
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) {
|
private static void addBiome(RetainedCatalog catalog, String key) {
|
||||||
for (IrisStructurePlacement placement : placements) {
|
String normalized = normalizeKey(key);
|
||||||
if (placement == null) {
|
if (catalog.biomes.containsKey(normalized)) {
|
||||||
continue;
|
return;
|
||||||
}
|
|
||||||
for (String key : placement.getStructures()) {
|
|
||||||
String normalized = normalizeKey(key);
|
|
||||||
if (!normalized.isEmpty()) {
|
|
||||||
structures.add(normalized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
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) {
|
private static <T extends IrisRegistrant> T find(Iterable<T> entries, String key) {
|
||||||
String normalized = normalizeKey(key);
|
String normalized = normalizeKey(key);
|
||||||
|
T caseInsensitiveMatch = null;
|
||||||
for (T entry : entries) {
|
for (T entry : entries) {
|
||||||
if (normalizeKey(entry.getLoadKey()).equals(normalized)) {
|
String entryKey = normalizeKey(entry.getLoadKey());
|
||||||
|
if (entryKey.equals(normalized)) {
|
||||||
return entry;
|
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) {
|
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) {
|
private static String normalizeObjectKey(String key) {
|
||||||
|
|||||||
Loaded 100 of 250 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user