Merged generation history

- Immutable generation epochs now coexist with dimension stacks
- Historical terrain, biomes, structures, and locate behavior remain stable across updates
- Versioned generation kernels preserve the combined runtime contract
This commit is contained in:
Brian Neumann-Fopiano
2026-09-04 15:06:55 -04:00
263 changed files with 39624 additions and 1849 deletions
@@ -8,9 +8,11 @@ import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
@@ -40,6 +42,7 @@ import org.bukkit.craftbukkit.CraftWorld;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -65,14 +68,11 @@ public class CustomBiomeSource extends BiomeSource {
private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile KMap<String, Holder<Biome>> customBiomes;
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private volatile IrisDimension cacheDimension;
private volatile int cacheRuntimeId;
private final ConcurrentHashMap<RuntimeNoiseKey, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<RuntimeNoiseKey, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<RuntimeColumnKey, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<RuntimeColumnKey, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, RuntimeBiomeState> runtimeBiomeStates = new ConcurrentHashMap<>();
public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine;
@@ -80,10 +80,10 @@ public class CustomBiomeSource extends BiomeSource {
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.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
this.cacheDimension = engine.getDimension();
this.cacheRuntimeId = engine.getCacheID();
if (engine instanceof IrisEngine irisEngine) {
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
}
runtimeBiomeState();
}
private static List<Holder<Biome>> getAllBiomes(
@@ -133,7 +133,7 @@ public class CustomBiomeSource extends BiomeSource {
if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), customBiome.getId());
customRegistry, engine, ownedBiome.dimension(), customBiome.getId());
if (customHolder == null) {
throw new IllegalStateException("Iris custom visible biome '"
+ customBiomeKey(ownedBiome.dimension(), customBiome.getId())
@@ -257,7 +257,7 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
runtimeBiomeState();
World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) {
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
@@ -284,7 +284,7 @@ public class CustomBiomeSource extends BiomeSource {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
String key = customBiomeKey(ownedBiome.dimension(), j.getId());
Holder<Biome> holder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), j.getId());
customRegistry, engine, ownedBiome.dimension(), j.getId());
if (holder == null) {
if (fallback != null) {
m.put(key, fallback);
@@ -318,7 +318,7 @@ public class CustomBiomeSource extends BiomeSource {
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), customBiome.getId());
customRegistry, engine, ownedBiome.dimension(), customBiome.getId());
if (customHolder != null) {
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
}
@@ -340,8 +340,7 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) {
return null;
}
ensureCachesCurrent();
return vanillaSpawnBiomes.get(biome.value());
return runtimeBiomeState().vanillaSpawnBiomes().get(biome.value());
}
}
@@ -354,7 +353,7 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) {
return null;
}
ensureCachesCurrent();
runtimeBiomeState();
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
int sampleX = QuartPos.toBlock(quartX);
@@ -390,15 +389,17 @@ public class CustomBiomeSource extends BiomeSource {
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
int blockX = x << 2;
int blockZ = z << 2;
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openHistoryCoordinateScope(blockX, blockZ, "bukkit_structure_biome");
GenerationSessionLease lease = requireGenerationLease(
"bukkit_structure_biome",
"Iris structure biome lookup was rejected during an engine transition");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
}
ensureCachesCurrent();
return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
}
}
@@ -409,7 +410,8 @@ public class CustomBiomeSource extends BiomeSource {
}
boolean cacheable = isBiomeCacheable(x, z);
long cacheKey = packNoiseKey(x, y, z);
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
engine.getCacheID(), packNoiseKey(x, y, z));
if (cacheable) {
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) {
@@ -455,7 +457,6 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime");
}
ensureCachesCurrent();
return findNaturalSurfaceBiomeHorizontal(
x, y, z, searchRadius, quartStep, allowed, random);
}
@@ -533,7 +534,6 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
}
ensureCachesCurrent();
int minQuartY = QuartPos.fromBlock(y - radius);
boolean monumentQuery = radius == 29
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
@@ -556,20 +556,27 @@ public class CustomBiomeSource extends BiomeSource {
}
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z);
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
if (surfaceHolder != null) {
return surfaceHolder;
int blockX = x << 2;
int blockZ = z << 2;
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openHistoryCoordinateScope(blockX, blockZ, "bukkit_surface_structure_biome")) {
RuntimeColumnKey columnKey = new RuntimeColumnKey(
engine.getCacheID(), packColumnKey(x, z));
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
if (surfaceHolder != null) {
return surfaceHolder;
}
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(
columnKey, resolvedSurfaceHolder);
if (existingSurfaceHolder != null) {
return existingSurfaceHolder;
}
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
surfaceStructureBiomeCache.clear();
}
return resolvedSurfaceHolder;
}
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
if (existingSurfaceHolder != null) {
return existingSurfaceHolder;
}
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
surfaceStructureBiomeCache.clear();
}
return resolvedSurfaceHolder;
}
private Pair<BlockPos, Holder<Biome>> findNaturalSurfaceBiomeHorizontal(
@@ -609,21 +616,27 @@ public class CustomBiomeSource extends BiomeSource {
}
private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z);
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
if (cachedHolder != null) {
return cachedHolder;
int blockX = x << 2;
int blockZ = z << 2;
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openHistoryCoordinateScope(blockX, blockZ, "bukkit_natural_structure_biome")) {
RuntimeColumnKey columnKey = new RuntimeColumnKey(
engine.getCacheID(), packColumnKey(x, z));
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
if (cachedHolder != null) {
return cachedHolder;
}
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
columnKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
naturalSurfaceStructureBiomeCache.clear();
}
return resolvedHolder;
}
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
columnKey, resolvedHolder);
if (existingHolder != null) {
return existingHolder;
}
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
naturalSurfaceStructureBiomeCache.clear();
}
return resolvedHolder;
}
private boolean isGuaranteedSurfaceBiome(int quartY) {
@@ -671,11 +684,12 @@ public class CustomBiomeSource extends BiomeSource {
}
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
x << 2, z << 2, "bukkit_visible_biome");
GenerationSessionLease lease = requireGenerationLease(
"bukkit_visible_biome",
"Iris visible biome lookup was rejected during an engine transition");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
prepareVisibleBiomeBatch();
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
}
@@ -685,7 +699,7 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
ensureCachesCurrent();
runtimeBiomeState();
}
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
@@ -717,7 +731,8 @@ public class CustomBiomeSource extends BiomeSource {
IrisDimensionCarvingResolver.State resolverState,
boolean cacheable
) {
long cacheKey = packNoiseKey(x, y, z);
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
engine.getCacheID(), packNoiseKey(x, y, z));
if (cacheable) {
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) {
@@ -768,32 +783,71 @@ public class CustomBiomeSource extends BiomeSource {
}
}
private GenerationSessionLease requireGenerationLease(String operation, String rejectionMessage) {
GenerationSessionLease lease = tryAcquireGenerationLease(operation);
if (lease == null) {
throw new IllegalStateException(rejectionMessage);
}
return lease;
}
private boolean isRuntimeAvailable() {
return !engine.isClosed() && engine.getComplex() != null;
}
private void ensureCachesCurrent() {
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
int blockX,
int blockZ,
String operation
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
try {
return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ);
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation
+ " could not route generation history at "
+ blockX + "," + blockZ + ".", failure);
}
}
private void evictRuntimeCaches(int runtimeId) {
runtimeBiomeStates.remove(runtimeId);
noiseBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
structureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
surfaceStructureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
naturalSurfaceStructureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
}
private RuntimeBiomeState runtimeBiomeState() {
IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID();
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
return;
RuntimeBiomeState existing = runtimeBiomeStates.get(runtimeId);
if (existing != null) {
if (existing.dimension() != dimension) {
throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions.");
}
return existing;
}
synchronized (this) {
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
return;
synchronized (runtimeBiomeStates) {
existing = runtimeBiomeStates.get(runtimeId);
if (existing != null) {
if (existing.dimension() != dimension) {
throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions.");
}
return existing;
}
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
biomeCustomRegistry, engine, fallbackBiome);
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
biomeCustomRegistry, biomeRegistry, engine);
noiseBiomeCache.clear();
structureBiomeCache.clear();
surfaceStructureBiomeCache.clear();
naturalSurfaceStructureBiomeCache.clear();
customBiomes = refreshedCustomBiomes;
vanillaSpawnBiomes = refreshedSpawnBiomes;
cacheDimension = dimension;
cacheRuntimeId = runtimeId;
RuntimeBiomeState created = new RuntimeBiomeState(
dimension,
refreshedCustomBiomes,
refreshedSpawnBiomes);
runtimeBiomeStates.put(runtimeId, created);
return created;
}
}
@@ -830,6 +884,14 @@ public class CustomBiomeSource extends BiomeSource {
IrisDimensionCarvingResolver.State resolverState,
DimensionStackLayout stackLayout
) {
int blockX = x << 2;
int blockY = y << 2;
int blockZ = z << 2;
Optional<String> historicalKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
blockX, blockY, blockZ);
if (historicalKey.isPresent()) {
return resolvePhysicalBiomeHolder(historicalKey.get());
}
BiomeResolution resolution = resolveBiomeResolution(
x, y, z, resolverState, true, stackLayout);
if (resolution == null) {
@@ -858,7 +920,7 @@ public class CustomBiomeSource extends BiomeSource {
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiomeKey(
Holder<Biome> holder = runtimeBiomeState().customBiomes().get(customBiomeKey(
resolution.dimension, customBiome.getId()));
if (holder != null) {
return holder;
@@ -872,6 +934,18 @@ public class CustomBiomeSource extends BiomeSource {
return resolveBiomeResolution(x, y, z, null, false);
}
private Holder<Biome> resolvePhysicalBiomeHolder(String physicalKey) {
Holder<Biome> holder = resolveBiomeHolder(biomeCustomRegistry, physicalKey);
if (holder == null) {
holder = resolveBiomeHolder(biomeRegistry, physicalKey);
}
if (holder == null) {
throw new IllegalStateException("Historical Iris biome '" + physicalKey
+ "' is not registered in the active world registry.");
}
return holder;
}
private BiomeResolution resolveBiomeResolution(
int x,
int y,
@@ -1008,17 +1082,19 @@ public class CustomBiomeSource extends BiomeSource {
private static Holder<Biome> resolveCustomBiomeHolder(
Registry<Biome> customRegistry,
Engine engine,
IrisDimension dimension,
String customBiomeId
) {
if (customRegistry == null || dimension == null
if (customRegistry == null || engine == null || dimension == null
|| customBiomeId == null || customBiomeId.isBlank()) {
return null;
}
Identifier resourceLocation = Identifier.tryParse(customBiomeKey(dimension, customBiomeId));
String physicalKey = engine.getData().customBiomeResourceKey(dimension, customBiomeId);
Identifier resourceLocation = Identifier.tryParse(physicalKey);
if (resourceLocation == null) {
return null;
throw new IllegalStateException("Invalid Iris custom biome resource key '" + physicalKey + "'.");
}
Biome biome = customRegistry.getValue(resourceLocation);
if (biome == null) {
@@ -1170,4 +1246,17 @@ public class CustomBiomeSource extends BiomeSource {
private record OwnedBiome(IrisDimension dimension, IrisBiome biome) {
}
private record RuntimeNoiseKey(int runtimeId, long coordinateKey) {
}
private record RuntimeColumnKey(int runtimeId, long coordinateKey) {
}
private record RuntimeBiomeState(
IrisDimension dimension,
KMap<String, Holder<Biome>> customBiomes,
Map<Biome, Holder<Biome>> vanillaSpawnBiomes
) {
}
}
@@ -62,9 +62,7 @@ final class ImportedFeatureStage {
private static final String CYCLE_MARKER = "Feature order cycle found";
private final Engine engine;
private volatile FeatureTable featureTable;
private volatile IrisDimension inertDimension;
private volatile int settledRuntimeId;
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
ImportedFeatureStage(Engine engine) {
this.engine = engine;
@@ -76,7 +74,8 @@ final class ImportedFeatureStage {
* does today.
*/
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
FeatureTable table = featureTable;
RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) {
return null;
}
@@ -87,50 +86,46 @@ final class ImportedFeatureStage {
/**
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
*
* <p>The volatile fast path is unlocked, so a prepared stage costs two reads per chunk. The build is
* synchronized: every worldgen thread decorating a chunk calls this, and two threads that both found the
* stage unprepared would each run {@code FeatureSorter}, whose cycle detection is the expensive part.
*/
void prepare(WorldGenLevel level) {
IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID();
if (settled(dimension, runtimeId)) {
if (settled(runtimeStates.get(runtimeId), dimension)) {
return;
}
synchronized (this) {
if (settled(dimension, runtimeId)) {
if (settled(runtimeStates.get(runtimeId), dimension)) {
return;
}
build(level, dimension);
settledRuntimeId = runtimeId;
runtimeStates.put(runtimeId, build(level, dimension));
}
}
private boolean settled(IrisDimension dimension, int runtimeId) {
if (settledRuntimeId != runtimeId) {
return false;
}
FeatureTable current = featureTable;
if (current != null && current.dimension() == dimension) {
return true;
}
return inertDimension == dimension;
void evictRuntime(int runtimeId) {
runtimeStates.remove(runtimeId);
}
private void build(WorldGenLevel level, IrisDimension dimension) {
private RuntimeFeatureState currentState() {
IrisDimension dimension = engine.getDimension();
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
return settled(state, dimension) ? state : null;
}
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
return state != null && state.dimension() == dimension;
}
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
IrisImportedFeatureControl control;
try {
control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) {
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
+ error);
markInert(dimension);
return;
return new RuntimeFeatureState(dimension, null);
}
if (!control.shouldGenerateFeatures()) {
markInert(dimension);
return;
return new RuntimeFeatureState(dimension, null);
}
FeatureTable built;
try {
@@ -138,23 +133,15 @@ final class ImportedFeatureStage {
} catch (Throwable error) {
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": feature table construction failed: " + error);
markInert(dimension);
return;
return new RuntimeFeatureState(dimension, null);
}
if (built == null) {
markInert(dimension);
return;
return new RuntimeFeatureState(dimension, null);
}
featureTable = built;
inertDimension = null;
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
+ " custom-biome derivative maps");
}
private void markInert(IrisDimension dimension) {
featureTable = null;
inertDimension = dimension;
return new RuntimeFeatureState(dimension, built);
}
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
@@ -217,7 +204,7 @@ final class ImportedFeatureStage {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, engine.getDimension().getCustomBiomeKey(customBiome.getId()));
addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
}
}
return keys;
@@ -249,7 +236,10 @@ final class ImportedFeatureStage {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
derivatives.put(engine.getDimension().getCustomBiomeKey(customBiome.getId()), derivative);
derivatives.put(
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
derivative
);
}
}
return derivatives;
@@ -280,7 +270,8 @@ final class ImportedFeatureStage {
* control is disabled or the table degraded.
*/
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
FeatureTable table = featureTable;
RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) {
return;
}
@@ -467,4 +458,7 @@ final class ImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps,
Map<String, Holder<Biome>> derivatives, boolean filtered) {
}
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
}
}
@@ -7,9 +7,11 @@ import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
@@ -101,6 +103,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -133,8 +136,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private final int runtimeHeight;
private final int runtimeSeaLevel;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile IrisDimension spawnTableDimension;
private volatile int spawnTableRuntimeId;
private final ImportedFeatureStage importedFeatures;
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
private volatile ReachableStructureCache reachableStructureCache;
@@ -158,9 +159,25 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
this.runtimeMinY = level.getMinY();
this.runtimeHeight = level.getHeight();
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
if (engine instanceof IrisEngine irisEngine) {
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
}
installNativeStructureVolumeIndex(level);
}
private void evictRuntimeCaches(int runtimeId) {
importedFeatures.evictRuntime(runtimeId);
mergedSpawnTables.keySet().removeIf(key -> key.runtimeId() == runtimeId);
ReachableStructureCache reachable = reachableStructureCache;
if (reachable != null && reachable.runtimeId() == runtimeId) {
reachableStructureCache = null;
}
StructureStepCache steps = structureStepCache;
if (steps != null && steps.runtimeId() == runtimeId) {
structureStepCache = null;
}
}
private void installNativeStructureVolumeIndex(ServerLevel level) {
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
@@ -186,7 +203,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) {
return null;
}
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
SectionPos.blockToSectionCoord(pos.getX()),
SectionPos.blockToSectionCoord(pos.getZ()),
"bukkit_nms_structure_locate");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
NativeStructureVanillaLocator.Candidate nativeCandidate =
@@ -365,9 +387,16 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|| runtimeLevel.getChunkSource().getGeneratorState() != structureState) {
return;
}
ChunkPos chunkPos = access.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_structures");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!allowsRoutedDiscreteGeneration(access, ChunkStatus.STRUCTURE_STARTS)) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
@@ -399,15 +428,28 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (!start.isValid() || previousStarts.get(structure) == start) {
continue;
}
if (configuredStarts.containsKey(structure)) {
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
BoundingBox footprint = start.getBoundingBox();
if (!engine.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
footprint.maxZ())) {
access.setStartForStructure(structure, StructureStart.INVALID_START);
if (configuredStarts.containsKey(structure)) {
NativeStructureOwnershipStore.discard(
engine, structureId, chunkPos.x(), chunkPos.z());
}
continue;
}
if (configuredStarts.containsKey(structure)) {
continue;
}
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
@@ -634,7 +676,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (runtimeLevel.getChunkSource().getGenerator() != this) {
return;
}
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_references");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
NativeStructureReferenceRepair.createReferences(
@@ -644,7 +690,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_biomes");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
customBiomeSource.prepareVisibleBiomeBatch();
@@ -659,31 +709,63 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_build_surface");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_build_surface");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
}
}
@Override
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_apply_carvers");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_apply_carvers");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
}
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
ChunkPos chunkPos = ichunkaccess.getPos();
BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(
ichunkaccess.getPos(),
chunkPos,
"bukkit_nms_chunk_pipeline");
GenerationSessionLease lease;
GenerationHistoryRuntimeRouter.RuntimeRoute route;
try {
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
route = openHistoryRoute(chunkPos.x(), chunkPos.z(), "bukkit_nms_chunk_pipeline");
} catch (RuntimeException | Error failure) {
stage.close();
throw failure;
}
GenerationSessionLease lease;
try {
CompletableFuture<ChunkAccess> pipeline = delegate
.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
} catch (RuntimeException | Error failure) {
closeHistoryRoute(route, failure);
stage.close();
throw failure;
}
try {
CompletableFuture<ChunkAccess> delegatePipeline;
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
delegatePipeline = delegate.fillFromNoise(
blender, randomstate, structuremanager, ichunkaccess);
}
CompletableFuture<ChunkAccess> pipeline = delegatePipeline
.thenApply(filled -> {
try (IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope =
openHistoryRuntimeScope(route);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
primeWorldgenHeightmaps(filled);
return filled;
}
@@ -691,24 +773,78 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
pipeline.whenComplete((filled, failure) -> {
boolean cancelled = isCancellationFailure(failure);
lease.close();
stage.close();
if (failure == null) {
Throwable completionFailure = closeNoisePipelineResources(
failure, lease, route, stage);
if (completionFailure == null) {
completion.complete(filled);
} else if (cancelled) {
completion.cancel(false);
} else {
completion.completeExceptionally(failure);
completion.completeExceptionally(completionFailure);
}
});
return completion;
} catch (RuntimeException | Error failure) {
lease.close();
closeHistoryRoute(route, failure);
stage.close();
throw failure;
}
}
private static Throwable closeNoisePipelineResources(
Throwable failure,
GenerationSessionLease lease,
GenerationHistoryRuntimeRouter.RuntimeRoute route,
BukkitChunkGenerator.GenerationStagePermit stage
) {
Throwable result = failure;
try {
lease.close();
} catch (Throwable closeFailure) {
result = appendFailure(result, closeFailure);
}
try {
if (route != null) {
route.close();
}
} catch (Throwable closeFailure) {
result = appendFailure(result, closeFailure);
}
try {
stage.close();
} catch (Throwable closeFailure) {
result = appendFailure(result, closeFailure);
}
return result;
}
private static void closeHistoryRoute(
GenerationHistoryRuntimeRouter.RuntimeRoute route,
Throwable failure
) {
if (route == null) {
return;
}
try {
route.close();
} catch (Throwable closeFailure) {
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
}
}
private static Throwable appendFailure(Throwable failure, Throwable closeFailure) {
if (failure == null) {
return closeFailure;
}
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
return failure;
}
private static boolean isCancellationFailure(Throwable failure) {
Throwable current = failure;
while (current instanceof CompletionException && current.getCause() != null) {
@@ -739,6 +875,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns");
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_mob_spawns");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition);
}
}
private WeightedList<MobSpawnSettings.SpawnerData> getMobsAtWithActiveRuntime(
Holder<Biome> holder,
StructureManager structuremanager,
MobCategory enumcreaturetype,
BlockPos blockposition
) {
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
if (vanillaSpawnBiome == null) {
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
@@ -759,18 +909,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return explicitSpawns;
}
IrisDimension spawnDimension = engine.getDimension();
int spawnRuntimeId = engine.getCacheID();
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
synchronized (mergedSpawnTables) {
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
mergedSpawnTables.clear();
spawnTableDimension = spawnDimension;
spawnTableRuntimeId = spawnRuntimeId;
}
}
}
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), enumcreaturetype);
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
}
@@ -786,19 +926,26 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
chunkPos.x(), chunkPos.z(), "bukkit_nms_biome_decoration");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
// feature-order cycle is reported once and degraded to features-off.
importedFeatures.prepare(generatoraccessseed);
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
// dimension set importedFeatures.enabled.
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)) {
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
// feature-order cycle is reported once and degraded to features-off.
importedFeatures.prepare(generatoraccessseed);
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
// dimension set importedFeatures.enabled.
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
}
claimGeneratedSemantics(route, "bukkit_nms_biome_decoration");
}
}
@@ -850,6 +997,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
BoundingBox footprint = start.getBoundingBox();
if (!engine.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
footprint.maxZ())) {
continue;
}
NativeStructureOwnershipRecord ownership =
NativeStructureOwnershipRecovery.resolve(
engine, world.getLevel(), structureId, structure, start);
@@ -1001,13 +1156,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
int runtimeId = engine.getCacheID();
StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
return cached.structures();
}
synchronized (this) {
cached = structureStepCache;
if (cached != null && cached.registry() == registry) {
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
return cached.structures();
}
int steps = GenerationStep.Decoration.values().length;
@@ -1022,7 +1178,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
grouped.set(step, List.copyOf(grouped.get(step)));
}
List<List<Structure>> resolved = List.copyOf(grouped);
structureStepCache = new StructureStepCache(registry, resolved);
structureStepCache = new StructureStepCache(runtimeId, registry, resolved);
return resolved;
}
}
@@ -1054,7 +1210,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
ChunkPos chunkPos = chunkAccess.getPos();
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
chunkPos.getMinBlockX(), chunkPos.getMinBlockZ(), "bukkit_nms_heightmaps");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin();
@@ -1085,22 +1244,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome;
if (engine.getDimensionStackContext() == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} else {
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
center.getMinBlockX() + 8,
center.getMinBlockZ() + 8);
if (visibleBiome == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_spawn_original_mobs");
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
center.x(), center.z(), "bukkit_nms_spawn_original_mobs");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_spawn_original_mobs");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!allowsRoutedDiscreteGeneration(region.getChunk(center.x(), center.z()), ChunkStatus.SPAWN)) {
return;
}
Holder<Biome> visibleBiome;
if (engine.getDimensionStackContext() == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} else {
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
center.getMinBlockX() + 8,
center.getMinBlockZ() + 8);
if (visibleBiome == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
}
}
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
}
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
}
private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) {
if (chunk.getPersistedStatus().isOrAfter(stage)) {
return false;
}
ChunkPos chunkPos = chunk.getPos();
return engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
}
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
@@ -1133,7 +1310,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
i, j, "bukkit_nms_base_height");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
int height = engine.getDimensionStackContext() == null
@@ -1148,7 +1327,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
i, j, "bukkit_nms_base_column");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
boolean dimensionStack = engine.getDimensionStackContext() != null;
int block = dimensionStack
@@ -1181,6 +1362,69 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
}
private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(
int chunkX,
int chunkZ,
String operation
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
if (platformGenerator != null && platformGenerator.getGenerationHistory() != null) {
throw new IllegalStateException("Iris " + operation
+ " has no generation-history runtime router.");
}
return null;
}
try {
return router.openRoute(chunkX, chunkZ);
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation
+ " could not route generation history for chunk "
+ chunkX + "," + chunkZ + ".", failure);
}
}
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
int blockX,
int blockZ,
String operation
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
try {
return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ);
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation
+ " could not route generation history at "
+ blockX + "," + blockZ + ".", failure);
}
}
private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope(
GenerationHistoryRuntimeRouter.RuntimeRoute route
) {
return route == null ? null : route.openRuntimeScope();
}
private static void claimGeneratedSemantics(
GenerationHistoryRuntimeRouter.RuntimeRoute route,
String operation
) {
if (route == null) {
return;
}
try {
route.claimGeneratedSemantics();
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation
+ " could not durably claim generated chunk semantics.", failure);
}
}
private BukkitChunkGenerator.GenerationStagePermit requireGenerationStage(String operation) {
return platformGenerator == null
? BukkitChunkGenerator.GenerationStagePermit.noop()
@@ -1252,13 +1496,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
}
private record SpawnTableKey(Biome biome, MobCategory category) {
private record SpawnTableKey(int runtimeId, Biome biome, MobCategory category) {
}
private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set<String> keys) {
}
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
private record StructureStepCache(int runtimeId, Registry<Structure> registry,
List<List<Structure>> structures) {
}
record StudioStructureState(
@@ -33,6 +33,7 @@ import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -57,6 +58,9 @@ 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;
@@ -169,7 +173,6 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -181,11 +184,22 @@ public class NMSBinding implements INMSBinding {
private final AtomicBoolean injected = new AtomicBoolean();
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<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final AtomicCache<Method> byIdRef = new AtomicCache<>();
@Override
public PlatformGenerationRegistry generationRegistry() {
return new NmsGenerationRegistry(
this::registry,
"bukkit-generation-registry-v1",
"bukkit-v26_2_R1-generated-registry-json-v1"
);
}
private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source);
@@ -1544,6 +1558,23 @@ public class NMSBinding implements INMSBinding {
}
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)
@@ -1579,18 +1610,22 @@ public class NMSBinding implements INMSBinding {
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
partialStorageAccess,
partialPluginClassLoader
}) {
if (partial == null) {
continue;
}
try {
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable ignored) {
} catch (Throwable cleanupFailure) {
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
}
}
return false;
@@ -1637,16 +1672,52 @@ public class NMSBinding implements INMSBinding {
}
@Override
public boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer();
return ServerShutdownBoundary.await(
public boolean isServerStopping() {
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
}
@Override
public ServerShutdownBoundary createServerShutdownBoundary() {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
return new ServerShutdownBoundary(
() -> server.hasFullyShutdown,
server.getRunningThread(),
timeout,
unit
server.getRunningThread()
);
}
@Override
public void deferPluginClassLoaderClose() {
synchronized (injected) {
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
return;
}
Agent.retainClassLoader(getClass().getClassLoader());
pluginClassLoaderCloseDeferred = true;
}
}
@Override
public void releasePluginClassLoaderClose() {
ResettableClassFileTransformer transformer;
boolean releaseLoader;
synchronized (injected) {
transformer = pluginClassLoaderTransformer;
pluginClassLoaderTransformer = null;
releaseLoader = pluginClassLoaderCloseDeferred;
pluginClassLoaderCloseDeferred = false;
}
try {
if (transformer != null
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
}
} finally {
if (releaseLoader) {
Agent.releaseClassLoader(getClass().getClassLoader());
}
}
}
@Override
public KMap<Material, List<BlockProperty>> getBlockProperties() {
KMap<Material, List<BlockProperty>> states = new KMap<>();
@@ -1691,6 +1762,39 @@ public class NMSBinding implements INMSBinding {
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) {
@@ -0,0 +1,182 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.dimension.DimensionType;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Supplier;
final class NmsGenerationRegistry implements PlatformGenerationRegistry {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
private static final String BLOCK_REGISTRY = "minecraft:block";
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
private final Supplier<RegistryAccess> registryAccess;
private final String runtimeIdentity;
private final String rendererIdentity;
NmsGenerationRegistry(
Supplier<RegistryAccess> registryAccess,
String runtimeIdentity,
String rendererIdentity
) {
this.registryAccess = Objects.requireNonNull(registryAccess, "registryAccess");
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
}
@Override
public String runtimeIdentity() {
return runtimeIdentity;
}
@Override
public String generatedDefinitionRendererIdentity() {
return rendererIdentity;
}
@Override
public String customBiomeResourceKey(String identitySha256) {
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
}
@Override
public List<String> legacyCustomBiomeResourceKeys(
String packName,
String dimensionKey,
String customBiomeId
) {
return List.of(requireText(dimensionKey, "dimensionKey").toLowerCase(Locale.ROOT)
+ ":" + requireText(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT));
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return "iris:" + requireText(dimensionTypeKey, "dimensionTypeKey").toLowerCase(Locale.ROOT);
}
@Override
public Definition definition(String registryKey, String resourceKey) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
case DIMENSION_TYPE_REGISTRY -> presence(
access,
Registries.DIMENSION_TYPE,
registryKey,
resourceKey
);
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
};
}
@Override
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
default -> throw new IllegalArgumentException(
"Registry does not accept generated JSON definitions: " + registryKey
);
};
}
@Override
public Definition generatedDefinition(String registryKey, String resourceKey) {
RegistryAccess access = Objects.requireNonNull(registryAccess.get(), "Minecraft registry access");
return switch (registryKey) {
case BIOME_REGISTRY -> exactRegistered(
access,
Registries.BIOME,
Biome.DIRECT_CODEC,
resourceKey
);
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
access,
Registries.DIMENSION_TYPE,
DimensionType.DIRECT_CODEC,
resourceKey
);
default -> throw new IllegalArgumentException(
"Registry does not contain Iris-generated definitions: " + registryKey
);
};
}
private static <T> Definition presence(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
String registryResourceKey,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
+ " / " + resourceKey + ".");
}
return Definition.resourceIdentity(registryResourceKey, resourceKey);
}
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static <T> Definition exactRegistered(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
Codec<T> codec,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing generated registry definition "
+ registryKey.identifier() + " / " + resourceKey + ".");
}
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static Identifier requireIdentifier(String resourceKey) {
Identifier identifier = Identifier.tryParse(resourceKey);
if (identifier == null) {
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
}
return identifier;
}
private static String requireText(String value, String label) {
String required = Objects.requireNonNull(value, label).trim();
if (required.isEmpty()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
return required;
}
}
@@ -42,10 +42,10 @@ public class CustomBiomeSourceStructureContractTest {
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
assertFalse(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
assertTrue(source.contains("vanillaSpawnBiomes.get(biome.value())"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
assertTrue(source.contains("runtimeBiomeState().vanillaSpawnBiomes().get(biome.value())"));
assertTrue(source.contains("requireGenerationLease(\n \"bukkit_structure_biome\""));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
assertTrue(source.contains("requireGenerationLease(\n \"bukkit_visible_biome\""));
assertTrue(source.contains("if (engine.isClosed())"));
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
assertTrue(source.contains("catch (GenerationSessionException e)"));
@@ -140,7 +140,7 @@ public class CustomBiomeSourceStructureContractTest {
assertTrue(lookupEnd > lookupStart);
String lookup = source.substring(lookupStart, lookupEnd);
assertTrue(lookup.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
assertTrue(lookup.contains("requireGenerationLease(\n \"bukkit_visible_biome\""));
assertTrue(lookup.contains("prepareVisibleBiomeBatch()"));
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()")
@@ -0,0 +1,164 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class GenerationHistoryRoutingContractTest {
@Test
public void everyChunkStageScopesItsHistoryRuntimeBeforeSessionAdmission() throws IOException {
String source = chunkGeneratorSource();
assertRoutedStage(source, "public void createStructures(", "private void adjustGeneratedStructures");
assertRoutedStage(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
assertRoutedStage(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface");
assertRoutedStage(source, "public void buildSurface(", "public void applyCarvers(");
assertRoutedStage(source, "public void applyCarvers(", "public CompletableFuture<ChunkAccess> fillFromNoise");
assertRoutedStage(source, "public void spawnOriginalMobs(", "private boolean allowsRoutedDiscreteGeneration(");
assertRoutedStage(
source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
"public BiomeGenerationSettings getBiomeGenerationSettings");
String baseHeight = method(source, "public int getBaseHeight(", "public NoiseColumn getBaseColumn(");
String baseColumn = method(source, "public NoiseColumn getBaseColumn(", "private GenerationSessionLease requireGenerationLease");
assertBefore(baseHeight, "openHistoryCoordinateScope(", "engine.acquireGenerationLease(");
assertBefore(baseColumn, "openHistoryCoordinateScope(", "engine.acquireGenerationLease(");
}
@Test
public void asyncNoiseKeepsRouteUntilCompletionAndReopensThreadLocalScopes() throws IOException {
String source = chunkGeneratorSource();
String noise = method(
source,
"public CompletableFuture<ChunkAccess> fillFromNoise",
"private static boolean isCancellationFailure");
assertBefore(noise, "requireNoiseGenerationStage(", "openHistoryRoute(");
assertBefore(noise, "openHistoryRoute(", "requireGenerationLease(");
assertTrue(count(noise, "openHistoryRuntimeScope(route)") >= 2);
assertBefore(noise, ".thenApply(filled ->", "pipeline.whenComplete(");
assertTrue(noise.contains("closeNoisePipelineResources(\n failure, lease, route, stage)"));
}
@Test
public void transitionBandSuppressesDiscreteGenerationAndCrossingStarts() throws IOException {
String source = chunkGeneratorSource();
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
String adjustment = method(source, "private void adjustGeneratedStructures", "public ChunkGeneratorStructureState createState");
String decoration = method(
source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
"public BiomeGenerationSettings getBiomeGenerationSettings");
assertTrue(structures.contains("allowsRoutedDiscreteGeneration(access, ChunkStatus.STRUCTURE_STARTS)"));
assertTrue(decoration.contains("allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)"));
String discrete = method(source, "private boolean allowsRoutedDiscreteGeneration(",
"private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(");
assertTrue(discrete.contains("chunk.getPersistedStatus().isOrAfter(stage)"));
assertTrue(discrete.contains("engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z())"));
assertBefore(decoration, "delegate.applyBiomeDecoration(", "claimGeneratedSemantics(route,");
assertTrue(adjustment.contains("start.getBoundingBox()"));
assertTrue(adjustment.contains("allowsNewGenerationFootprint("));
assertTrue(adjustment.contains("NativeStructureOwnershipStore.discard("));
}
@Test
public void biomeCachesAndVisibleHistoricalBiomesAreRuntimeAware() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")))
.replace("\r\n", "\n");
String visibleResolution = method(
source,
"private Holder<Biome> resolveVisibleBiomeHolder(",
"private Holder<Biome> resolveCustomHolder(");
String activeBatch = method(
source,
"Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(\n int x,",
"private GenerationSessionLease tryAcquireGenerationLease");
assertTrue(source.contains("record RuntimeNoiseKey(int runtimeId, long coordinateKey)"));
assertTrue(source.contains("record RuntimeColumnKey(int runtimeId, long coordinateKey)"));
assertTrue(source.contains("ConcurrentHashMap<Integer, RuntimeBiomeState> runtimeBiomeStates"));
assertTrue(source.contains("addGenerationRuntimeRetirementListener(this::evictRuntimeCaches)"));
assertTrue(source.contains("runtimeBiomeStates.remove(runtimeId)"));
assertTrue(source.contains("noiseBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
assertBefore(visibleResolution, "historicalPhysicalBiomeKeyAt(", "resolveBiomeResolution(");
assertFalse(activeBatch.contains("openHistoryCoordinateScope("));
String chunkGenerator = chunkGeneratorSource();
assertTrue(chunkGenerator.contains("addGenerationRuntimeRetirementListener(this::evictRuntimeCaches)"));
assertTrue(chunkGenerator.contains("mergedSpawnTables.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
}
@Test
public void importedFeatureTablesAreRuntimeScopedAndRetiredCentrally() throws IOException {
String stage = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource"))
.resolveSibling("ImportedFeatureStage.java"))
.replace("\r\n", "\n");
String generator = chunkGeneratorSource();
assertTrue(stage.contains("ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates"));
assertTrue(stage.contains("runtimeStates.get(engine.getCacheID())"));
assertTrue(stage.contains("runtimeStates.remove(runtimeId)"));
assertFalse(stage.contains("volatile FeatureTable featureTable"));
assertFalse(stage.contains("volatile int settledRuntimeId"));
assertTrue(generator.contains("importedFeatures.evictRuntime(runtimeId)"));
}
@Test
public void nativeReferencesResolvePolicyInTheOriginRuntime() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource"))
.resolveSibling("NativeStructureReferenceRepair.java"))
.replace("\r\n", "\n");
String references = method(source, "public static void createReferences(",
"private static GenerationHistoryRuntimeRouter.CoordinateScope openOriginRuntimeScope(");
assertBefore(references, "openOriginRuntimeScope(engine, originChunkX, originChunkZ)", "scanStart(");
assertBefore(references, "scanStart(", "isTargetRelevant(");
assertTrue(source.contains("irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4)"));
assertFalse(references.contains("allowsNewGenerationFootprint("));
}
private static void assertRoutedStage(String source, String start, String end) {
String stage = method(source, start, end);
assertBefore(stage, "requireGenerationStage(", "openHistoryRoute(");
assertBefore(stage, "openHistoryRoute(", "requireGenerationLease(");
assertBefore(stage, "openHistoryRuntimeScope(route)", "requireGenerationLease(");
}
private static String chunkGeneratorSource() throws IOException {
return Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")))
.replace("\r\n", "\n");
}
private static String method(String source, String startToken, String endToken) {
int start = source.indexOf(startToken);
int end = source.indexOf(endToken, start);
assertTrue("Missing source contract token: " + startToken, start >= 0);
assertTrue("Missing source contract token: " + endToken, end > start);
return source.substring(start, end);
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static int count(String source, String token) {
int count = 0;
int index = 0;
while ((index = source.indexOf(token, index)) >= 0) {
count++;
index += token.length();
}
return count;
}
}
@@ -295,26 +295,29 @@ public class IrisChunkGeneratorFailureContractTest {
assertBefore(productionFill,
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
"GenerationHistoryRuntimeRouter.RuntimeRoute route");
assertBefore(productionFill,
"GenerationHistoryRuntimeRouter.RuntimeRoute route",
"GenerationSessionLease lease");
assertBefore(productionFill,
"requireNoiseGenerationStage(",
"\"bukkit_nms_chunk_pipeline\")");
assertBefore(productionFill,
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")",
".fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)");
"delegate.fillFromNoise(");
assertTrue(productionFill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
assertTrue(productionFill.contains("openHistoryRuntimeScope(route)"));
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "lease.close();");
assertBefore(completion, "lease.close();", "stage.close();");
assertBefore(completion, "stage.close();", "completion.complete(filled)");
assertBefore(completion, "stage.close();", "completion.cancel(false)");
assertBefore(completion, "stage.close();", "completion.completeExceptionally(failure)");
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "closeNoisePipelineResources(");
assertBefore(completion, "closeNoisePipelineResources(", "completion.complete(filled)");
assertBefore(completion, "closeNoisePipelineResources(", "completion.cancel(false)");
assertBefore(completion, "closeNoisePipelineResources(", "completion.completeExceptionally(completionFailure)");
assertTrue(completion.contains("else if (cancelled)"));
assertFalse(completion.contains("pipeline.isCancelled()"));
assertFalse(completion.contains("finally"));
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)"));
assertTrue(productionFill.contains("lease.close();\n stage.close();\n throw failure;"));
assertTrue(productionFill.contains("lease.close();\n closeHistoryRoute(route, failure);\n stage.close();\n throw failure;"));
assertTrue(productionFill.contains("return completion;"));
assertFalse(productionFill.contains("return pipeline;"));
assertFalse(productionFill.contains("pipeline.cancel("));
@@ -422,6 +425,9 @@ public class IrisChunkGeneratorFailureContractTest {
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
String biomes = method(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface");
String surface = method(source, "public void buildSurface(", "public void applyCarvers(");
String carvers = method(source, "public void applyCarvers(", "public CompletableFuture<ChunkAccess> fillFromNoise");
String mobs = method(source, "public void spawnOriginalMobs(", "private boolean allowsRoutedDiscreteGeneration(");
String noise = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise", "private static boolean isCancellationFailure");
String decoration = method(source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
@@ -430,6 +436,9 @@ public class IrisChunkGeneratorFailureContractTest {
assertStageBeforeLease(structures, "bukkit_nms_create_structures");
assertStageBeforeLease(references, "bukkit_nms_create_references");
assertStageBeforeLease(biomes, "bukkit_nms_create_biomes");
assertStageBeforeLease(surface, "bukkit_nms_build_surface");
assertStageBeforeLease(carvers, "bukkit_nms_apply_carvers");
assertStageBeforeLease(mobs, "bukkit_nms_spawn_original_mobs");
assertBefore(noise,
"requireNoiseGenerationStage(",
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
@@ -437,7 +446,7 @@ public class IrisChunkGeneratorFailureContractTest {
assertBefore(decoration,
"requireGenerationLease(\"bukkit_nms_biome_decoration\")",
"importedFeatures.prepare(generatoraccessseed)");
assertEquals(4, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
assertEquals(7, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
assertEquals(2, occurrences(source, "requireNoiseGenerationStage("));
assertFalse(source.contains("GenerationSessionManager"));
}
@@ -6,16 +6,48 @@ import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
public class NMSBindingShutdownBoundaryContractTest {
@Test
public void shutdownClassificationUsesTheServerStopFlagInsteadOfPluginEnablement() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
String stopping = section(source, "public boolean isServerStopping()", "public ServerShutdownBoundary createServerShutdownBoundary");
assertTrue(stopping.contains("((CraftServer) Bukkit.getServer()).getServer().hasStopped()"));
assertFalse(stopping.contains("isEnabled()"));
assertFalse(stopping.contains("isRunning()"));
}
@Test
public void pluginLoaderGuardUsesSystemAgentIdentityAndIsRemovedAtRelease() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
String injection = section(source, "public boolean injectBukkit()", "public void ensureServerLevelInjection()");
String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class ChunkAccessAdvice");
String release = section(source, "public void releasePluginClassLoaderClose()", "public KMap<Material, List<BlockProperty>> getBlockProperties");
assertTrue(injection.contains("Agent.requireClassLoaderCloseDeferral()"));
assertTrue(injection.contains("getMethod(\"close\").getDeclaringClass()"));
assertTrue(injection.contains("Advice.to(PluginClassLoaderCloseAdvice.class)"));
assertTrue(guard.contains("skipOn = Advice.OnNonDefaultValue.class"));
assertTrue(guard.contains("ClassLoader.getSystemClassLoader()"));
assertTrue(guard.contains("getMethod(\"deferClassLoaderClose\", ClassLoader.class)"));
assertTrue(guard.contains(".invoke(null, loader)"));
assertFalse(guard.contains("Bukkit.getPluginManager()"));
assertTrue(release.indexOf("transformer.reset(") > release.indexOf("pluginClassLoaderCloseDeferred = false"));
assertTrue(release.indexOf("Agent.releaseClassLoader(") > release.indexOf("transformer.reset("));
}
@Test
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
String boundary = section(source, "public boolean awaitServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
assertTrue(boundary.contains("ServerShutdownBoundary.await("));
assertTrue(boundary.contains("new ServerShutdownBoundary("));
assertTrue(boundary.contains("server.hasFullyShutdown"));
assertTrue(boundary.contains("server.getRunningThread()"));
assertTrue(boundary.contains("((CraftServer) Bukkit.getServer()).getServer()"));
assertFalse(boundary.contains("getHandle()"));
}
private static String section(String source, String startMarker, String endMarker) {
@@ -19,6 +19,7 @@ import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
@@ -80,6 +81,30 @@ public class NativeStructureVolumeIndexTest {
assertEquals(coldResolutions, resolver.resolutions());
}
@Test
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
AtomicInteger runtimeId = new AtomicInteger(1);
Engine engine = runtimeEngine(runtimeId);
CountingResolver resolver = new CountingResolver(0, 0);
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(289, resolver.resolutions());
index.resolve(engine, 0, 0, 15, 15);
assertEquals(289, resolver.resolutions());
runtimeId.set(2);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(578, resolver.resolutions());
index.resolve(engine, 0, 0, 15, 15);
assertEquals(578, resolver.resolutions());
index.evictRuntime(1);
runtimeId.set(1);
index.resolve(engine, 0, 0, 15, 15);
assertEquals(867, resolver.resolutions());
}
@Test
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
@@ -188,12 +213,45 @@ public class NativeStructureVolumeIndexTest {
assertTrue(source.contains("isStructureChunk("));
}
@Test
public void originResolutionRoutesAndCachesByScopedRuntime() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
assertTrue(source.contains("openHistoryCoordinateScope(engine, chunkX, chunkZ)"));
assertTrue(source.contains("new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ))"));
assertTrue(source.contains("addGenerationRuntimeRetirementListener(retirementListener)"));
assertTrue(source.contains("originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
assertTrue(source.contains("queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
}
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
}
private static Engine runtimeEngine(AtomicInteger runtimeId) {
return (Engine) Proxy.newProxyInstance(
Engine.class.getClassLoader(),
new Class<?>[]{Engine.class},
(proxy, method, arguments) -> {
if (method.getName().equals("getCacheID")) {
return runtimeId.get();
}
if (method.getName().equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (method.getName().equals("equals")) {
return proxy == arguments[0];
}
if (method.getName().equals("toString")) {
return "runtime-engine-" + runtimeId.get();
}
throw new UnsupportedOperationException(method.getName());
}
);
}
private static int findDisjointWindow(int chunkX, int chunkZ) {
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
for (int candidate = 64; candidate < 16_384; candidate += 17) {
@@ -55,6 +55,7 @@ import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.ServerShutdownBoundary;
import art.arcane.iris.core.gui.BukkitGuiHost;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.BoardSVC;
@@ -139,6 +140,8 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileDescriptor;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@@ -160,6 +163,7 @@ import java.util.regex.Pattern;
@SuppressWarnings("CanBeFinal")
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L;
private static final PrintStream SHUTDOWN_ERRORS = new PrintStream(new FileOutputStream(FileDescriptor.err), true);
private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
@@ -190,6 +194,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
private volatile boolean generatorDrainCompleted;
private volatile PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState;
@@ -206,6 +212,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private BukkitDebugDump debugDump;
private volatile SettingsHotloadWatch settingsHotloadWatch;
private volatile Thread serverLifecycleThread;
private volatile ServerShutdownBoundary serverShutdownBoundary;
public static VolmitSender getSender() {
if (sender == null) {
@@ -427,6 +434,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
if (e == null) {
return;
}
if (instance != null && instance.serverStopTeardownDeferred.get()) {
e.printStackTrace(SHUTDOWN_ERRORS);
return;
}
boolean debug = false;
if (instance != null) {
@@ -466,6 +477,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public static void reportError(String context, Throwable e) {
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
if (instance != null && instance.serverStopTeardownDeferred.get()) {
SHUTDOWN_ERRORS.println("[Iris] " + message);
error.printStackTrace(SHUTDOWN_ERRORS);
return;
}
try {
if (instance != null) {
@@ -603,6 +619,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
sharedRuntimeClosed.set(false);
startupBoundaryRestart.set(false);
terminalCleanupCompleted.set(false);
runtimeTeardownFailed.set(false);
generatorDrainCompleted = false;
deferredShutdownGenerators.clear();
MultiBurst.burst.reopen();
MultiBurst.ioBurst.reopen();
@@ -742,6 +760,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void addShutdownHook() {
removeShutdownHook();
serverLifecycleThread = Thread.currentThread();
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
try {
Runtime.getRuntime().addShutdownHook(shutdownHook);
@@ -963,6 +982,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
drainWorldGenerators(reason, timeoutSeconds);
}
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
return;
}
if (services != null && servicesDisabled.compareAndSet(false, true)) {
// Only services whose onEnable actually completed; disabling a service that
// never initialized runs teardown against uninitialized state.
@@ -970,6 +993,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
service.onDisable();
} catch (Throwable e) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
}
}
@@ -979,8 +1003,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
J.attempt(MultiBurst.burst::close);
J.attempt(MultiBurst.ioBurst::close);
try {
MultiBurst.burst.close();
} catch (Throwable failure) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to close Iris generation workers.", failure);
}
try {
MultiBurst.ioBurst.close();
} catch (Throwable failure) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to close Iris I/O workers.", failure);
}
clearQueues();
IrisServices.clear();
}
@@ -988,6 +1022,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private void quiesceRuntimeForServerShutdown(String reason) {
serverStopTeardownDeferred.set(true);
INMS.get().deferPluginClassLoaderClose();
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
if (jigsawStudioService != null) {
try {
@@ -1060,29 +1095,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return;
}
if (!awaitServerShutdownBoundary()) {
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
return;
}
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
}
private boolean awaitServerShutdownBoundary() {
if (!INMS.isBound()) {
ServerShutdownBoundary boundary = serverShutdownBoundary;
if (boundary == null) {
return true;
}
try {
return INMS.get().awaitServerShutdownBoundary(
return boundary.await(
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
TimeUnit.SECONDS
);
} catch (Throwable e) {
Iris.reportError("Failed to await Paper's post-world-close shutdown boundary.", e);
SHUTDOWN_ERRORS.println("[Iris] Failed to await Paper's post-world-close shutdown boundary.");
e.printStackTrace(SHUTDOWN_ERRORS);
return false;
}
}
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
teardownRuntime(reason, timeoutSeconds);
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup is incomplete; retaining services, pools and the plugin class loader until JVM exit.");
return;
}
finishTerminalCleanup();
}
@@ -1094,12 +1135,36 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
runPostShutdown();
} catch (Throwable e) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
} finally {
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
runtimeTeardownFailed.set(true);
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
}
IrisPlatforms.unbind();
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
} else {
try {
INMS.get().releasePluginClassLoaderClose();
} catch (Throwable failure) {
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
failure.printStackTrace(SHUTDOWN_ERRORS);
}
}
}
}
private boolean hasActiveShutdownResources() {
if (!MultiBurst.burst.isTerminated() || !MultiBurst.ioBurst.isTerminated()) {
return true;
}
return services != null
&& services.get(PreservationSVC.class) instanceof PreservationSVC preservation
&& preservation.hasActiveResources();
}
private void drainWorldGenerators(String reason, long timeoutSeconds) {
List<World> irisWorlds = new ArrayList<>();
List<PlatformChunkGenerator> generators = new ArrayList<>();
@@ -1115,6 +1180,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
}
}
if (generators.isEmpty()) {
generatorDrainCompleted = true;
Iris.debug("No Iris worlds to freeze.");
return;
}
@@ -1139,12 +1205,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try {
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
.get(timeoutSeconds, TimeUnit.SECONDS);
Iris.debug("All Iris chunk generators parked. Safe to unload.");
generatorDrainCompleted = closes.size() == generators.size();
if (generatorDrainCompleted) {
Iris.debug("All Iris chunk generators parked. Safe to unload.");
}
} catch (TimeoutException e) {
Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway.");
Iris.reportError("Iris generator drain timed out after " + timeoutSeconds + "s.", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Iris.warn("Iris generator drain interrupted; unload proceeding.");
Iris.reportError("Iris generator drain was interrupted.", e);
} catch (ExecutionException e) {
Iris.reportError(e.getCause() == null ? e : e.getCause());
}
@@ -765,13 +765,13 @@ public final class BukkitWorldReconciler {
@Override
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
File snapshotRoot = snapshotRoot(worldKey);
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
String configuredWorldName = configuredWorldName(worldKey);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
if (irisDimension == null) {
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
}
File snapshotRoot = snapshotRoot(worldKey);
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
}
private File snapshotRoot(NamespacedKey worldKey) {
@@ -788,7 +788,7 @@ public final class BukkitWorldReconciler {
throw new IllegalStateException("Iris world storage does not match the current platform layout for "
+ worldKey + ".");
}
return IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
return IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot);
}
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.core;
import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader;
@@ -32,6 +33,13 @@ import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
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.GenerationHistoryPaths;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
@@ -255,15 +263,21 @@ public final class IrisWorldGeneratorResolver {
File levelRoot = IrisWorldStorage.levelRoot();
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName());
File pack = IrisWorldStorage.frozenDimensionRoot(
File dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
configuredWorldName,
worldKey
)
.map(IrisWorldStorage::requireFrozenPackRoot)
.orElse(null);
IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id);
IrisDimension dimension = null;
if (dimensionRoot != null) {
long worldSeed = requireStoredWorldSeed(configuredWorldName, dimensionRoot);
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
File pack = requireActivePack(history);
requireSnapshotLoadable(pack);
dimension = requireHistoricalDimension(history, pack, id);
}
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
@@ -445,7 +459,7 @@ public final class IrisWorldGeneratorResolver {
return;
}
if (dimensionRoot != null
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasFrozenPack(dimensionRoot))) {
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasGenerationStorage(dimensionRoot))) {
return;
}
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
@@ -455,9 +469,10 @@ public final class IrisWorldGeneratorResolver {
+ " type=<pack>; Iris registers them with Multiverse itself.");
}
private static boolean hasFrozenPack(File dimensionRoot) {
Path packRoot = dimensionRoot.toPath().toAbsolutePath().normalize().resolve("iris").resolve("pack");
return Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS);
private static boolean hasGenerationStorage(File dimensionRoot) {
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot.toPath());
return Files.isDirectory(paths.generationRoot(), LinkOption.NOFOLLOW_LINKS)
|| Files.isDirectory(paths.legacyPackRoot(), LinkOption.NOFOLLOW_LINKS);
}
private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) {
@@ -475,7 +490,9 @@ public final class IrisWorldGeneratorResolver {
throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for "
+ worldKey + ".");
}
File snapshotRoot = IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
long worldSeed = requireStoredWorldSeed(worldName, dimensionRoot);
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
File snapshotRoot = requireActivePack(history);
try {
requireSnapshotLoadable(snapshotRoot);
} catch (BrokenPackException exception) {
@@ -487,18 +504,14 @@ public final class IrisWorldGeneratorResolver {
throw exception;
}
IrisDimension dimension = IrisData.get(snapshotRoot).getDimensionLoader().load(id, false);
if (dimension == null) {
throw new IllegalStateException("Frozen Iris pack snapshot at " + snapshotRoot
+ " does not contain dimension " + id + ".");
}
IrisDimension dimension = requireHistoricalDimension(history, snapshotRoot, id);
Iris.debug("Assuming IrisDimension: " + dimension.getName());
IrisWorld world = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.seed(history.activeEpoch().worldSeed())
.worldFolder(dimensionRoot)
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
@@ -506,7 +519,133 @@ public final class IrisWorldGeneratorResolver {
Iris.debug("Generator Config: " + world);
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey());
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey(), history);
}
private static GenerationHistory requireGenerationHistory(
File dimensionRoot,
String dimensionKey,
long worldSeed
) {
Path root = dimensionRoot.toPath().toAbsolutePath().normalize();
try {
Optional<GenerationHistory> current = GenerationHistory.openIfPresent(root, worldSeed);
GenerationHistory history = current.isPresent()
? current.get()
: adoptLegacyGenerationHistory(root, dimensionKey, worldSeed);
List<GenerationRegistryContract> retainedContracts = history.manifest()
.epochs()
.stream()
.map(GenerationEpoch::registryContract)
.toList();
history.requireRegistryDefinitions(
GenerationRegistryContractFactory.captureRequiredDefinitions(retainedContracts)
);
return history;
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
}
}
private static GenerationHistory adoptLegacyGenerationHistory(
Path dimensionRoot,
String dimensionKey,
long worldSeed
) throws IOException {
Path legacyPack = GenerationHistoryPaths.forDimension(dimensionRoot).legacyPackRoot();
requireSnapshotLoadable(legacyPack.toFile());
String fingerprint = GenerationPackFingerprint.compute(
legacyPack,
GenerationPackFingerprint.CURRENT_VERSION
);
IrisData data = IrisData.openDatapackCompiler(legacyPack.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IOException("Legacy Iris pack does not contain dimension " + dimensionKey + ".");
}
GenerationEpoch.DimensionContract dimensionContract = captureDimensionContract(data, dimension);
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
data,
dimension,
fingerprint,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
);
return GenerationHistory.adoptLegacyPack(
dimensionRoot,
fingerprint,
worldSeed,
dimensionContract,
registryContract
);
} finally {
data.close();
}
}
private static IrisDimension requireHistoricalDimension(
GenerationHistory history,
File packRoot,
String dimensionKey
) {
GenerationEpoch epoch = history.activeEpoch();
if (!epoch.dimensionContract().dimensionKey().equals(dimensionKey)) {
throw new IllegalStateException("Configured dimension " + dimensionKey
+ " does not match active generation history dimension "
+ epoch.dimensionContract().dimensionKey() + ".");
}
IrisData data = IrisData.get(packRoot);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IllegalStateException("Immutable Iris generation pack at " + packRoot
+ " does not contain dimension " + dimensionKey + ".");
}
GenerationEpoch.DimensionContract actualContract = captureDimensionContract(data, dimension);
if (!epoch.dimensionContract().equals(actualContract)) {
throw new IllegalStateException("Immutable Iris generation pack dimension contract changed at "
+ packRoot + ".");
}
return dimension;
}
private static GenerationEpoch.DimensionContract captureDimensionContract(
IrisData data,
IrisDimension dimension
) {
String dimensionTypeKey = IrisPlatforms.get()
.registries()
.generationRegistry()
.dimensionTypeResourceKey(
data.getDataFolder().getName(),
dimension.getLoadKey(),
dimension.getDimensionTypeKey()
);
return GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
dimensionTypeKey
);
}
private static File requireActivePack(GenerationHistory history) {
try {
return history.activePackRoot().toFile();
} catch (IOException failure) {
throw new IllegalStateException("Active Iris generation pack is unusable.", failure);
}
}
private static long requireStoredWorldSeed(String worldName, File dimensionRoot) {
Long configuredSeed = IrisWorlds.readBukkitWorldSeed(worldName);
if (configuredSeed != null) {
return configuredSeed;
}
try {
return WorldReplacementSeed.readAuthoritativeSeed(dimensionRoot.toPath());
} catch (IOException failure) {
throw new IllegalStateException("Iris world " + worldName
+ " has no authoritative generation seed.", failure);
}
}
private record FreshValidation(
@@ -164,24 +164,28 @@ public final class PendingWorldReplacementManager implements Listener {
boolean configurationApplied = false;
try {
Files.createDirectory(paths.stage());
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
requiredSender,
requiredDimension,
paths.stage().toFile()
);
if (installed == null) {
throw new IOException("Iris could not stage the dimension pack.");
}
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
paths.target(),
paths.stage(),
seedSelection
);
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
requiredSender,
requiredDimension,
paths.stage().toFile(),
effectiveSeed
);
if (installed == null) {
throw new IOException("Iris could not stage the dimension pack.");
}
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId);
}
File stagedPack = paths.stage().resolve("iris/pack").toFile();
File stagedPack = IrisWorldStorage.requireActiveGenerationPackRoot(
paths.stage().toFile(),
effectiveSeed
);
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
transaction = new Transaction(
@@ -806,7 +810,10 @@ public final class PendingWorldReplacementManager implements Listener {
validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind());
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
paths.target().resolve("iris/pack"));
IrisWorldStorage.requireActiveGenerationPackRoot(
paths.target().toFile(),
transaction.seed()
).toPath());
if (!transaction.packFingerprint().equals(fingerprint)) {
throw new IOException("The replacement pack changed before runtime verification.");
}
@@ -176,7 +176,7 @@ public class CommandDeveloper implements DirectorExecutor {
return z ^ (z >>> 31);
}
@Director(description = "Update the pack of a world (UNSAFE!)", descriptionKey = "iris.director.commanddeveloper.director.update_pack_world_unsafe", name = "update-world", aliases = "^world")
@Director(description = "Stage a world generation update", descriptionKey = "iris.director.commanddeveloper.director.stage_world_generation_update", name = "update-world", aliases = "^world")
public void updateWorld(
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
World world,
@@ -202,7 +202,7 @@ public class CommandDeveloper implements DirectorExecutor {
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
pack.getLoadKey()
)) {
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder);
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
} catch (LifecycleOperationCoordinator.BusyException e) {
sender().sendMessage(C.YELLOW + e.getMessage());
}
@@ -28,8 +28,8 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.history.GenerationSemanticQueries;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -41,8 +41,10 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.director.specialhandlers.HydrologyTypeHandler;
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.util.common.director.specialhandlers.LocatableObjectHandler;
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
import art.arcane.iris.util.common.director.specialhandlers.ReachableRegionHandler;
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender;
@@ -90,7 +92,7 @@ public class CommandFind implements DirectorExecutor {
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
public void region(
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look")
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
IrisRegion region,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
boolean teleport
@@ -138,10 +140,6 @@ public class CommandFind implements DirectorExecutor {
return;
}
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
if (runtime == null) {
commandSender.sendMessage(C.YELLOW + "Hydrology is not active in this world.");
return;
}
HydrologyFeatureQuery query;
try {
query = HydrologyFeatureQuery.parse(type);
@@ -150,15 +148,19 @@ public class CommandFind implements DirectorExecutor {
return;
}
Location origin = target.getLocation();
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime.maximumFeatureSearchDistance(
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
int requestedDistance = runtime == null
? 8192
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime == null
? requestedDistance
: runtime.maximumFeatureSearchDistance(
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
J.a(() -> {
try {
HydrologyFeatureRef feature = runtime.nearestFeature(
query.types(),
query.profileKey(),
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
activeEngine,
query,
origin.getBlockX(),
origin.getBlockZ(),
maximumDistance,
@@ -171,15 +173,14 @@ public class CommandFind implements DirectorExecutor {
+ maximumDistance + " blocks.");
return;
}
int worldY = feature.y() + activeEngine.getDimension().getMinHeight();
String label = type + " hydrology feature";
if (!teleport) {
sendStructureMessage(target, commandSender,
C.GREEN + "Found " + label + " at " + feature.x() + ", " + worldY + ", " + feature.z() + ".");
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
return;
}
Location destination = new Location(
target.getWorld(), feature.x(), worldY, feature.z());
target.getWorld(), feature.x(), feature.y(), feature.z());
prepareStructureTeleport(
target, target.getWorld(), commandSender, label, destination, true);
} catch (Throwable error) {
@@ -233,6 +234,12 @@ public class CommandFind implements DirectorExecutor {
registered, decision, nativePlacement, locatableNativePlacement,
locatableEditablePlacement, nativeGenerationEnabled, reachable);
if (route != StructureLookupRoute.NATIVE && route != StructureLookupRoute.IRIS
&& GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
if (route == StructureLookupRoute.UNKNOWN) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
return;
@@ -672,7 +679,7 @@ public class CommandFind implements DirectorExecutor {
J.a(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
GenerationSemanticQueries.nearestStructure(engine, structure, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
sendStructureMessage(target, commandSender,
C.YELLOW + "Unable to locate " + structure
@@ -697,7 +704,7 @@ public class CommandFind implements DirectorExecutor {
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
public void object(
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = ObjectHandler.class)
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = LocatableObjectHandler.class)
String object,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
boolean teleport
@@ -721,7 +728,7 @@ public class CommandFind implements DirectorExecutor {
}
}
if (e.hasObjectPlacement(object)) {
if (GenerationFindCatalog.hasObjectPlacement(e, object)) {
EngineBukkitOps.gotoObject(e, object, player(), teleport);
return;
}
@@ -279,12 +279,14 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
dispatchSenders.set(sender);
DirectorContext.touch(new VolmitSender(sender));
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
} catch (Throwable e) {
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return DirectorExecutionResult.notHandled();
} finally {
DirectorContext.remove();
dispatchSenders.remove();
}
}
@@ -9,6 +9,40 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class IrisShutdownOrderingTest {
@Test
public void serverStopHoldsThePluginLoaderUntilTerminalCleanupCompletes() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String quiesce = section(source, "private void quiesceRuntimeForServerShutdown", "private void startPostStopFinisher");
String cleanup = section(source, "private void finishTerminalCleanup()", "private void drainWorldGenerators");
String teardown = section(source, "private void teardownRuntime", "private void quiesceRuntimeForServerShutdown");
String deferred = section(source, "private void finishDeferredRuntimeTeardown", "private void finishTerminalCleanup");
assertOrdered(quiesce, "serverStopTeardownDeferred.set(true)", "deferPluginClassLoaderClose()",
"jigsawStudioService.quiesceForServerShutdown()");
assertOrdered(cleanup, "runPostShutdown()", "hasActiveShutdownResources()", "IrisPlatforms.unbind()",
"runtimeTeardownFailed.get()", "releasePluginClassLoaderClose()");
assertTrue(cleanup.contains("!MultiBurst.burst.isTerminated() || !MultiBurst.ioBurst.isTerminated()"));
assertTrue(cleanup.contains("preservation.hasActiveResources()"));
assertOrdered(teardown, "drainWorldGenerators(reason, timeoutSeconds)", "!generatorDrainCompleted",
"return;", "service.onDisable()");
assertOrdered(deferred, "teardownRuntime(reason, timeoutSeconds)", "!generatorDrainCompleted",
"SHUTDOWN_ERRORS.println", "return;", "finishTerminalCleanup()");
}
@Test
public void deferredShutdownErrorsBypassClosedLoggingServices() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String throwableReport = section(source, "public static void reportError(Throwable e)",
"public static void reportError(String context, Throwable e)");
String contextualReport = section(source, "public static void reportError(String context, Throwable e)",
"public static void dump()");
assertOrdered(throwableReport, "serverStopTeardownDeferred.get()",
"e.printStackTrace(SHUTDOWN_ERRORS)", "boolean debug = false");
assertOrdered(contextualReport, "serverStopTeardownDeferred.get()",
"SHUTDOWN_ERRORS.println(", "error.printStackTrace(SHUTDOWN_ERRORS)", "Iris.error(message)");
}
@Test
public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
@@ -18,6 +52,22 @@ public class IrisShutdownOrderingTest {
assertFalse("Shutdown must not close Mantle plates before generation drains", drain.contains("saveAllNow()"));
}
@Test
public void shutdownBoundaryIsPreparedBeforeHookRegistrationAndDoesNotUseClosedBindings() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
String registration = section(source, "public void addShutdownHook()", "public void removeShutdownHook()");
String boundary = section(source, "private boolean awaitServerShutdownBoundary()",
"private void finishDeferredRuntimeTeardown");
assertOrdered(registration, "createServerShutdownBoundary()", "new Thread(this::runShutdownHook",
"Runtime.getRuntime().addShutdownHook");
assertTrue(boundary.contains("boundary.await("));
assertFalse(boundary.contains("INMS.get()"));
assertFalse(boundary.contains("Iris.reportError"));
assertTrue(source.contains("new FileOutputStream(FileDescriptor.err)"));
assertOrdered(boundary, "SHUTDOWN_ERRORS.println(", "e.printStackTrace(SHUTDOWN_ERRORS)", "return false;");
}
@Test
public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
@@ -43,6 +93,7 @@ public class IrisShutdownOrderingTest {
"finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)",
"return;",
"awaitServerShutdownBoundary()",
"SHUTDOWN_ERRORS.println",
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
assertOrdered(finisher,
"finisher.setDaemon(false)",
@@ -85,8 +136,8 @@ public class IrisShutdownOrderingTest {
assertOrdered(teardown,
"drainWorldGenerators(reason, timeoutSeconds)",
"service.onDisable()",
"MultiBurst.burst::close",
"MultiBurst.ioBurst::close");
"MultiBurst.burst.close()",
"MultiBurst.ioBurst.close()");
assertTrue("Engine service must retain its generator close behind deferred service teardown",
engineDisable.contains("startClose("));
assertTrue("Studio service must retain its generator close behind deferred service teardown",
@@ -195,7 +195,7 @@ public class IrisWorldGeneratorResolverTest {
}
@Test
public void configuredWorldResolutionUsesOnlyFrozenWorldLocalSnapshot() throws Exception {
public void configuredWorldResolutionUsesOnlyActiveImmutableGenerationPack() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n");
int resolverStart = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator(");
@@ -208,26 +208,32 @@ public class IrisWorldGeneratorResolverTest {
"Frozen Iris world storage does not match the current platform layout",
currentPlatformRoot
);
int snapshotRoot = resolver.indexOf("IrisWorldStorage.requireFrozenPackRoot(dimensionRoot)");
int history = resolver.indexOf("requireGenerationHistory(dimensionRoot, id, worldSeed)");
int snapshotRoot = resolver.indexOf("requireActivePack(history)", history);
int validation = resolver.indexOf("requireSnapshotLoadable(snapshotRoot)");
int exactLoad = resolver.indexOf(
"IrisData.get(snapshotRoot).getDimensionLoader().load(id, false)"
);
int exactLoad = resolver.indexOf("requireHistoricalDimension(history, snapshotRoot, id)");
int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())");
int historicalSeed = resolver.indexOf(".seed(history.activeEpoch().worldSeed())");
int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)");
assertTrue(dimensionRoot >= 0);
assertTrue(currentPlatformRoot > dimensionRoot);
assertTrue(layoutRefusal > currentPlatformRoot);
assertTrue(snapshotRoot > layoutRefusal);
assertTrue(history > layoutRefusal);
assertTrue(snapshotRoot > history);
assertTrue(validation > snapshotRoot);
assertTrue(exactLoad > validation);
assertTrue(canonicalIdentity > exactLoad);
assertTrue(resolvedStorage > canonicalIdentity);
assertTrue(historicalSeed > canonicalIdentity);
assertTrue(resolvedStorage > historicalSeed);
assertFalse(resolver.contains("loadDimension("));
assertFalse(resolver.contains("loadAnyDimension("));
assertFalse(resolver.contains("replaceIntoWorld("));
assertFalse(resolver.contains("installIntoWorld("));
assertFalse(resolver.contains(".seed(1337)"));
assertTrue(resolver.contains("GenerationHistory.openIfPresent(root, worldSeed)"));
assertTrue(resolver.contains("GenerationHistory.adoptLegacyPack("));
assertTrue(resolver.contains("GenerationRegistryContractFactory.captureRequiredDefinitions("));
}
@Test
@@ -0,0 +1,26 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class CommandDeveloperGenerationHistoryTest {
@Test
public void updateWorldStagesThePackWithTheLiveWorldSeed() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/commands/CommandDeveloper.java"
)).replace("\r\n", "\n");
int updateStart = source.indexOf("public void updateWorld(");
int updateEnd = source.indexOf("@Director(description = \"Test\"", updateStart);
String update = source.substring(updateStart, updateEnd);
assertTrue(update.contains(
"replaceIntoWorld(sender(), pack, folder, world.getSeed())"
));
assertFalse(update.contains("iris/pack"));
}
}
@@ -0,0 +1,26 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class CommandFindRiverContractTest {
@Test
public void riverLookupMergesRecordedAndEligibleActiveFeatures() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
int methodStart = source.indexOf("public void river(");
int methodEnd = source.indexOf("public void structure(", methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("GenerationSemanticQueries.nearestRiver("));
assertTrue(method.contains("runtime == null"));
assertTrue(method.contains("feature.y()"));
assertFalse(method.contains("runtime.nearestFeature("));
assertFalse(method.contains("feature.y() + activeEngine.getDimension().getMinHeight()"));
}
}
@@ -0,0 +1,27 @@
package art.arcane.iris.core.commands;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class CommandFindStructureHistoryContractTest {
@Test
public void admitsRetainedStructuresBeforeActiveOnlyRejectionsAndUsesSemanticSearch() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.commandFindSource")));
int structureStart = source.indexOf("public void structure(");
int retainedAdmission = source.indexOf("GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)", structureStart);
int unknownRejection = source.indexOf("if (route == StructureLookupRoute.UNKNOWN)", structureStart);
assertTrue(retainedAdmission > structureStart && retainedAdmission < unknownRejection);
int locateStart = source.indexOf("private void locateIrisStructure(");
int locateEnd = source.indexOf("public void object(", locateStart);
String locate = source.substring(locateStart, locateEnd);
assertTrue(locate.contains("GenerationSemanticQueries.nearestStructure("));
assertFalse(locate.contains("IrisStructureLocator.locate("));
}
}
@@ -30,7 +30,7 @@ public class BukkitEngineLifecycleContractTest {
String baseHeight = method(source, "public int getBaseHeight(");
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
assertTrue(baseHeight.contains("catch (GenerationSessionException e)"));
assertTrue(baseHeight.contains("catch (GenerationSessionException | IOException e)"));
String generation = method(source, "public void generateNoise(");
assertTrue(generation.contains("throw new IllegalStateException"));
@@ -0,0 +1,155 @@
package art.arcane.iris.core.service;
import art.arcane.iris.Iris;
import art.arcane.iris.core.commands.CommandIris;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.director.DirectorEngineOptions;
import art.arcane.volmlib.util.director.compat.DirectorEngineFactory;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionDispatcher;
import art.arcane.volmlib.util.director.runtime.DirectorExecutionResult;
import art.arcane.volmlib.util.director.runtime.DirectorRuntimeEngine;
import art.arcane.volmlib.util.plugin.ComponentMessenger;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
public class CommandSVCFindParsingTest {
@Test
public void registeredBiomeHandlerResolvesActiveThenRemovedNamesDuringParsing() throws Exception {
Fixture fixture = new Fixture();
TestCommandService service = new TestCommandService(DirectorExecutionDispatcher.IMMEDIATE);
IrisBiome plateau = biome("plateau");
IrisBiome meadow = biome("meadow");
try (MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
MockedStatic<GenerationFindCatalog> catalog = mockStatic(GenerationFindCatalog.class);
MockedStatic<EngineBukkitOps> operations = mockStatic(EngineBukkitOps.class)) {
toolbelt.when(() -> IrisToolbelt.access(fixture.world)).thenReturn(fixture.generator);
catalog.when(() -> GenerationFindCatalog.biomes(fixture.engine))
.thenReturn(new KList<>(plateau, meadow));
assertTrue(execute(service, fixture.player, "biome", "biome=plateau").isSuccess());
assertNull(DirectorContext.get());
assertTrue(execute(service, fixture.player, "biome", "biome=meadow").isSuccess());
assertNull(DirectorContext.get());
operations.verify(() -> EngineBukkitOps.gotoBiome(same(fixture.engine), same(plateau), same(fixture.player), eq(true)));
operations.verify(() -> EngineBukkitOps.gotoBiome(same(fixture.engine), same(meadow), same(fixture.player), eq(true)));
catalog.verify(() -> GenerationFindCatalog.biomes(fixture.engine), times(2));
catalog.verifyNoMoreInteractions();
}
}
@Test
public void registeredHistoricalHandlersParseBeforeDeferredInvocationAndClearFailedContext() throws Exception {
Fixture fixture = new Fixture();
List<Runnable> queued = new ArrayList<>();
TestCommandService service = new TestCommandService((mode, action) -> queued.add(action));
IrisBiome meadow = biome("meadow");
IrisRegion oldRegion = new IrisRegion();
oldRegion.setLoadKey("old-region");
try (MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
MockedStatic<GenerationFindCatalog> catalog = mockStatic(GenerationFindCatalog.class);
MockedStatic<EngineBukkitOps> operations = mockStatic(EngineBukkitOps.class);
MockedStatic<ComponentMessenger> messages = mockStatic(ComponentMessenger.class)) {
toolbelt.when(() -> IrisToolbelt.access(fixture.world)).thenReturn(fixture.generator);
catalog.when(() -> GenerationFindCatalog.biomes(fixture.engine)).thenReturn(new KList<>(meadow));
catalog.when(() -> GenerationFindCatalog.regions(fixture.engine)).thenReturn(new KList<>(oldRegion));
catalog.when(() -> GenerationFindCatalog.objectKeys(fixture.engine)).thenReturn(new KList<>("old-tree"));
operations.when(() -> EngineBukkitOps.gotoBiome(fixture.engine, meadow, fixture.player, true))
.thenAnswer(invocation -> {
assertSame(fixture.player, DirectorContext.get().player());
return null;
});
assertTrue(execute(service, fixture.player, "biome", "biome=meadow").isSuccess());
assertTrue(execute(service, fixture.player, "region", "region=old-region").isSuccess());
assertTrue(execute(service, fixture.player, "object", "object=old-tree").isSuccess());
assertEquals(3, queued.size());
assertNull(DirectorContext.get());
queued.getFirst().run();
assertNull(DirectorContext.get());
operations.verify(() -> EngineBukkitOps.gotoBiome(fixture.engine, meadow, fixture.player, true));
DirectorExecutionResult rejected = execute(service, fixture.player, "biome", "biome=authoring-only");
assertFalse(rejected.isSuccess());
assertTrue(rejected.getMessage().contains("Cannot convert"));
assertEquals(3, queued.size());
assertNull(DirectorContext.get());
catalog.verify(() -> GenerationFindCatalog.regions(fixture.engine));
catalog.verify(() -> GenerationFindCatalog.objectKeys(fixture.engine));
}
}
private static DirectorExecutionResult execute(CommandSVC service, Player player, String command, String argument)
throws Exception {
Method run = CommandSVC.class.getDeclaredMethod("runDirector", CommandSender.class, String.class, String[].class);
run.setAccessible(true);
return (DirectorExecutionResult) run.invoke(service, player, "iris",
new String[]{"find", command, argument, "teleport=true"});
}
private static IrisBiome biome(String key) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
return biome;
}
private static final class Fixture {
private final Engine engine = mock(Engine.class);
private final Player player = mock(Player.class);
private final World world = mock(World.class);
private final PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
private Fixture() {
when(player.getWorld()).thenReturn(world);
when(player.getUniqueId()).thenReturn(UUID.fromString("00000000-0000-0000-0000-000000000001"));
when(generator.getEngine()).thenReturn(engine);
}
}
private static final class TestCommandService extends CommandSVC {
private final DirectorRuntimeEngine director;
private TestCommandService(DirectorExecutionDispatcher dispatcher) {
try (MockedStatic<Iris> iris = mockStatic(Iris.class)) {
director = DirectorEngineFactory.create(new CommandIris(), DirectorEngineOptions.builder()
.dispatcher(dispatcher)
.invocationHook(this)
.build());
}
}
@Override
public DirectorRuntimeEngine getDirector() {
return director;
}
}
}
+3
View File
@@ -26,6 +26,8 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
@@ -163,6 +165,7 @@ dependencies {
"art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(),
libs.paralithic,
libs.asm,
libs.lru,
libs.caffeine,
libs.dom4j,
+3
View File
@@ -26,6 +26,8 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
@@ -162,6 +164,7 @@ dependencies {
"art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(),
libs.paralithic,
libs.asm,
libs.lru,
libs.caffeine,
libs.dom4j,
@@ -1,9 +1,11 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
@@ -23,6 +25,7 @@ import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.ArrayList;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -48,14 +51,17 @@ public final class NativeStructureReferenceRepair {
originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) {
for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS;
originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) {
ChunkAccess originChunk = level.getChunk(
originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS);
for (Map.Entry<Structure, StructureStart> entry : originChunk.getAllStarts().entrySet()) {
ScannedStart start = scanStart(
engine, serverLevel, structureManager, targetChunk, registry,
originChunk, entry.getKey(), entry.getValue());
if (start != null && isTargetRelevant(engine, targetChunk, start)) {
starts.add(start);
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openOriginRuntimeScope(engine, originChunkX, originChunkZ)) {
ChunkAccess originChunk = level.getChunk(
originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS);
for (Map.Entry<Structure, StructureStart> entry : originChunk.getAllStarts().entrySet()) {
ScannedStart start = scanStart(
engine, serverLevel, structureManager, targetChunk, registry,
originChunk, entry.getKey(), entry.getValue());
if (start != null && isTargetRelevant(engine, targetChunk, start)) {
starts.add(start);
}
}
}
}
@@ -75,6 +81,22 @@ public final class NativeStructureReferenceRepair {
}
}
private static GenerationHistoryRuntimeRouter.CoordinateScope openOriginRuntimeScope(
Engine engine,
int chunkX,
int chunkZ
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
try {
return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4);
} catch (IOException failure) {
throw new IllegalStateException("Unable to resolve native structure references for origin "
+ chunkX + "," + chunkZ + " through generation history.", failure);
}
}
private static boolean isTargetRelevant(
Engine engine, ChunkAccess targetChunk, ScannedStart scanned) {
StructureStart start = scanned.start();
@@ -1,11 +1,13 @@
package art.arcane.iris.nativegen;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
@@ -28,6 +30,7 @@ import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -38,6 +41,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.Supplier;
/**
@@ -60,13 +64,14 @@ public final class NativeStructureVolumeIndex {
private final Context context;
private final OriginResolver origins;
private final Map<Long, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
private final Map<Long, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
new ConcurrentHashMap<>();
private final ReentrantLock[] originWindowStripes = createOriginWindowStripes();
private final IntConsumer retirementListener = this::evictRuntime;
private NativeStructureVolumeIndex(Context context) {
this.context = Objects.requireNonNull(context, "Native structure volume context must not be null");
@@ -79,13 +84,25 @@ public final class NativeStructureVolumeIndex {
}
public static void install(Engine engine, Context context) {
Objects.requireNonNull(engine, "Native structure volume index requires an engine");
INDEXES.put(engine, new NativeStructureVolumeIndex(context));
Engine requiredEngine = Objects.requireNonNull(engine, "Native structure volume index requires an engine");
NativeStructureVolumeIndex installed = new NativeStructureVolumeIndex(context);
synchronized (INDEXES) {
NativeStructureVolumeIndex previous = INDEXES.put(requiredEngine, installed);
if (previous != null) {
previous.detachRetirementListener(requiredEngine);
}
installed.attachRetirementListener(requiredEngine);
}
}
public static void uninstall(Engine engine) {
if (engine != null) {
INDEXES.remove(engine);
synchronized (INDEXES) {
NativeStructureVolumeIndex removed = INDEXES.remove(engine);
if (removed != null) {
removed.detachRetirementListener(engine);
}
}
}
}
@@ -121,6 +138,7 @@ public final class NativeStructureVolumeIndex {
}
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
int runtimeId = runtimeId(engine);
int fromChunkX = Math.min(minX, maxX) >> 4;
int toChunkX = Math.max(minX, maxX) >> 4;
int fromChunkZ = Math.min(minZ, maxZ) >> 4;
@@ -128,7 +146,7 @@ public final class NativeStructureVolumeIndex {
KList<NativeStructureVolume> matches = null;
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
for (NativeStructureVolume volume : chunkVolumes(engine, chunkX, chunkZ)) {
for (NativeStructureVolume volume : chunkVolumes(engine, runtimeId, chunkX, chunkZ)) {
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
continue;
}
@@ -144,8 +162,13 @@ public final class NativeStructureVolumeIndex {
return matches == null ? NativeStructureVolume.NONE : matches;
}
private KList<NativeStructureVolume> chunkVolumes(Engine engine, int chunkX, int chunkZ) {
return cached(queryCache, queryBuilds, chunkKey(chunkX, chunkZ),
private KList<NativeStructureVolume> chunkVolumes(
Engine engine,
int runtimeId,
int chunkX,
int chunkZ
) {
return cached(queryCache, queryBuilds, new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
() -> buildChunkVolumes(engine, chunkX, chunkZ));
}
@@ -196,8 +219,16 @@ public final class NativeStructureVolumeIndex {
}
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
return cached(originCache, originBuilds, chunkKey(chunkX, chunkZ),
() -> origins.volumesAt(engine, chunkX, chunkZ));
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openHistoryCoordinateScope(engine, chunkX, chunkZ)) {
int runtimeId = runtimeId(engine);
return cached(
originCache,
originBuilds,
new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
() -> origins.volumesAt(engine, chunkX, chunkZ)
);
}
}
private KList<NativeStructureVolume> buildOriginVolumes(Engine engine, int chunkX, int chunkZ) {
@@ -362,9 +393,23 @@ public final class NativeStructureVolumeIndex {
return target;
}
private KList<NativeStructureVolume> cached(Map<Long, KList<NativeStructureVolume>> cache,
ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> builds,
long key, Supplier<KList<NativeStructureVolume>> loader) {
void evictRuntime(int runtimeId) {
synchronized (originCache) {
originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
}
synchronized (queryCache) {
queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
}
originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
}
private KList<NativeStructureVolume> cached(
Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache,
ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> builds,
RuntimeChunkKey key,
Supplier<KList<NativeStructureVolume>> loader
) {
synchronized (cache) {
KList<NativeStructureVolume> hit = cache.get(key);
if (hit != null) {
@@ -392,7 +437,9 @@ public final class NativeStructureVolumeIndex {
try {
KList<NativeStructureVolume> built = loader.get();
synchronized (cache) {
cache.put(key, built);
if (builds.get(key) == future) {
cache.put(key, built);
}
}
future.complete(built);
return built;
@@ -451,15 +498,52 @@ public final class NativeStructureVolumeIndex {
}
}
private static Map<Long, KList<NativeStructureVolume>> lru(int capacity) {
private static GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
Engine engine,
int chunkX,
int chunkZ
) {
if (!(engine instanceof IrisEngine irisEngine)) {
return null;
}
try {
return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4);
} catch (IOException failure) {
throw new IllegalStateException("Unable to route native structure origin "
+ chunkX + "," + chunkZ + " through generation history.", failure);
}
}
private static int runtimeId(Engine engine) {
return engine == null ? 0 : engine.getCacheID();
}
private void attachRetirementListener(Engine engine) {
if (engine instanceof IrisEngine irisEngine) {
irisEngine.addGenerationRuntimeRetirementListener(retirementListener);
}
}
private void detachRetirementListener(Engine engine) {
if (engine instanceof IrisEngine irisEngine) {
irisEngine.removeGenerationRuntimeRetirementListener(retirementListener);
}
}
private static Map<RuntimeChunkKey, KList<NativeStructureVolume>> lru(int capacity) {
return new LinkedHashMap<>(16, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Long, KList<NativeStructureVolume>> eldest) {
protected boolean removeEldestEntry(
Map.Entry<RuntimeChunkKey, KList<NativeStructureVolume>> eldest
) {
return size() > capacity;
}
};
}
private record RuntimeChunkKey(int runtimeId, long chunkKey) {
}
/**
* The generator, biome source and structure state arrive as suppliers so an installed index never holds the
* level (and through it the engine) strongly. That keeps the weakly keyed registry collectable on world unload.
@@ -22,9 +22,11 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
@@ -51,6 +53,7 @@ import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -72,7 +75,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
private final BiomeSource serializedSource;
private final Set<String> warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet();
private final Set<StructureStateBiomeSource> structureStateSources = ConcurrentHashMap.newKeySet();
// Pack generation. Every cache on this source is keyed by it, which is what keeps memoized tables from
// surviving a repoint with the previous pack's content.
private final AtomicLong packGeneration = new AtomicLong();
@@ -105,9 +107,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
possibleStructureBiomeKeys = null;
biomeKeySets = null;
possibleBiomesCache = null;
for (StructureStateBiomeSource source : structureStateSources) {
source.clearCache();
}
}
void evictRuntime(int runtimeIdentity) {
visibleBiomeCache.evictRuntime(runtimeIdentity);
structureBiomeCache.evictRuntime(runtimeIdentity);
surfaceStructureBiomeCache.evictRuntime(runtimeIdentity);
}
/**
@@ -141,7 +146,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
}
});
StructureStateBiomeSource source = new StructureStateBiomeSource(this, Set.copyOf(possible));
structureStateSources.add(source);
return source;
}
@@ -308,15 +312,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (engine == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
engine, quartX << 2, quartZ << 2, "modded_structure_biome");
try (historyScope) {
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
}
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
}
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
}
}
@@ -394,16 +402,17 @@ final class IrisModdedBiomeSource extends BiomeSource {
QuartPos.toBlock(quartZ)
);
long key = packNoiseKey(quartX, quartY, quartZ);
int runtimeIdentity = engine.getCacheID();
BiomeHolderTable cache = structureBiomeCache;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
Holder<Biome> cached = cache.get(runtimeIdentity, key);
if (cached != null) {
return cached;
}
}
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
if (cacheable) {
cache.put(key, resolved);
cache.put(runtimeIdentity, key, resolved);
}
return resolved;
}
@@ -413,16 +422,20 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (engine == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
engine, quartX << 2, quartZ << 2, "modded_visible_biome");
try (historyScope) {
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
if (lease == null) {
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
}
return getVisibleNoiseBiomeWithActiveGenerationLease(
engine, quartX, quartY, quartZ, sampler);
}
return getVisibleNoiseBiomeWithActiveGenerationLease(
engine, quartX, quartY, quartZ, sampler);
}
}
@@ -439,16 +452,18 @@ final class IrisModdedBiomeSource extends BiomeSource {
QuartPos.toBlock(quartZ)
);
long key = packNoiseKey(quartX, quartY, quartZ);
int runtimeIdentity = engine.getCacheID();
BiomeHolderTable cache = visibleBiomeCache;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
Holder<Biome> cached = cache.get(runtimeIdentity, key);
if (cached != null) {
return cached;
}
}
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
Holder<Biome> resolved = resolveVisibleBiome(
engine, quartX, quartY, quartZ, sampler, null);
if (cacheable) {
cache.put(key, resolved);
cache.put(runtimeIdentity, key, resolved);
}
return resolved;
}
@@ -458,45 +473,50 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (engine == null) {
return null;
}
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
if (lease == null) {
return null;
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
engine, blockX, blockZ, "modded_surface_spawn_biome");
try (historyScope) {
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
if (lease == null) {
return null;
}
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return null;
}
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
int sampleX = QuartPos.toBlock(quartX);
int sampleZ = QuartPos.toBlock(quartZ);
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
DimensionStackContext stackContext = engine.getDimensionStackContext();
DimensionStackLayout.Layer layer = stackContext == null
? null
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
int internalY = layer == null
? Engine.hostHeight(engine, sampleX, sampleZ, true)
: layer.clippedSurfaceY();
int quartY = QuartPos.fromBlock(internalY + engine.getMinHeight());
long key = packNoiseKey(quartX, quartY, quartZ);
BiomeHolderTable cache = visibleBiomeCache;
if (cacheable) {
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
return null;
}
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
return null;
}
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
int sampleX = QuartPos.toBlock(quartX);
int sampleZ = QuartPos.toBlock(quartZ);
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
DimensionStackContext stackContext = engine.getDimensionStackContext();
DimensionStackLayout.Layer layer = stackContext == null
? null
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
int internalY = layer == null
? Engine.hostHeight(engine, sampleX, sampleZ, true)
: layer.clippedSurfaceY();
int quartY = QuartPos.fromBlock(internalY + engine.getMinHeight());
long key = packNoiseKey(quartX, quartY, quartZ);
int runtimeIdentity = engine.getCacheID();
BiomeHolderTable cache = visibleBiomeCache;
if (cacheable) {
Holder<Biome> cached = cache.get(runtimeIdentity, key);
if (cached != null) {
return cached;
}
}
Holder<Biome> resolved = resolveVisibleBiome(
engine, quartX, quartY, quartZ, null, null);
if (cacheable) {
cache.put(runtimeIdentity, key, resolved);
}
return resolved;
}
Holder<Biome> resolved = resolveVisibleBiome(
engine, quartX, quartY, quartZ, null);
if (cacheable) {
cache.put(key, resolved);
}
return resolved;
}
}
@@ -535,13 +555,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (lease == null) {
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
boolean surfaceQuery;
try (lease) {
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
engine, x, z, "modded_biomes_within");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
}
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight());
}
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
if (!surfaceQuery) {
return super.getBiomesWithin(x, y, z, radius, sampler);
}
int minQuartX = QuartPos.fromBlock(x - radius);
@@ -552,7 +578,11 @@ final class IrisModdedBiomeSource extends BiomeSource {
Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
engine, quartX << 2, quartZ << 2, "modded_biomes_within");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
}
}
}
return biomes;
@@ -562,13 +592,14 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
Climate.Sampler sampler) {
long key = packColumnKey(quartX, quartZ);
int runtimeIdentity = engine.getCacheID();
BiomeHolderTable cache = surfaceStructureBiomeCache;
Holder<Biome> cached = cache.get(key);
Holder<Biome> cached = cache.get(runtimeIdentity, key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
cache.put(key, resolved);
cache.put(runtimeIdentity, key, resolved);
return resolved;
}
@@ -618,11 +649,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
return resolved;
}
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
Climate.Sampler sampler) {
return resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler, null);
}
private Holder<Biome> resolveVisibleBiome(
Engine engine,
int quartX,
@@ -637,20 +663,21 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
}
return resolveVisibleBiome(
engine, registry, resolution, quartX, quartY, quartZ, sampler);
}
private Holder<Biome> resolveVisibleBiome(
Engine engine,
Registry<Biome> registry,
BiomeResolution resolution,
int quartX,
int quartY,
int quartZ,
Climate.Sampler sampler
) {
String historicalBiomeKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
resolution.blockX(),
resolution.blockY(),
resolution.blockZ()).orElse(null);
if (historicalBiomeKey != null) {
Holder<Biome> historical = resolveHolder(registry, historicalBiomeKey);
if (historical == null) {
throw new IllegalStateException("Historical Iris biome '" + historicalBiomeKey
+ "' is not registered at block " + resolution.blockX() + ","
+ resolution.blockY() + "," + resolution.blockZ());
}
return historical;
}
String biomeKey;
String unresolvedBiomeKey;
if (resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
@@ -658,18 +685,27 @@ final class IrisModdedBiomeSource extends BiomeSource {
return fallbackBiome(registry, "custom derivative of '"
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
}
biomeKey = customBiomeRef(
String logicalBiomeKey = customBiomeRef(
resolution.packName(), resolution.dimension(), customBiome.getId());
biomeKey = resolution.dimension() == engine.getDimension()
&& resolution.data() == engine.getData()
? ModdedWorldgenIds.biomeRef(engine, customBiome.getId())
: resolution.data().customBiomeResourceKey(resolution.dimension(), customBiome);
unresolvedBiomeKey = logicalBiomeKey.equals(biomeKey)
? biomeKey
: logicalBiomeKey + " mapped to " + biomeKey;
} else if (resolution.underground()) {
biomeKey = resolution.irisBiome().getGroundBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
unresolvedBiomeKey = biomeKey;
} else {
biomeKey = resolution.irisBiome().getSkyBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
unresolvedBiomeKey = biomeKey;
}
Holder<Biome> resolved = resolveHolder(registry, biomeKey);
return resolved == null
? fallbackBiome(registry, biomeKey, quartX, quartY, quartZ, sampler)
? fallbackBiome(registry, unresolvedBiomeKey, quartX, quartY, quartZ, sampler)
: resolved;
}
@@ -784,43 +820,30 @@ final class IrisModdedBiomeSource extends BiomeSource {
);
}
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
private Holder<Biome> resolveRequiredStructureBiome(
int quartX,
int quartY,
int quartZ,
Climate.Sampler sampler
) {
IrisModdedChunkGenerator current = generator;
if (current == null) {
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
}
Engine engine = current.awaitStructureEngine();
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
engine, quartX << 2, quartZ << 2, "modded_structure_state_biome");
try (historyScope) {
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
if (lease == null) {
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
}
Registry<Biome> registry = biomeRegistry();
if (registry == null) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
}
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
}
IrisBiome irisBiome;
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
irisBiome = resolveSurfaceStructureBiome(engine, quartX << 2, quartZ << 2);
} else {
BiomeResolution resolution = resolveStructureBiomeResolution(
engine, quartX, quartY, quartZ);
irisBiome = resolution == null ? null : resolution.irisBiome();
}
if (irisBiome == null) {
throw new IllegalStateException("Iris returned no structure biome at quart "
+ quartX + "," + quartY + "," + quartZ);
}
String derivativeKey = irisBiome.getStructureDerivativeKey();
Holder<Biome> resolved = resolveHolder(registry, derivativeKey);
if (resolved == null) {
throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey
+ "' is not registered at quart " + quartX + "," + quartY + "," + quartZ);
}
return resolved;
}
}
@@ -875,6 +898,30 @@ final class IrisModdedBiomeSource extends BiomeSource {
return current == null ? null : current.structureEngineOrNull();
}
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
Engine engine,
int blockX,
int blockZ,
String operation
) {
IrisModdedChunkGenerator current = generator;
if (current != null && current.allowsGenerationHistoryBypass(engine)) {
return null;
}
if (!(engine instanceof IrisEngine irisEngine)) {
throw new IllegalStateException("Iris " + operation + " requires an IrisEngine runtime.");
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElseThrow(() ->
new IllegalStateException("Iris " + operation
+ " requires an attached generation-history runtime router."));
try {
return router.openCoordinateScope(blockX, blockZ);
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation + " could not route block "
+ blockX + "," + blockZ + " through generation history.", failure);
}
}
private GenerationSessionLease tryAcquireGenerationLease(Engine engine, String operation) {
if (engine == null || engine.isClosed()) {
return null;
@@ -950,8 +997,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
}
Set<String> structureRequired = collectStructureBiomeKeys(
engine.getDimension(), engine.getData());
LinkedHashSet<String> structureRequired = new LinkedHashSet<>(collectStructureBiomeKeys(
engine.getDimension(), engine.getData()));
structureRequired.addAll(IrisModdedChunkGenerator.retainedBiomeKeys(engine));
LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
boolean visibleFallbackPossible = false;
@@ -959,11 +1007,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
IrisBiome irisBiome = ownedBiome.biome();
if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customKey = customBiomeRef(
ownedBiome.data().getDataFolder().getName(),
ownedBiome.dimension(),
customBiome.getId()
);
String customKey = ownedBiome.data().customBiomeResourceKey(
ownedBiome.dimension(), customBiome);
visibleRequired.add(customKey);
visibleEmittable.add(customKey);
}
@@ -1035,6 +1080,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
irisBiome,
underground,
dimension,
data,
data.getDataFolder().getName(),
blockX,
blockY,
@@ -1181,6 +1227,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
IrisBiome irisBiome,
boolean underground,
IrisDimension dimension,
IrisData data,
String packName,
int blockX,
int blockY,
@@ -1224,35 +1271,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
private volatile AtomicReferenceArray<Entry> table;
Holder<Biome> get(long key) {
Holder<Biome> get(int runtimeIdentity, long key) {
AtomicReferenceArray<Entry> slots = table;
if (slots == null) {
return null;
}
int home = home(key);
int home = home(runtimeIdentity, key);
for (int probe = 0; probe < PROBE_LIMIT; probe++) {
Entry entry = slots.get((home + probe) & MASK);
if (entry == null) {
return null;
}
if (entry.key() == key) {
if (entry.runtimeIdentity() == runtimeIdentity && entry.key() == key) {
return entry.value();
}
}
return null;
}
void put(long key, Holder<Biome> value) {
void put(int runtimeIdentity, long key, Holder<Biome> value) {
AtomicReferenceArray<Entry> slots = table;
if (slots == null) {
slots = install();
}
int home = home(key);
Entry entry = new Entry(key, value);
int home = home(runtimeIdentity, key);
Entry entry = new Entry(runtimeIdentity, key, value);
for (int probe = 0; probe < PROBE_LIMIT; probe++) {
int slot = (home + probe) & MASK;
Entry existing = slots.get(slot);
if (existing == null || existing.key() == key) {
if (existing == null
|| (existing.runtimeIdentity() == runtimeIdentity && existing.key() == key)) {
slots.set(slot, entry);
return;
}
@@ -1260,6 +1308,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
slots.set(home, entry);
}
void evictRuntime(int runtimeIdentity) {
AtomicReferenceArray<Entry> slots = table;
if (slots == null) {
return;
}
for (int index = 0; index < slots.length(); index++) {
Entry entry = slots.get(index);
if (entry != null && entry.runtimeIdentity() == runtimeIdentity) {
slots.compareAndSet(index, entry, null);
}
}
}
private synchronized AtomicReferenceArray<Entry> install() {
AtomicReferenceArray<Entry> existing = table;
if (existing != null) {
@@ -1270,29 +1331,25 @@ final class IrisModdedBiomeSource extends BiomeSource {
return created;
}
private static int home(long key) {
long mixed = key * 0x9E3779B97F4A7C15L;
private static int home(int runtimeIdentity, long key) {
long mixed = (key ^ ((long) runtimeIdentity * 0xC2B2AE3D27D4EB4FL))
* 0x9E3779B97F4A7C15L;
return (int) ((mixed ^ (mixed >>> 32)) & MASK);
}
private record Entry(long key, Holder<Biome> value) {
private record Entry(int runtimeIdentity, long key, Holder<Biome> value) {
}
}
private static final class StructureStateBiomeSource extends BiomeSource {
private final IrisModdedBiomeSource delegate;
private final Set<Holder<Biome>> possibleBiomes;
private volatile BiomeHolderTable resolvedBiomes = new BiomeHolderTable();
private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set<Holder<Biome>> possibleBiomes) {
this.delegate = delegate;
this.possibleBiomes = possibleBiomes;
}
private void clearCache() {
resolvedBiomes = new BiomeHolderTable();
}
@Override
protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
@@ -1305,15 +1362,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
@Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
long key = packNoiseKey(x, y, z);
BiomeHolderTable cache = resolvedBiomes;
Holder<Biome> cached = cache.get(key);
if (cached != null) {
return cached;
}
Holder<Biome> resolved = delegate.resolveRequiredStructureBiome(x, y, z);
cache.put(key, resolved);
return resolved;
return delegate.resolveRequiredStructureBiome(x, y, z, sampler);
}
@Override
@@ -20,11 +20,17 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.nativegen.NativeStructureStartInjector;
@@ -73,6 +79,7 @@ import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomState;
@@ -85,18 +92,25 @@ import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
public final class IrisModdedChunkGenerator extends ChunkGenerator {
// Vanilla-shaped fallback for an unbound generator (matches IrisDimension defaults). getMinY,
@@ -127,16 +141,20 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
private final ModdedImportedFeatureStage importedFeatures;
private final AtomicBoolean announced = new AtomicBoolean(false);
private final IntConsumer generationRuntimeRetirementListener = this::retireGenerationRuntimeCaches;
private volatile boolean unloading;
private volatile Engine engine;
private volatile String activePack;
private volatile String activeDimensionKey;
private volatile Path immutablePackRoot;
private volatile ModdedGenerationMode generationMode = ModdedGenerationMode.PERSISTENT_RESTORE;
private volatile long seedOverride = Long.MIN_VALUE;
private volatile long lastChunkGenAt = 0L;
private volatile Set<String> configuredStructureBiomeKeys;
private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack;
private volatile ModdedDimensionMetadata.DimensionMetadata heightMetadata;
private volatile ServerLevel boundLevel;
private volatile IrisEngine retirementListenerEngine;
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
@@ -169,12 +187,39 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
repoint(
pack,
packDimensionKey,
seed,
null,
ModdedGenerationMode.PERSISTENT_RESTORE
);
}
synchronized void repoint(String pack, String packDimensionKey, long seed, Path packRoot) {
repoint(
pack,
packDimensionKey,
seed,
packRoot,
ModdedGenerationMode.PERSISTENT_RESTORE
);
}
synchronized void repoint(
String pack,
String packDimensionKey,
long seed,
Path packRoot,
ModdedGenerationMode generationMode
) {
ServerLevel level = boundLevel();
if (level != null) {
this.generationMode = generationMode;
repointAndBind(level, pack, packDimensionKey, seed);
return;
}
applyUnboundConfiguration(pack, packDimensionKey, seed);
applyUnboundConfiguration(pack, packDimensionKey, seed, packRoot, generationMode);
}
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
@@ -185,7 +230,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
Engine activeEngine = engineIfBound();
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
Engine replacement = ModdedWorldEngines.prepareReplacement(
level,
pack,
packDimensionKey,
seed,
generationMode
);
try {
if (activeEngine != null) {
requireStructureBiomeUniverseCompatible(
@@ -213,13 +264,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.engine = replacement;
bindGenerationRuntimeRetirementListener(replacement);
this.immutablePackRoot = replacement.getData().getDataFolder().toPath().toAbsolutePath().normalize();
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
this.heightMetadata = engineHeights(replacement);
this.engineBinding.reset();
this.announced.set(false);
resetRuntimeCaches();
this.engine = replacement;
this.engineBinding.complete(replacement);
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
@@ -242,10 +295,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
private void clearEngineBinding() {
unbindGenerationRuntimeRetirementListener();
this.engine = null;
this.boundLevel = null;
this.configuredStructureBiomeKeys = null;
this.configuredPack = null;
this.immutablePackRoot = null;
this.engineBinding.reset();
this.announced.set(false);
this.structureBiomeSource.clearCaches();
@@ -254,10 +309,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.spawnTables.resetVanillaSpawnBiomes();
}
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
private void applyUnboundConfiguration(
String pack,
String packDimensionKey,
long seed,
Path packRoot,
ModdedGenerationMode generationMode
) {
unbindGenerationRuntimeRetirementListener();
this.activePack = pack;
this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed;
this.immutablePackRoot = packRoot == null ? null : packRoot.toAbsolutePath().normalize();
this.generationMode = Objects.requireNonNull(generationMode, "generationMode");
this.engine = null;
this.boundLevel = null;
this.configuredStructureBiomeKeys = null;
@@ -272,7 +336,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
public synchronized void resetToDefault() {
repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE);
repoint(
defaultPack,
defaultDimensionKey,
Long.MIN_VALUE,
null,
ModdedGenerationMode.PERSISTENT_RESTORE
);
}
public String activePack() {
@@ -312,7 +382,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
BlockPos pos, int radius,
boolean findUnexplored) {
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
int chunkX = Math.floorDiv(pos.getX(), 16);
int chunkZ = Math.floorDiv(pos.getZ(), 16);
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
current, chunkX, chunkZ, "modded_structure_locate");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures(
level, holders, current);
@@ -481,14 +556,22 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return existing;
}
try {
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
Engine created = ModdedWorldEngines.get(
level,
activePack,
activeDimensionKey,
seedOverride,
generationMode
);
requireCompletedShutdown(created);
if (created.isClosed() || created.getComplex() == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' created an engine without a ready biome complex");
}
engine = created;
bindGenerationRuntimeRetirementListener(created);
boundLevel = level;
immutablePackRoot = created.getData().getDataFolder().toPath().toAbsolutePath().normalize();
heightMetadata = engineHeights(created);
configuredStructureBiomeKeys = null;
structureBiomeSource.clearCaches();
@@ -649,10 +732,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Engine current = engine;
if (current != null && !current.isClosed() && !current.isClosing()) {
Set<String> retained = retainedBiomeKeys(current);
if (!retained.isEmpty()) {
configuredStructureBiomeKeys = retained;
return retained;
}
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
current.getAllBiomes(), current.getDimension().getLoadKey());
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(current);
configuredStructureBiomeKeys = resolved;
return resolved;
} catch (GenerationSessionException e) {
@@ -669,6 +756,27 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
static Set<String> retainedBiomeKeys(Engine engine) {
if (!(engine instanceof IrisEngine irisEngine)) {
return Set.of();
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return Set.of();
}
LinkedHashSet<String> keys = new LinkedHashSet<>();
Collection<GenerationEpoch> epochs = router.history().manifest().epochs();
for (GenerationEpoch epoch : epochs) {
GenerationRegistryContract contract = epoch.registryContract();
for (GenerationRegistryContract.PhysicalResourceKey key : contract.definitions().keySet()) {
if (GenerationRegistryContractFactory.BIOME_REGISTRY.equals(key.registryKey())) {
keys.add(key.resourceKey());
}
}
}
return Set.copyOf(keys);
}
private static ModdedDimensionMetadata.DimensionMetadata engineHeights(Engine engine) {
IrisDimension dimension = engine.getDimension();
int minY = engine.getMinHeight();
@@ -713,8 +821,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (configuredPack != null) {
return configuredPack;
}
PackValidationRegistry.requireLoadable(activePack);
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
Path packRoot = immutablePackRoot;
File packDirectory;
if (packRoot == null) {
PackValidationRegistry.requireLoadable(activePack);
packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
} else {
PackValidationRegistry.requireLoadable(packRoot);
packDirectory = packRoot.toFile();
}
IrisData data = IrisData.get(packDirectory);
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
if (dimension == null) {
@@ -800,36 +915,54 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = biome.value().getMobSettings().getMobs(category);
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
biome, structureManager, category, pos);
if (resolvedSpawns != explicitSpawns) {
return resolvedSpawns;
}
Engine current = engine();
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
current, pos.getX(), pos.getZ(), "modded_mob_spawn_table");
GenerationSessionLease lease = requireGenerationLease(current, "modded_mob_spawn_table");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns =
biome.value().getMobSettings().getMobs(category);
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
biome, structureManager, category, pos);
if (resolvedSpawns != explicitSpawns) {
return resolvedSpawns;
}
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
if (vanillaSpawnBiome == null) {
return explicitSpawns;
}
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
if (vanillaSpawnBiome == null) {
return explicitSpawns;
}
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category);
if (explicitSpawns.isEmpty()) {
return vanillaSpawns;
}
if (vanillaSpawns.isEmpty()) {
return explicitSpawns;
}
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns =
vanillaSpawnBiome.value().getMobSettings().getMobs(category);
if (explicitSpawns.isEmpty()) {
return vanillaSpawns;
}
if (vanillaSpawns.isEmpty()) {
return explicitSpawns;
}
return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns);
return spawnTables.mergedSpawnTable(
current,
biome.value(),
category,
vanillaSpawns,
explicitSpawns
);
}
}
@Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
StructureManager structureManager, ChunkAccess chunk) {
Engine current = engine();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes");
ChunkPos chunkPos = chunk.getPos();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
current, chunkPos.x(), chunkPos.z(), "modded_create_biomes");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
return CompletableFuture.completedFuture(chunk);
@@ -844,19 +977,35 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
ModdedIrisLog.debug("Iris generating chunk {},{}", pos.x(), pos.z());
PlatformBlockState air = IrisPlatforms.get().registries().air();
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
generationEngine, pos.x(), pos.z(), "modded_chunk_pipeline");
if (ModdedGenPool.parallelChunkSystem()) {
return CompletableFuture.completedFuture(
generateTerrain(chunk, generationEngine, pos, air));
try {
if (ModdedGenPool.parallelChunkSystem()) {
try {
return CompletableFuture.completedFuture(
generateTerrain(chunk, generationEngine, pos, air, route));
} finally {
if (route != null) {
route.close();
}
}
}
CompletableFuture<ChunkAccess> pipeline = CompletableFuture.supplyAsync(
() -> generateTerrain(chunk, generationEngine, pos, air, route),
ModdedGenPool.pool());
return closeRouteOnCompletion(pipeline, route);
} catch (RuntimeException | Error failure) {
closeHistoryRoute(route, failure);
throw failure;
}
return CompletableFuture.supplyAsync(
() -> generateTerrain(chunk, generationEngine, pos, air),
ModdedGenPool.pool());
}
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
PlatformBlockState air) {
try (GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
PlatformBlockState air,
GenerationHistoryRuntimeRouter.RuntimeRoute route) {
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) {
if (announced.compareAndSet(false, true)) {
ModdedIrisLog.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
@@ -875,6 +1024,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
Heightmap.Types.MOTION_BLOCKING,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
if (route != null) {
route.claimGeneratedSemantics();
}
return chunk;
} catch (GenerationSessionException e) {
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
@@ -890,6 +1042,67 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
private static CompletableFuture<ChunkAccess> closeRouteOnCompletion(
CompletableFuture<ChunkAccess> pipeline,
GenerationHistoryRuntimeRouter.RuntimeRoute route
) {
if (route == null) {
return pipeline;
}
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
pipeline.whenComplete((ChunkAccess chunk, Throwable failure) -> {
boolean cancelled = isCancellationFailure(failure);
Throwable completionFailure = failure;
try {
route.close();
} catch (Throwable closeFailure) {
completionFailure = appendFailure(completionFailure, closeFailure);
}
if (completionFailure == null) {
completion.complete(chunk);
} else if (cancelled) {
completion.cancel(false);
} else {
completion.completeExceptionally(completionFailure);
}
});
return completion;
}
private static void closeHistoryRoute(
GenerationHistoryRuntimeRouter.RuntimeRoute route,
Throwable failure
) {
if (route == null) {
return;
}
try {
route.close();
} catch (Throwable closeFailure) {
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
}
}
private static Throwable appendFailure(Throwable failure, Throwable closeFailure) {
if (failure == null) {
return closeFailure;
}
if (failure != closeFailure) {
failure.addSuppressed(closeFailure);
}
return failure;
}
private static boolean isCancellationFailure(Throwable failure) {
Throwable current = failure;
while (current instanceof CompletionException && current.getCause() != null) {
current = current.getCause();
}
return current instanceof CancellationException;
}
private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
int baseX = pos.getMinBlockX();
int baseZ = pos.getMinBlockZ();
@@ -961,18 +1174,47 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) {
Engine current = engine(region.getLevel());
ChunkPos chunkPos = chunk.getPos();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
current, chunkPos.x(), chunkPos.z(), "modded_apply_carvers");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_apply_carvers");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
}
}
@Override
public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) {
Engine current = engine(region.getLevel());
ChunkPos chunkPos = chunk.getPos();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
current, chunkPos.x(), chunkPos.z(), "modded_build_surface");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_build_surface");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
}
}
@Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
Engine current = engine();
importedFeatures.prepare(current);
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
Engine current = engine(level.getLevel());
ChunkPos chunkPos = chunk.getPos();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
current, chunkPos.x(), chunkPos.z(), "modded_biome_decoration");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
if (!allowsRoutedDiscreteGeneration(
current,
route,
chunkPos,
chunk,
ChunkStatus.FEATURES
)) {
return;
}
importedFeatures.prepare(current);
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
// Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk
// step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the
@@ -984,8 +1226,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
Engine current = engine(levelKey);
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
ChunkPos chunkPos = chunk.getPos();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
current, chunkPos.x(), chunkPos.z(), "modded_create_structures");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
if (!allowsRoutedDiscreteGeneration(
current,
route,
chunkPos,
chunk,
ChunkStatus.STRUCTURE_STARTS
)) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
@@ -1008,7 +1263,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
Engine current = engine(level.getLevel());
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
ChunkPos chunkPos = chunk.getPos();
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
current, chunkPos.x(), chunkPos.z(), "modded_create_references");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
NativeStructureReferenceRepair.createReferences(
current, level, structureManager, chunk);
@@ -1021,25 +1280,42 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override
public void spawnOriginalMobs(WorldGenRegion region) {
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry);
ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome;
if (engine == null || engine.getDimensionStackContext() == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} else {
visibleBiome = structureBiomeSource.getVisibleSurfaceBiome(
center.getMinBlockX() + 8,
center.getMinBlockZ() + 8);
if (visibleBiome == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
Engine current = engine(region.getLevel());
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
current, center.x(), center.z(), "modded_spawn_original_mobs");
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_original_mobs");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
ChunkAccess centerChunk = region.getChunk(center.x(), center.z());
if (!allowsRoutedDiscreteGeneration(
current,
route,
center,
centerChunk,
ChunkStatus.SPAWN
)) {
return;
}
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry);
Holder<Biome> visibleBiome;
if (current.getDimensionStackContext() == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} else {
visibleBiome = structureBiomeSource.getVisibleSurfaceBiome(
center.getMinBlockX() + 8,
center.getMinBlockZ() + 8);
if (visibleBiome == null) {
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
}
}
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
}
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
}
@Override
@@ -1075,7 +1351,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
Engine current = requireDataQueryEngine("base height");
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
current, x, z, "modded_base_height");
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
int height = current.getDimensionStackContext() == null
? current.getHeight(x, z, ignoreFluid)
@@ -1091,7 +1369,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
int minY = heightAccessor.getMinY();
BlockState[] states = new BlockState[heightAccessor.getHeight()];
Engine current = requireDataQueryEngine("base column");
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
current, x, z, "modded_base_column");
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
BlockState airState = Blocks.AIR.defaultBlockState();
boolean dimensionStack = current.getDimensionStackContext() != null;
@@ -1114,6 +1394,115 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
private boolean allowsRoutedDiscreteGeneration(
Engine current,
GenerationHistoryRuntimeRouter.RuntimeRoute route,
ChunkPos chunkPos,
ChunkAccess chunk,
ChunkStatus stage
) {
if (chunk.getPersistedStatus().isOrAfter(stage)) {
return false;
}
if (route == null) {
return allowsGenerationHistoryBypass(current);
}
return current.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
}
private void bindGenerationRuntimeRetirementListener(Engine current) {
if (!(current instanceof IrisEngine irisEngine)) {
unbindGenerationRuntimeRetirementListener();
return;
}
IrisEngine previous = retirementListenerEngine;
if (previous == irisEngine) {
return;
}
if (previous != null) {
previous.removeGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
}
irisEngine.addGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
retirementListenerEngine = irisEngine;
}
private void unbindGenerationRuntimeRetirementListener() {
IrisEngine previous = retirementListenerEngine;
if (previous == null) {
return;
}
retirementListenerEngine = null;
previous.removeGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
}
private void retireGenerationRuntimeCaches(int runtimeIdentity) {
structureBiomeSource.evictRuntime(runtimeIdentity);
importedFeatures.evictRuntime(runtimeIdentity);
spawnTables.evictRuntime(runtimeIdentity);
}
private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(
Engine current,
int chunkX,
int chunkZ,
String operation
) {
if (allowsGenerationHistoryBypass(current)) {
return null;
}
GenerationHistoryRuntimeRouter router = requireHistoryRouter(current, operation);
try {
return router.openRoute(chunkX, chunkZ);
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation + " could not route chunk "
+ chunkX + "," + chunkZ + " through generation history.", failure);
}
}
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
Engine current,
int blockX,
int blockZ,
String operation
) {
if (allowsGenerationHistoryBypass(current)) {
return null;
}
GenerationHistoryRuntimeRouter router = requireHistoryRouter(current, operation);
try {
return router.openCoordinateScope(blockX, blockZ);
} catch (IOException failure) {
throw new IllegalStateException("Iris " + operation + " could not route block "
+ blockX + "," + blockZ + " through generation history.", failure);
}
}
private GenerationHistoryRuntimeRouter requireHistoryRouter(Engine current, String operation) {
if (!(current instanceof IrisEngine irisEngine)) {
throw new IllegalStateException("Iris " + operation + " requires an IrisEngine runtime.");
}
return irisEngine.getGenerationHistoryRuntimeRouter().orElseThrow(() ->
new IllegalStateException("Iris " + operation
+ " requires an attached generation-history runtime router."));
}
boolean allowsGenerationHistoryBypass(Engine current) {
if (generationMode != ModdedGenerationMode.TRANSIENT_STUDIO) {
return false;
}
if (current instanceof IrisEngine irisEngine && irisEngine.isStudio()) {
return true;
}
throw new IllegalStateException("Only an explicitly transient Iris Studio engine may bypass "
+ "generation-history routing.");
}
private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope(
GenerationHistoryRuntimeRouter.RuntimeRoute route
) {
return route == null ? null : route.openRuntimeScope();
}
GenerationSessionLease requireGenerationLease(Engine current, String operation) {
try {
return current.acquireGenerationLease(operation);
@@ -142,6 +142,19 @@ public final class MainWorldService {
}
}
static Path configuredWorldRootIfPresent() throws IOException {
Path instanceRoot = Path.of("").toAbsolutePath().normalize();
Path properties = instanceRoot.resolve(PROPERTIES_NAME);
if (!Files.isRegularFile(properties)) {
return null;
}
try {
return resolveWorldRoot(instanceRoot, properties);
} catch (MissingWorldRootException missing) {
return null;
}
}
/**
* net.minecraft.server.Main reads server.properties as Paths.get("server.properties"), so the authoritative
* instance root is the JVM working directory - not configDir().getParent(), which points somewhere else
@@ -18,6 +18,9 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
@@ -33,6 +36,7 @@ import net.minecraft.world.level.biome.Biome;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
@@ -98,11 +102,11 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return key;
}
Engine engine = context.getEngine();
String prefix = engine.getDimension().getCustomBiomeKeyPrefix();
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
return key;
BiomeOwner owner = findCustomBiomeOwner(engine, key);
if (owner != null) {
return owner.physicalKey();
}
return ModdedWorldgenIds.biomeRef(engine, key.substring(prefix.length()));
return engine.getData().physicalBiomeResourceKey(engine.getDimension(), key);
}
@Override
@@ -139,31 +143,81 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
}
private int idForDerivative(Registry<Biome> registry, String key) {
IrisBiome owner = findCustomBiomeOwner(key);
BiomeOwner owner = findCustomBiomeOwner(key);
if (owner == null) {
return -1;
}
String derivativeKey = owner.getVanillaDerivativeKey();
String derivativeKey = owner.biome().getVanillaDerivativeKey();
if (derivativeKey == null) {
return -1;
}
return idForKey(registry, derivativeKey);
}
private IrisBiome findCustomBiomeOwner(String key) {
private BiomeOwner findCustomBiomeOwner(String key) {
for (Engine engine : ModdedWorldEngines.activeEngines()) {
if (engine == null || engine.isClosed()) {
continue;
}
IrisDimension dimension = engine.getDimension();
for (IrisBiome biome : dimension.getAllBiomes(engine)) {
if (!biome.isCustom()) {
continue;
}
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))) {
return biome;
}
BiomeOwner owner = findCustomBiomeOwner(engine, key);
if (owner != null) {
return owner;
}
}
return null;
}
private BiomeOwner findCustomBiomeOwner(Engine engine, String key) {
IrisDimension hostDimension = engine.getDimension();
BiomeOwner owner = findCustomBiomeOwner(
hostDimension,
engine.getData(),
hostDimension.getAllBiomes(engine),
key
);
if (owner != null) {
return owner;
}
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return null;
}
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
if (terrainContext.isSelfReferencing()) {
continue;
}
IrisDimension dimension = terrainContext.getDimension();
owner = findCustomBiomeOwner(
dimension,
terrainContext.getData(),
dimension.getAllBiomes(terrainContext),
key
);
if (owner != null) {
return owner;
}
}
return null;
}
private BiomeOwner findCustomBiomeOwner(
IrisDimension dimension,
IrisData data,
Iterable<IrisBiome> biomes,
String key
) {
for (IrisBiome biome : biomes) {
if (!biome.isCustom()) {
continue;
}
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
String contractLogicalKey = dimension.getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + custom.getId().toLowerCase(Locale.ROOT);
String physicalKey = data.customBiomeResourceKey(dimension, custom);
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))
|| key.equalsIgnoreCase(contractLogicalKey)
|| key.equalsIgnoreCase(physicalKey)) {
return new BiomeOwner(dimension, data, biome, physicalKey);
}
}
}
@@ -220,4 +274,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
this.registry = registry;
}
}
private record BiomeOwner(IrisDimension dimension, IrisData data, IrisBiome biome, String physicalKey) {
}
}
@@ -19,7 +19,9 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationActivation;
import art.arcane.iris.engine.object.IrisDimension;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
@@ -47,9 +49,12 @@ import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.storage.ServerLevelData;
import net.minecraft.world.level.storage.WorldData;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -120,14 +125,76 @@ public final class ModdedDimensionManager {
return generator.commandEngine();
}
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
ModdedStartup.requirePackForWorldCreation(pack);
public static Handle createTransientStudio(
MinecraftServer server,
String dimensionId,
String pack,
String packDimensionKey,
long seed
) {
return create(
server,
dimensionId,
pack,
packDimensionKey,
seed,
ModdedGenerationMode.TRANSIENT_STUDIO
);
}
static Handle restorePersistent(
MinecraftServer server,
String dimensionId,
String pack,
String packDimensionKey,
long seed
) {
return create(
server,
dimensionId,
pack,
packDimensionKey,
seed,
ModdedGenerationMode.PERSISTENT_RESTORE
);
}
private static Handle create(
MinecraftServer server,
String dimensionId,
String pack,
String packDimensionKey,
long seed,
ModdedGenerationMode generationMode
) {
ModdedServerAccess serverAccess = requireAccess();
synchronized (LOCK) {
ResourceKey<Level> key = levelKey(dimensionId);
RuntimePack runtimePack = switch (generationMode) {
case TRANSIENT_STUDIO -> transientStudioPack(pack, packDimensionKey);
case PERSISTENT_CREATE -> persistentRuntimePack(
ModdedGenerationHistoryStorage.createOrStage(
server,
key,
pack,
packDimensionKey,
seed
),
generationMode
);
case PERSISTENT_RESTORE -> persistentRuntimePack(
ModdedGenerationHistoryStorage.restoreOrAdopt(
server,
key,
pack,
packDimensionKey,
seed
),
generationMode
);
};
Handle existing = HANDLES.get(dimensionId);
if (existing != null && serverAccess.hasLevel(server, key)) {
existing.generator().repointAndBind(existing.level(), pack, packDimensionKey, seed);
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
HANDLES.put(dimensionId, refreshed);
return refreshed;
@@ -138,14 +205,22 @@ public final class ModdedDimensionManager {
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
}
ModdedIrisLog.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
generator.repointAndBind(present, pack, packDimensionKey, seed);
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
HANDLES.put(dimensionId, handle);
return handle;
}
try {
Handle handle = inject(server, serverAccess, dimensionId, key, pack, packDimensionKey, seed);
Handle handle = inject(
server,
serverAccess,
dimensionId,
key,
pack,
packDimensionKey,
seed,
runtimePack
);
HANDLES.put(dimensionId, handle);
ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
return handle;
@@ -157,28 +232,109 @@ public final class ModdedDimensionManager {
}
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
ModdedDimensionRegistryStore.PersistentDimension previous =
ModdedDimensionRegistryStore.get(server, dimensionId);
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed));
try {
return create(server, dimensionId, pack, packDimensionKey, seed);
} catch (Throwable e) {
synchronized (LOCK) {
ModdedDimensionRegistryStore.PersistentDimension previous =
ModdedDimensionRegistryStore.get(server, dimensionId);
if (previous != null) {
throw new IllegalStateException("Iris dimension '" + dimensionId
+ "' already exists. Use the Iris world update command to stage a pack update.");
}
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(
dimensionId,
pack,
packDimensionKey,
seed
));
try {
if (previous == null) {
return create(
server,
dimensionId,
pack,
packDimensionKey,
seed,
ModdedGenerationMode.PERSISTENT_CREATE
);
} catch (Throwable e) {
try {
ModdedDimensionRegistryStore.remove(server, dimensionId);
} else {
ModdedDimensionRegistryStore.put(server, previous);
} catch (Throwable rollbackFailure) {
e.addSuppressed(rollbackFailure);
}
} catch (Throwable rollbackFailure) {
e.addSuppressed(rollbackFailure);
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (e instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
}
if (e instanceof RuntimeException runtimeException) {
throw runtimeException;
}
}
public static UpdateResult stagePersistentUpdate(
MinecraftServer server,
String dimensionId,
String pack,
String packDimensionKey
) {
synchronized (LOCK) {
ModdedDimensionRegistryStore.PersistentDimension previous =
ModdedDimensionRegistryStore.get(server, dimensionId);
if (previous == null) {
throw new IllegalStateException("Iris dimension '" + dimensionId
+ "' is not registered. Create it before staging an update.");
}
if (e instanceof Error fatalError) {
throw fatalError;
ModdedDimensionRegistryStore.PersistentDimension updated =
new ModdedDimensionRegistryStore.PersistentDimension(
dimensionId,
pack,
packDimensionKey,
previous.seed()
);
ModdedDimensionRegistryStore.put(server, updated);
try {
ModdedGenerationHistoryStorage.ActivePack active =
ModdedGenerationHistoryStorage.createOrStage(
server,
levelKey(dimensionId),
pack,
packDimensionKey,
previous.seed()
);
Optional<GenerationActivation> pending = active.history().pendingActivation();
long activeActivationId = active.history().activeActivation().activationId();
OptionalLong pendingActivationId = pending.isPresent()
? OptionalLong.of(pending.orElseThrow().activationId())
: OptionalLong.empty();
UpdateResult result = new UpdateResult(
pending.isPresent(),
pending.isPresent(),
activeActivationId,
pendingActivationId
);
if (result.restartRequired()) {
ModdedIrisLog.warn("Iris dimension '{}' staged generation activation {} from pack={}:{}. Restart the server to activate it.",
dimensionId, pendingActivationId.orElseThrow(), pack, packDimensionKey);
} else {
ModdedIrisLog.info("Iris dimension '{}' already uses pack={}:{}; no generation update was staged.",
dimensionId, pack, packDimensionKey);
}
return result;
} catch (Throwable failure) {
try {
ModdedDimensionRegistryStore.put(server, previous);
} catch (Throwable rollbackFailure) {
failure.addSuppressed(rollbackFailure);
}
if (failure instanceof RuntimeException runtimeException) {
throw runtimeException;
}
if (failure instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris generation update staging failed for "
+ dimensionId + ".", failure);
}
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
}
}
@@ -524,48 +680,47 @@ public final class ModdedDimensionManager {
server.execute(task);
}
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
private static Holder<DimensionType> resolveDimensionType(
RegistryAccess registryAccess,
String pack,
String packDimensionKey,
RuntimePack runtimePack
) {
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey);
String typeRef = runtimePack.dimensionTypeKey();
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
ModdedRuntimeRegistry.ensureCustomBiomes(registryAccess, dimension, pack);
ModdedRuntimeRegistry.ensureDimensionType(registry, typeKey, typeRef);
return ModdedForcedDatapack.requireRegisteredDimensionType(
typeRef, registry.get(typeKey), pack, packDimensionKey);
}
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
try {
File packFolder = ModdedWorldEngines.packFolder(pack);
if (!packFolder.isDirectory()) {
throw new IllegalStateException("Iris pack directory is missing: " + packFolder.getAbsolutePath());
}
IrisDimension dimension = IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + pack
+ "' does not contain dimension '" + packDimensionKey + "'");
}
return dimension;
} catch (Throwable e) {
ModdedIrisLog.error("Iris could not load pack '{}' dimension '{}' for dimension type resolution",
pack, packDimensionKey, e);
if (e instanceof Error fatalError) {
throw fatalError;
}
throw new IllegalStateException("Iris could not load pack '" + pack + "' dimension '"
+ packDimensionKey + "' for dimension type resolution", e);
}
}
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String pack, String packDimensionKey, long seed) {
private static Handle inject(
MinecraftServer server,
ModdedServerAccess serverAccess,
String dimensionId,
ResourceKey<Level> key,
String pack,
String packDimensionKey,
long seed,
RuntimePack runtimePack
) {
RegistryAccess registryAccess = server.registryAccess();
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess, pack, packDimensionKey);
Holder<DimensionType> dimensionType = resolveDimensionType(
registryAccess,
pack,
packDimensionKey,
runtimePack
);
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef);
generator.repoint(pack, packDimensionKey, seed);
generator.repoint(
pack,
runtimePack.dimensionKey(),
seed,
runtimePack.packRoot(),
runtimePack.generationMode()
);
LevelStem stem = new LevelStem(dimensionType, generator);
WorldData worldData = server.getWorldData();
@@ -681,6 +836,77 @@ public final class ModdedDimensionManager {
return bound;
}
private static RuntimePack persistentRuntimePack(
ModdedGenerationHistoryStorage.ActivePack active,
ModdedGenerationMode generationMode
) {
return new RuntimePack(
active.packRoot(),
active.dimensionKey(),
active.dimensionContract().dimensionTypeKey(),
generationMode
);
}
private static RuntimePack transientStudioPack(String pack, String dimensionKey) {
ModdedStartup.requirePackForWorldCreation(pack);
Path packRoot = ModdedWorldEngines.resolvePack(pack, dimensionKey)
.toPath()
.toAbsolutePath()
.normalize();
PackValidationRegistry.requireLoadable(packRoot);
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IllegalStateException("Transient Iris Studio pack does not contain dimension '"
+ dimensionKey + "'.");
}
} finally {
data.close();
}
return new RuntimePack(
packRoot,
dimensionKey,
ModdedWorldgenIds.dimensionTypeRef(pack, dimensionKey),
ModdedGenerationMode.TRANSIENT_STUDIO
);
}
private record RuntimePack(
Path packRoot,
String dimensionKey,
String dimensionTypeKey,
ModdedGenerationMode generationMode
) {
private RuntimePack {
packRoot = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
dimensionKey = Objects.requireNonNull(dimensionKey, "dimensionKey");
dimensionTypeKey = Objects.requireNonNull(dimensionTypeKey, "dimensionTypeKey");
generationMode = Objects.requireNonNull(generationMode, "generationMode");
}
}
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
}
public record UpdateResult(
boolean pending,
boolean restartRequired,
long activeActivationId,
OptionalLong pendingActivationId
) {
public UpdateResult {
pendingActivationId = Objects.requireNonNull(
pendingActivationId,
"pendingActivationId"
);
if (pending != pendingActivationId.isPresent()) {
throw new IllegalArgumentException("Pending update status must match the pending activation ID.");
}
if (restartRequired != pending) {
throw new IllegalArgumentException("A pending generation activation requires a restart.");
}
}
}
}
@@ -19,6 +19,9 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
@@ -28,6 +31,7 @@ import net.minecraft.resources.Identifier;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import java.util.function.Function;
final class ModdedDimensionMetadata {
private ModdedDimensionMetadata() {
@@ -45,7 +49,10 @@ final class ModdedDimensionMetadata {
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
LinkedHashSet<String> keys = new LinkedHashSet<>(
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
collectConfiguredBiomeKeys(
dimension.getReachableBiomes(() -> data),
customBiome -> data.customBiomeResourceKey(dimension, customBiome)
));
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
if (region == null) {
continue;
@@ -60,7 +67,27 @@ final class ModdedDimensionMetadata {
return Set.copyOf(keys);
}
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
static Set<String> collectConfiguredBiomeKeys(Engine engine) {
LinkedHashSet<String> keys = new LinkedHashSet<>(collectConfiguredBiomeKeys(
engine.getDimension(), engine.getData()));
DimensionStackContext stackContext = engine.getDimensionStackContext();
if (stackContext == null) {
return Set.copyOf(keys);
}
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
if (terrainContext.isSelfReferencing()) {
continue;
}
keys.addAll(collectConfiguredBiomeKeys(
terrainContext.getDimension(), terrainContext.getData()));
}
return Set.copyOf(keys);
}
static Set<String> collectConfiguredBiomeKeys(
Iterable<IrisBiome> biomes,
Function<IrisBiomeCustom, String> customBiomeKey
) {
LinkedHashSet<String> keys = new LinkedHashSet<>();
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null) {
@@ -74,12 +101,19 @@ final class ModdedDimensionMetadata {
continue;
}
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
keys.add(IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId()));
keys.add(customBiomeKey.apply(customBiome).toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(keys);
}
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
return collectConfiguredBiomeKeys(
biomes,
customBiome -> IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId())
);
}
static int clampSpawnHeight(int minY, int height) {
int minimum = minY + 1;
int maximum = minY + height - 2;
@@ -53,6 +53,10 @@ public final class ModdedDimensionRegistryStore {
return contents(file).dimensions();
}
static List<PersistentDimension> loadWorldRoot(Path worldRoot) {
return load(storeFile(worldRoot));
}
/**
* Boot-safe load: a corrupt registry must not abort server start. The broken file is renamed aside so the
* next write starts clean, and every id we can still recognise in the raw text is reported as lost.
@@ -232,7 +236,11 @@ public final class ModdedDimensionRegistryStore {
}
private static Path storeFile(MinecraftServer server) {
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
return storeFile(server.getWorldPath(LevelResource.ROOT));
}
private static Path storeFile(Path worldRoot) {
return worldRoot.resolve("iris").resolve(FILE_NAME);
}
public record PersistentDimension(String id, String pack, String dimension, long seed) {
@@ -19,6 +19,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.history.GenerationHistoryPaths;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.Level;
@@ -38,7 +39,8 @@ public final class ModdedDimensionStorage {
"region",
"entities",
"poi",
IrisEngineMantle.STORAGE_FOLDER_NAME
IrisEngineMantle.STORAGE_FOLDER_NAME,
GenerationHistoryPaths.IRIS_DIRECTORY_NAME
);
private ModdedDimensionStorage() {
@@ -18,7 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.IrisDatapackCompiler;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
@@ -27,11 +27,25 @@ import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationHistoryPaths;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContract.PhysicalResourceKey;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
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.IrisPlatforms;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import net.minecraft.network.chat.Component;
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.packs.PackLocationInfo;
import net.minecraft.server.packs.PackSelectionConfig;
import net.minecraft.server.packs.PackType;
@@ -39,25 +53,28 @@ import net.minecraft.server.packs.PathPackResources;
import net.minecraft.server.packs.repository.Pack;
import net.minecraft.server.packs.repository.PackSource;
import net.minecraft.server.packs.repository.RepositorySource;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
@@ -67,14 +84,10 @@ public final class ModdedForcedDatapack {
private static final String PACK_ID = "iris_worldgen";
private static final String PACK_FOLDER = "iris";
private static final String HASH_FILE_NAME = "packs.hash";
// Bump this whenever the emitted datapack content changes for reasons the pack-directory hash cannot see.
// v2: custom biomes now inherit their vanilla derivative's biome tags, so every already-published pack has
// to regenerate once.
private static final String HASH_SALT = "iris-forced-datapack-v2";
private static final String HASH_SALT = "iris-forced-datapack-v3-content-addressed-biomes";
private static final long PACKS_HASH_TTL_NANOS = 2_000_000_000L;
private static final Object LOCK = new Object();
private static final AtomicBoolean LOADED = new AtomicBoolean(false);
private static final AtomicBoolean STALE_SERVE_LOGGED = new AtomicBoolean(false);
private static volatile PublishedState published;
private static volatile HashMemo packsHashMemo;
@@ -119,11 +132,9 @@ public final class ModdedForcedDatapack {
reason = "no published pack";
} else if (!hash.isEmpty() && !state.packsHash().equals(hash)) {
reason = "stale cache (hash changed)";
} else if (hash.isEmpty()) {
reason = "current registry inputs could not be verified";
} else {
if (hash.isEmpty() && STALE_SERVE_LOGGED.compareAndSet(false, true)) {
ModdedIrisLog.warn("Iris cannot hash the installed packs; serving the last generated forced datapack from {} unverified",
state.directory());
}
try {
return requireReadablePack(state.directory());
} catch (RuntimeException unreadable) {
@@ -163,17 +174,7 @@ public final class ModdedForcedDatapack {
}
private static Pack buildPack() {
try {
return requireReadablePack(regenerate());
} catch (RuntimeException | Error generationFailure) {
Path lastKnownGood = packDirectory();
if (Files.isRegularFile(lastKnownGood.resolve("pack.mcmeta"))) {
ModdedIrisLog.error("Iris kept the last known-good generated datapack after regeneration failed",
generationFailure);
return requireReadablePack(lastKnownGood);
}
throw generationFailure;
}
return requireReadablePack(regenerate());
}
private static Pack requireReadablePack(Path directory) {
@@ -263,7 +264,6 @@ public final class ModdedForcedDatapack {
// Publish the hash this run was built from as the memo too: a memo captured before staging would
// otherwise mismatch what was just published and send the next serve straight back into buildPack.
packsHashMemo = new HashMemo(packsHash, System.nanoTime());
STALE_SERVE_LOGGED.set(false);
return packDirectory();
} catch (IOException | RuntimeException | Error failure) {
try {
@@ -335,10 +335,6 @@ public final class ModdedForcedDatapack {
return hash;
}
/**
* Content hash over the installed packs: relative path, size and mtime of every regular file, plus the
* pack format and loader the generated datapack is shaped for.
*/
private static String packsHash() throws IOException {
MessageDigest digest;
try {
@@ -348,39 +344,13 @@ public final class ModdedForcedDatapack {
}
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
+ '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
Path root = packsRoot();
if (Files.isDirectory(root)) {
List<String> entries = new ArrayList<>();
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) {
return !directory.equals(root)
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())
? FileVisitResult.SKIP_SUBTREE
: FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
if (attributes.isRegularFile()) {
entries.add(root.relativize(file).toString().replace('\\', '/')
+ '|' + attributes.size() + '|' + attributes.lastModifiedTime().toMillis());
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException failure) {
entries.add(root.relativize(file).toString().replace('\\', '/') + "|unreadable");
return FileVisitResult.CONTINUE;
}
});
entries.sort(Comparator.naturalOrder());
for (String entry : entries) {
digest.update(entry.getBytes(StandardCharsets.UTF_8));
digest.update((byte) '\n');
}
}
String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
compilationPackRoots(),
List.of(),
DataVersion.getLatest().get(),
false
);
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(digest.digest());
}
@@ -390,10 +360,11 @@ public final class ModdedForcedDatapack {
int packCount = 0;
KList<String> presetIds = new KList<>();
File root = packsRoot().toFile();
List<File> packs = PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(root);
for (File pack : packs) {
if (stagePack(pack, fixer, stagingDirectory, seenBiomes, presetIds)) {
Path worldRoot = worldRootOrNull();
Set<PackSelection> legacySelections = unadoptedSelections(worldRoot);
for (File pack : compilationPackRoots(worldRoot)) {
PackStageContext context = packStageContext(pack.toPath(), legacySelections);
if (stagePack(context, fixer, stagingDirectory, seenBiomes, presetIds)) {
packCount++;
}
}
@@ -414,19 +385,28 @@ public final class ModdedForcedDatapack {
}
}
private static boolean stagePack(File sourcePack, IDataFixer fixer, Path stagingDirectory,
private static boolean stagePack(PackStageContext context, IDataFixer fixer, Path stagingDirectory,
Map<String, KSet<String>> seenBiomes,
KList<String> presetIds) throws IOException {
File sourcePack = context.packRoot().toFile();
PackValidationResult validation;
try {
validation = PackValidator.validateForDatapackBootstrap(sourcePack);
} catch (Throwable validationFailure) {
if (context.required()) {
throw requiredPackFailure(context, "validation", validationFailure);
}
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because validation failed",
sourcePack.getName(), validationFailure);
rethrowIfUnrecoverable(validationFailure);
return false;
}
if (!validation.isLoadable()) {
if (context.required()) {
throw new IOException("Retained Iris generation pack at " + context.packRoot()
+ " has " + validation.getBlockingErrors().size()
+ " blocking validation error(s): " + validation.getBlockingErrors().getFirst());
}
ModdedIrisLog.error("Iris excluded pack '{}' from Create World: {} blocking validation error(s); first error: {}",
sourcePack.getName(), validation.getBlockingErrors().size(),
validation.getBlockingErrors().getFirst());
@@ -441,8 +421,11 @@ public final class ModdedForcedDatapack {
packFolders.add(packStagingDirectory.toFile());
boolean installed;
try {
installed = installPack(sourcePack, fixer, packFolders, packBiomes, packPresetIds);
installed = installPack(context, fixer, packFolders, packBiomes, packPresetIds);
} catch (Throwable installationFailure) {
if (context.required()) {
throw requiredPackFailure(context, "datapack serialization", installationFailure);
}
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because datapack serialization failed",
sourcePack.getName(), installationFailure);
rethrowIfUnrecoverable(installationFailure);
@@ -481,7 +464,7 @@ public final class ModdedForcedDatapack {
}
}
private static void mergeDirectory(Path sourceDirectory, Path destinationDirectory) throws IOException {
static void mergeDirectory(Path sourceDirectory, Path destinationDirectory) throws IOException {
List<Path> entries = new ArrayList<>();
try (Stream<Path> walk = Files.walk(sourceDirectory)) {
walk.sorted(Comparator.comparingInt(Path::getNameCount)).forEach(entries::add);
@@ -490,9 +473,14 @@ public final class ModdedForcedDatapack {
Path relative = sourceDirectory.relativize(source);
Path destination = destinationDirectory.resolve(relative);
if (Files.isDirectory(source)) {
if (Files.exists(destination) && !Files.isDirectory(destination)) {
throw new IOException("Conflicting Iris datapack output at " + destination + ".");
}
Files.createDirectories(destination);
} else if (!Files.exists(destination)) {
Files.copy(source, destination);
} else if (!Files.isRegularFile(destination) || Files.mismatch(source, destination) != -1L) {
throw new IOException("Conflicting Iris datapack output at " + destination + ".");
}
}
}
@@ -505,37 +493,65 @@ public final class ModdedForcedDatapack {
}
}
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders,
private static boolean installPack(PackStageContext context, IDataFixer fixer, KList<File> folders,
Map<String, KSet<String>> seenBiomes,
KList<String> presetIds) throws IOException {
String packName = packFolder.getName();
File packFolder = context.packRoot().toFile();
String packName = context.packName();
File dimensionsDirectory = new File(packFolder, "dimensions");
if (!dimensionsDirectory.isDirectory()) {
return false;
}
IrisData data = IrisData.get(packFolder);
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
if (dimensionKeys == null || dimensionKeys.length == 0) {
return false;
}
List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
for (String dimensionKey : sortedDimensionKeys) {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
+ dimensionKey + "' did not load while building the forced datapack");
IrisData data = IrisData.openDatapackCompiler(packFolder);
try {
if (context.retainedContract() != null) {
data.bindGenerationRegistryContract(context.retainedContract());
}
String biomePathPrefix = ModdedWorldgenIds.biomePathPrefix(packName, dimensionKey);
dimension.installBiomes(fixer, () -> data, folders, "irisworldgen", biomePathPrefix,
biomesForNamespace(seenBiomes, biomePathPrefix));
dimension.installBiomes(fixer, () -> data, folders,
biomesForNamespace(seenBiomes, dimension.getLoadKey()));
writeDimensionType(folders, fixer, dimension, packName, dimensionKey);
String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey);
writeWorldPreset(folders, packName, dimensionKey, presetRef);
presetIds.add(presetRef);
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
if (dimensionKeys == null || dimensionKeys.length == 0) {
return false;
}
int installedDimensions = 0;
List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
for (String dimensionKey : sortedDimensionKeys) {
if (context.dimensionKey() != null && !context.dimensionKey().equals(dimensionKey)) {
continue;
}
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
+ dimensionKey + "' did not load while building the forced datapack");
}
dimension.installBiomes(
fixer,
() -> data,
folders,
biomesForNamespace(seenBiomes, "iris"),
context
);
writeDimensionType(
folders,
fixer,
dimension,
context.dimensionTypeRef(dimensionKey),
context.emitLegacyDimensionTypeAlias(),
context
);
if (context.emitPresets()) {
String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey);
writeWorldPreset(folders, packName, dimensionKey, presetRef);
presetIds.add(presetRef);
}
installedDimensions++;
}
if (context.required() && installedDimensions == 0) {
throw new IOException("Retained Iris generation pack at " + context.packRoot()
+ " does not contain dimension '" + context.dimensionKey() + "'.");
}
return installedDimensions > 0;
} finally {
data.close();
}
return true;
}
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
@@ -625,15 +641,41 @@ public final class ModdedForcedDatapack {
static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension,
String pack, String packDimensionKey) throws IOException {
writeDimensionType(
folders,
fixer,
dimension,
ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey),
true,
IrisCustomBiomeAliasResolver.none()
);
}
private static void writeDimensionType(
KList<File> folders,
IDataFixer fixer,
IrisDimension dimension,
String typeRef,
boolean emitLegacyAlias,
IrisCustomBiomeAliasResolver sourceResolver
) throws IOException {
IrisDimensionType type = dimension.getDimensionType();
String json = type.toJson(fixer);
String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey);
String json = Objects.requireNonNull(sourceResolver, "sourceResolver").generatedSource(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
typeRef,
type.toJson(fixer),
fixer
);
String typePath = typeRef.substring(typeRef.indexOf(':') + 1);
String typeNamespace = typeRef.substring(0, typeRef.indexOf(':'));
for (File datapackRoot : folders) {
Path output = datapackRoot.toPath().resolve("data").resolve("irisworldgen")
Path output = datapackRoot.toPath().resolve("data").resolve(typeNamespace)
.resolve("dimension_type").resolve(typePath + ".json");
Files.createDirectories(output.getParent());
Files.writeString(output, json, StandardCharsets.UTF_8);
if (!emitLegacyAlias) {
continue;
}
// Load-bearing, not dead: worlds created before the scoped pack path reference
// irisworldgen:<dimensionTypeKey> in their level.dat, and ModdedWorldEngines accepts that legacy
// key when validating the runtime dimension contract. Removing this emission unloads those worlds.
@@ -725,10 +767,239 @@ public final class ModdedForcedDatapack {
}
}
private static List<File> compilationPackRoots() throws IOException {
return compilationPackRoots(worldRootOrNull());
}
private static List<File> compilationPackRoots(Path worldRoot) throws IOException {
Path dataDirectory = packsRoot().getParent();
Path historyRoot = worldRoot == null
? dataDirectory.resolve(".iris-world-unavailable")
: worldRoot;
return IrisDatapackCompiler.collectPackRoots(dataDirectory, historyRoot);
}
private static Path worldRootOrNull() throws IOException {
MinecraftServer server = ModdedEngineBootstrap.currentServer();
if (server != null) {
return server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
}
if (ModdedEngineBootstrap.loader().clientEnvironment()) {
return null;
}
return MainWorldService.configuredWorldRootIfPresent();
}
private static Set<PackSelection> unadoptedSelections(Path worldRoot) throws IOException {
if (worldRoot == null) {
return Set.of();
}
Set<PackSelection> selections = new LinkedHashSet<>();
for (ModdedDimensionRegistryStore.PersistentDimension dimension
: ModdedDimensionRegistryStore.loadWorldRoot(worldRoot)) {
Identifier identifier = Identifier.tryParse(dimension.id());
if (identifier == null) {
throw new IOException("Invalid persistent Iris dimension ID '" + dimension.id() + "'.");
}
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, identifier);
Path dimensionRoot = DimensionType.getStorageFolder(key, worldRoot).toAbsolutePath().normalize();
Path generationRoot = GenerationHistoryPaths.forDimension(dimensionRoot).generationRoot();
if (!Files.exists(generationRoot)) {
selections.add(new PackSelection(dimension.pack(), dimension.dimension()));
}
}
return Set.copyOf(selections);
}
private static PackStageContext packStageContext(
Path packRoot,
Set<PackSelection> unadoptedSelections
) throws IOException {
Path normalizedRoot = packRoot.toAbsolutePath().normalize();
RetainedPack retained = retainedPack(normalizedRoot);
if (retained != null) {
ModdedWorldgenIds.ScopedDimension identity = ModdedWorldgenIds
.scopedDimensionType(retained.epoch().dimensionContract().dimensionTypeKey())
.orElseThrow(() -> new IOException("Retained Iris dimension type key is not scoped: "
+ retained.epoch().dimensionContract().dimensionTypeKey()));
if (!identity.dimension().equals(retained.epoch().dimensionContract().dimensionKey())) {
throw new IOException("Retained Iris dimension type identity does not match its epoch contract.");
}
return PackStageContext.retained(normalizedRoot, identity.pack(), retained.epoch());
}
String packName = normalizedRoot.getFileName().toString();
Set<String> legacyDimensions = new LinkedHashSet<>();
for (PackSelection selection : unadoptedSelections) {
if (selection.pack().equals(packName)) {
legacyDimensions.add(selection.dimension());
}
}
return PackStageContext.installed(normalizedRoot, packName, legacyDimensions);
}
private static RetainedPack retainedPack(Path packRoot) throws IOException {
Path epochRoot = packRoot.getParent();
Path epochsRoot = epochRoot == null ? null : epochRoot.getParent();
Path generationRoot = epochsRoot == null ? null : epochsRoot.getParent();
Path irisRoot = generationRoot == null ? null : generationRoot.getParent();
Path dimensionRoot = irisRoot == null ? null : irisRoot.getParent();
if (epochRoot == null
|| epochsRoot == null
|| generationRoot == null
|| irisRoot == null
|| dimensionRoot == null
|| !"pack".equals(packRoot.getFileName().toString())
|| !GenerationHistoryPaths.EPOCHS_DIRECTORY_NAME.equals(epochsRoot.getFileName().toString())
|| !GenerationHistoryPaths.GENERATION_DIRECTORY_NAME.equals(generationRoot.getFileName().toString())
|| !GenerationHistoryPaths.IRIS_DIRECTORY_NAME.equals(irisRoot.getFileName().toString())) {
return null;
}
GenerationHistory history = GenerationHistory.open(dimensionRoot);
String epochId = epochRoot.getFileName().toString();
GenerationEpoch epoch = history.manifest().epoch(epochId).orElseThrow(
() -> new IOException("Retained Iris pack is not referenced by generation history: " + packRoot)
);
if (!history.paths().packRoot(epochId).toAbsolutePath().normalize().equals(packRoot)) {
throw new IOException("Retained Iris pack path does not match generation history: " + packRoot);
}
return new RetainedPack(history, epoch);
}
private static IOException requiredPackFailure(
PackStageContext context,
String operation,
Throwable failure
) {
if (failure instanceof IOException ioFailure) {
return ioFailure;
}
return new IOException("Retained Iris generation pack at " + context.packRoot()
+ " failed " + operation + ".", failure);
}
private static Path packsRoot() {
return IrisPlatforms.get().packsFolderNoCreate().toPath();
}
private record PackSelection(String pack, String dimension) {
}
private record RetainedPack(GenerationHistory history, GenerationEpoch epoch) {
}
private record PackStageContext(
Path packRoot,
String packName,
String dimensionKey,
String retainedDimensionTypeRef,
boolean emitPresets,
boolean emitLegacyDimensionTypeAlias,
boolean required,
Set<String> legacyDimensions,
GenerationRegistryContract retainedContract
) implements IrisCustomBiomeAliasResolver {
private static PackStageContext installed(
Path packRoot,
String packName,
Set<String> legacyDimensions
) {
return new PackStageContext(
packRoot,
packName,
null,
null,
true,
true,
false,
Set.copyOf(legacyDimensions),
null
);
}
private static PackStageContext retained(
Path packRoot,
String packName,
GenerationEpoch epoch
) {
return new PackStageContext(
packRoot,
packName,
epoch.dimensionContract().dimensionKey(),
epoch.dimensionContract().dimensionTypeKey(),
false,
true,
true,
Set.of(),
epoch.registryContract()
);
}
private String dimensionTypeRef(String installedDimensionKey) {
return retainedDimensionTypeRef == null
? ModdedWorldgenIds.dimensionTypeRef(packName, installedDimensionKey)
: retainedDimensionTypeRef;
}
@Override
public String physicalResourceKey(
IrisDimension dimension,
IrisBiome biome,
IrisBiomeCustom customBiome,
String currentPhysicalResourceKey
) throws IOException {
if (retainedContract == null) {
return currentPhysicalResourceKey;
}
return dimension.getLoader().customBiomeResourceKey(dimension, customBiome);
}
@Override
public List<String> aliases(
IrisDimension dimension,
IrisBiome biome,
IrisBiomeCustom customBiome,
String physicalResourceKey
) {
List<String> candidates = ModdedWorldgenIds.legacyBiomeRefs(
packName,
dimension.getLoadKey(),
customBiome.getId()
);
if (legacyDimensions.contains(dimension.getLoadKey())) {
return candidates;
}
if (retainedContract == null) {
return List.of();
}
List<String> aliases = new ArrayList<>(candidates.size());
for (String candidate : candidates) {
PhysicalResourceKey key = new PhysicalResourceKey(
GenerationRegistryContractFactory.BIOME_REGISTRY,
candidate
);
if (retainedContract.definitions().containsKey(key)) {
aliases.add(candidate);
}
}
return List.copyOf(aliases);
}
@Override
public String generatedSource(
String registryKey,
String resourceKey,
String currentSource,
IDataFixer fixer
) throws IOException {
if (retainedContract == null) {
return currentSource;
}
PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey);
return GenerationRegistryContractFactory.requireGeneratedSource(retainedContract, key, fixer);
}
}
private record PublishedState(Path directory, String packsHash) {
}
@@ -0,0 +1,361 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
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.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
final class ModdedGenerationHistoryStorage {
private ModdedGenerationHistoryStorage() {
}
static ActivePack createOrStage(
MinecraftServer server,
ResourceKey<Level> levelKey,
String selectedPack,
String selectedDimensionKey,
long worldSeed
) {
Path dimensionRoot = dimensionRoot(server, levelKey);
try {
Path source = selectedPackSource(selectedPack, selectedDimensionKey);
Candidate candidate = captureCandidate(source, selectedDimensionKey);
GenerationHistory history = createOrStage(
dimensionRoot,
source,
worldSeed,
candidate,
ModdedGenerationHistoryStorage::requireRegistryDefinitions,
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
);
return resolveActive(history);
} catch (IOException failure) {
throw historyFailure(levelKey, failure);
}
}
static ActivePack restoreOrAdopt(
MinecraftServer server,
ResourceKey<Level> levelKey,
String selectedPack,
String selectedDimensionKey,
long worldSeed
) {
Path dimensionRoot = dimensionRoot(server, levelKey);
try {
GenerationHistory history = restoreOrAdopt(
dimensionRoot,
worldSeed,
() -> {
Path source = selectedPackSource(selectedPack, selectedDimensionKey);
return new AdoptionSource(source, captureCandidate(
source,
selectedDimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
));
},
ModdedGenerationHistoryStorage::requireRegistryDefinitions
);
return resolveActive(history);
} catch (IOException failure) {
throw historyFailure(levelKey, failure);
}
}
static ActivePack openOrAdopt(
ServerLevel level,
String selectedPack,
String selectedDimensionKey,
long worldSeed
) {
return restoreOrAdopt(
level.getServer(),
level.dimension(),
selectedPack,
selectedDimensionKey,
worldSeed
);
}
static GenerationHistory createOrStage(
Path dimensionRoot,
Path source,
long worldSeed,
Candidate candidate,
RegistryContractValidator validator,
int transitionWidthBlocks
) throws IOException {
Path requiredRoot = normalize(dimensionRoot);
Path requiredSource = normalize(source);
Candidate requiredCandidate = Objects.requireNonNull(candidate, "candidate");
RegistryContractValidator requiredValidator = Objects.requireNonNull(validator, "validator");
Optional<GenerationHistory> existing = GenerationHistory.openIfPresent(requiredRoot, worldSeed);
if (existing.isEmpty()) {
captureAndRequireRegistryDefinitions(
List.of(requiredCandidate.registryContract()),
requiredValidator
);
Files.createDirectories(requiredRoot);
return GenerationHistory.create(
requiredRoot,
requiredSource,
requiredCandidate.packFingerprint(),
worldSeed,
requiredCandidate.dimensionContract(),
requiredCandidate.registryContract()
);
}
GenerationHistory history = existing.get();
captureAndRequireRegistryDefinitions(registryContracts(history), requiredValidator);
history.stageUpdate(
requiredSource,
requiredCandidate.packFingerprint(),
requiredCandidate.dimensionContract(),
requiredCandidate.registryContract(),
transitionWidthBlocks
);
return history;
}
static GenerationHistory restoreOrAdopt(
Path dimensionRoot,
long worldSeed,
AdoptionSourceSupplier sourceSupplier,
RegistryContractValidator validator
) throws IOException {
Path requiredRoot = normalize(dimensionRoot);
RegistryContractValidator requiredValidator = Objects.requireNonNull(validator, "validator");
Optional<GenerationHistory> existing = GenerationHistory.openIfPresent(requiredRoot, worldSeed);
if (existing.isPresent()) {
requireRegistryDefinitions(existing.get(), requiredValidator);
return existing.get();
}
AdoptionSource adoption = Objects.requireNonNull(sourceSupplier, "sourceSupplier").get();
Candidate candidate = Objects.requireNonNull(adoption.candidate(), "candidate");
captureAndRequireRegistryDefinitions(List.of(candidate.registryContract()), requiredValidator);
Files.createDirectories(requiredRoot);
return GenerationHistory.create(
requiredRoot,
normalize(adoption.packSource()),
candidate.packFingerprint(),
worldSeed,
candidate.dimensionContract(),
candidate.registryContract()
);
}
static ActivePack resolveActive(GenerationHistory history) throws IOException {
GenerationEpoch epoch = history.activeEpoch();
Path packRoot = normalize(history.activePackRoot());
validatePack(packRoot, epoch.packFingerprint());
validateDimension(packRoot, epoch.dimensionContract());
return new ActivePack(
packRoot,
epoch.dimensionContract().dimensionKey(),
epoch.dimensionContract(),
history
);
}
private static void validatePack(Path packRoot, String fingerprint) {
PackValidationResult validation = PackValidator.validate(packRoot.toFile());
PackValidationRegistry.publish(packRoot, validation, fingerprint);
if (!validation.isLoadable()) {
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
}
}
private static void validateDimension(
Path packRoot,
GenerationEpoch.DimensionContract recordedContract
) throws IOException {
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(recordedContract.dimensionKey(), false);
if (dimension == null) {
throw new IOException("Immutable generation pack does not contain dimension '"
+ recordedContract.dimensionKey() + "'.");
}
GenerationEpoch.DimensionContract actualContract = GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
recordedContract.dimensionTypeKey()
);
if (!recordedContract.equals(actualContract)) {
throw new IOException("Immutable generation pack no longer matches its recorded dimension contract.");
}
} finally {
data.close();
}
}
private static Candidate captureCandidate(Path source, String dimensionKey) throws IOException {
return captureCandidate(
source,
dimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.CONTENT_ADDRESSED_ONLY
);
}
private static Candidate captureCandidate(
Path source,
String dimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy aliasPolicy
) throws IOException {
String fingerprint = GenerationPackFingerprint.compute(
source,
GenerationPackFingerprint.CURRENT_VERSION
);
IrisData data = IrisData.openDatapackCompiler(source.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IOException("Selected Iris pack does not contain dimension '" + dimensionKey + "'.");
}
PlatformGenerationRegistry generationRegistry = IrisPlatforms.get()
.registries()
.generationRegistry();
String dimensionTypeKey = generationRegistry.dimensionTypeResourceKey(
source.getFileName().toString(),
dimension.getLoadKey(),
dimension.getDimensionTypeKey()
);
GenerationEpoch.DimensionContract dimensionContract = GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
dimensionTypeKey
);
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
data,
dimension,
fingerprint,
aliasPolicy
);
return new Candidate(fingerprint, dimensionContract, registryContract);
} finally {
data.close();
}
}
private static Path selectedPackSource(String pack, String dimensionKey) {
ModdedStartup.requirePackForWorldCreation(pack);
return normalize(ModdedWorldEngines.resolvePack(pack, dimensionKey).toPath());
}
private static GenerationRegistryContract requireRegistryDefinitions(
Collection<GenerationRegistryContract> contracts
)
throws IOException {
return GenerationRegistryContractFactory.captureRequiredDefinitions(contracts);
}
private static void requireRegistryDefinitions(
GenerationHistory history,
RegistryContractValidator validator
) throws IOException {
captureAndRequireRegistryDefinitions(registryContracts(history), validator);
}
private static void captureAndRequireRegistryDefinitions(
Collection<GenerationRegistryContract> contracts,
RegistryContractValidator validator
) throws IOException {
GenerationRegistryContract available = validator.capture(contracts);
for (GenerationRegistryContract contract : contracts) {
contract.requireDefinitionsAvailableIn(available);
}
}
private static List<GenerationRegistryContract> registryContracts(GenerationHistory history) {
Collection<GenerationEpoch> epochs = history.manifest().epochs();
List<GenerationRegistryContract> contracts = new ArrayList<>(epochs.size());
for (GenerationEpoch epoch : epochs) {
contracts.add(epoch.registryContract());
}
return contracts;
}
private static Path dimensionRoot(MinecraftServer server, ResourceKey<Level> levelKey) {
return normalize(ModdedDimensionStorage.storageFolder(server, levelKey).toPath());
}
private static Path normalize(Path path) {
return Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
}
private static IllegalStateException historyFailure(ResourceKey<Level> levelKey, IOException failure) {
return new IllegalStateException("Iris generation history is unusable for '"
+ levelKey.identifier() + "'.", failure);
}
record ActivePack(
Path packRoot,
String dimensionKey,
GenerationEpoch.DimensionContract dimensionContract,
GenerationHistory history
) {
ActivePack {
packRoot = normalize(packRoot);
dimensionKey = Objects.requireNonNull(dimensionKey, "dimensionKey");
dimensionContract = Objects.requireNonNull(dimensionContract, "dimensionContract");
history = Objects.requireNonNull(history, "history");
}
}
record Candidate(
String packFingerprint,
GenerationEpoch.DimensionContract dimensionContract,
GenerationRegistryContract registryContract
) {
Candidate {
packFingerprint = Objects.requireNonNull(packFingerprint, "packFingerprint");
dimensionContract = Objects.requireNonNull(dimensionContract, "dimensionContract");
registryContract = Objects.requireNonNull(registryContract, "registryContract");
}
}
record AdoptionSource(Path packSource, Candidate candidate) {
AdoptionSource {
packSource = normalize(packSource);
candidate = Objects.requireNonNull(candidate, "candidate");
}
}
@FunctionalInterface
interface AdoptionSourceSupplier {
AdoptionSource get() throws IOException;
}
@FunctionalInterface
interface RegistryContractValidator {
GenerationRegistryContract capture(Collection<GenerationRegistryContract> contracts) throws IOException;
}
}
@@ -0,0 +1,17 @@
package art.arcane.iris.modded;
enum ModdedGenerationMode {
PERSISTENT_CREATE(true),
PERSISTENT_RESTORE(true),
TRANSIENT_STUDIO(false);
private final boolean historyRequired;
ModdedGenerationMode(boolean historyRequired) {
this.historyRequired = historyRequired;
}
boolean historyRequired() {
return historyRequired;
}
}
@@ -0,0 +1,191 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.dimension.DimensionType;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
final class ModdedGenerationRegistry implements PlatformGenerationRegistry {
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
private static final String BLOCK_REGISTRY = "minecraft:block";
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
private final Supplier<MinecraftServer> server;
private final String runtimeIdentity;
private final String rendererIdentity;
ModdedGenerationRegistry(
Supplier<MinecraftServer> server,
String runtimeIdentity,
String rendererIdentity
) {
this.server = Objects.requireNonNull(server, "server");
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
}
@Override
public String runtimeIdentity() {
return runtimeIdentity;
}
@Override
public String generatedDefinitionRendererIdentity() {
return rendererIdentity;
}
@Override
public String customBiomeResourceKey(String identitySha256) {
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
}
@Override
public List<String> legacyCustomBiomeResourceKeys(
String packName,
String dimensionKey,
String customBiomeId
) {
return ModdedWorldgenIds.legacyBiomeRefs(
requireText(packName, "packName"),
requireText(dimensionKey, "dimensionKey"),
requireText(customBiomeId, "customBiomeId")
);
}
@Override
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
return ModdedWorldgenIds.dimensionTypeRef(
requireText(packName, "packName"),
requireText(dimensionKey, "dimensionKey")
);
}
@Override
public Definition definition(String registryKey, String resourceKey) {
MinecraftServer instance = Objects.requireNonNull(server.get(), "Minecraft server");
RegistryAccess access = instance.registryAccess();
return switch (registryKey) {
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
case DIMENSION_TYPE_REGISTRY -> presence(
access,
Registries.DIMENSION_TYPE,
registryKey,
resourceKey
);
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
};
}
@Override
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
MinecraftServer instance = Objects.requireNonNull(server.get(), "Minecraft server");
RegistryAccess access = instance.registryAccess();
return switch (registryKey) {
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
default -> throw new IllegalArgumentException(
"Registry does not accept generated JSON definitions: " + registryKey
);
};
}
@Override
public Definition generatedDefinition(String registryKey, String resourceKey) {
MinecraftServer instance = Objects.requireNonNull(server.get(), "Minecraft server");
RegistryAccess access = instance.registryAccess();
return switch (registryKey) {
case BIOME_REGISTRY -> exactRegistered(
access,
Registries.BIOME,
Biome.DIRECT_CODEC,
resourceKey
);
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
access,
Registries.DIMENSION_TYPE,
DimensionType.DIRECT_CODEC,
resourceKey
);
default -> throw new IllegalArgumentException(
"Registry does not contain Iris-generated definitions: " + registryKey
);
};
}
private static <T> Definition presence(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
String registryResourceKey,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
+ " / " + resourceKey + ".");
}
return Definition.resourceIdentity(registryResourceKey, resourceKey);
}
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static <T> Definition exactRegistered(
RegistryAccess access,
ResourceKey<? extends Registry<T>> registryKey,
Codec<T> codec,
String resourceKey
) {
Registry<T> registry = access.lookupOrThrow(registryKey);
Identifier identifier = requireIdentifier(resourceKey);
T value = registry.getValue(identifier);
if (value == null) {
throw new IllegalStateException("Missing generated registry definition "
+ registryKey.identifier() + " / " + resourceKey + ".");
}
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
return Definition.exactJson(encoded.toString());
}
private static Identifier requireIdentifier(String resourceKey) {
Identifier identifier = Identifier.tryParse(resourceKey);
if (identifier == null) {
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
}
return identifier;
}
private static String requireText(String value, String label) {
String required = Objects.requireNonNull(value, label).trim();
if (required.isEmpty()) {
throw new IllegalArgumentException(label + " must not be blank.");
}
return required;
}
}
@@ -82,13 +82,11 @@ import java.util.function.Predicate;
*/
final class ModdedImportedFeatureStage {
private static final String CYCLE_MARKER = "Feature order cycle found";
private static final long NO_GENERATION = Long.MIN_VALUE;
private final IrisModdedBiomeSource biomeSource;
private final ReentrantLock buildLock = new ReentrantLock();
private final ConcurrentHashMap<Integer, FeatureTable> featureTables = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> inertGenerations = new ConcurrentHashMap<>();
private volatile IrisModdedChunkGenerator generator;
private volatile FeatureTable featureTable;
private volatile long inertGeneration = NO_GENERATION;
ModdedImportedFeatureStage(IrisModdedBiomeSource biomeSource) {
this.biomeSource = biomeSource;
@@ -103,12 +101,12 @@ final class ModdedImportedFeatureStage {
* pack can never serve another.
*/
void invalidate() {
featureTable = null;
inertGeneration = NO_GENERATION;
featureTables.clear();
inertGenerations.clear();
}
boolean active() {
return featureTable != null;
return !featureTables.isEmpty();
}
/**
@@ -120,7 +118,8 @@ final class ModdedImportedFeatureStage {
* <p>With {@code importedFeatures} disabled there is no table and this is vanilla's default getter.
*/
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
FeatureTable table = featureTable;
Engine engine = generator == null ? null : generator.engineOrNull();
FeatureTable table = engine == null ? null : featureTables.get(engine.getCacheID());
if (table == null) {
return biome.value().getGenerationSettings();
}
@@ -151,8 +150,9 @@ final class ModdedImportedFeatureStage {
if (engine == null || engine.isClosed() || engine.isClosing()) {
return;
}
int runtimeIdentity = engine.getCacheID();
long generation = biomeSource.packGeneration();
if (settled(generation)) {
if (settled(runtimeIdentity, generation)) {
return;
}
if (waitForBuild) {
@@ -161,35 +161,35 @@ final class ModdedImportedFeatureStage {
return;
}
try {
if (settled(generation)) {
if (settled(runtimeIdentity, generation)) {
return;
}
build(engine, generation);
build(engine, runtimeIdentity, generation);
} finally {
buildLock.unlock();
}
}
private boolean settled(long generation) {
FeatureTable current = featureTable;
private boolean settled(int runtimeIdentity, long generation) {
FeatureTable current = featureTables.get(runtimeIdentity);
if (current != null && current.generation() == generation) {
return true;
}
return inertGeneration == generation;
return Long.valueOf(generation).equals(inertGenerations.get(runtimeIdentity));
}
private void build(Engine engine, long generation) {
private void build(Engine engine, int runtimeIdentity, long generation) {
IrisImportedFeatureControl control;
try {
control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) {
ModdedIrisLog.error("Iris could not read importedFeatures for this dimension; features off: {}",
error.toString());
markInert(generation);
markInert(runtimeIdentity, generation);
return;
}
if (!control.shouldGenerateFeatures()) {
markInert(generation);
markInert(runtimeIdentity, generation);
return;
}
FeatureTable built;
@@ -199,15 +199,15 @@ final class ModdedImportedFeatureStage {
} catch (Throwable error) {
ModdedIrisLog.error("Iris importedFeatures is off for {}: feature table construction failed: {}",
dimensionKey(engine), error.toString());
markInert(generation);
markInert(runtimeIdentity, generation);
return;
}
if (built == null) {
markInert(generation);
markInert(runtimeIdentity, generation);
return;
}
featureTable = built;
inertGeneration = NO_GENERATION;
featureTables.put(runtimeIdentity, built);
inertGenerations.remove(runtimeIdentity);
// Arm the worldcheck log watch here, before any chunk decorates: arming from the first pass instead
// missed every far-chunk write the first chunk made. No-op unless -Diris.worldcheck is set.
WorldCheckFeaturePlacement.arm();
@@ -216,9 +216,9 @@ final class ModdedImportedFeatureStage {
built.derivatives().size());
}
private void markInert(long generation) {
featureTable = null;
inertGeneration = generation;
private void markInert(int runtimeIdentity, long generation) {
featureTables.remove(runtimeIdentity);
inertGenerations.put(runtimeIdentity, generation);
}
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
@@ -312,7 +312,8 @@ final class ModdedImportedFeatureStage {
* control is disabled or the table degraded.
*/
void run(WorldGenLevel level, ChunkAccess chunk, Engine engine) {
FeatureTable table = featureTable;
int runtimeIdentity = engine.getCacheID();
FeatureTable table = featureTables.get(runtimeIdentity);
if (table == null) {
WorldCheckFeaturePlacement.recordFeaturesOff();
return;
@@ -320,7 +321,8 @@ final class ModdedImportedFeatureStage {
if (table.generation() != biomeSource.packGeneration()) {
// A repoint landed between the prepare above and this chunk. Refuse stale content outright; the
// next chunk's prepare rebuilds against the new pack.
invalidate();
featureTables.remove(runtimeIdentity, table);
inertGenerations.remove(runtimeIdentity);
return;
}
IrisModdedChunkGenerator owner = generator;
@@ -423,6 +425,11 @@ final class ModdedImportedFeatureStage {
};
}
void evictRuntime(int runtimeIdentity) {
featureTables.remove(runtimeIdentity);
inertGenerations.remove(runtimeIdentity);
}
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed,
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -233,17 +234,30 @@ final class ModdedNativeStructureStage {
if (!start.isValid() || previousStarts.get(structure) == start) {
continue;
}
if (configuredStarts.containsKey(structure)) {
recordWorldCheckStructureShift(
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
continue;
}
Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString();
if (structureId == null) {
throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z());
}
BoundingBox footprint = start.getBoundingBox();
if (!current.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
footprint.maxZ())) {
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
if (configuredStarts.containsKey(structure)) {
NativeStructureOwnershipStore.discard(
current, structureId, chunkPos.x(), chunkPos.z());
}
continue;
}
if (configuredStarts.containsKey(structure)) {
recordWorldCheckStructureShift(
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
continue;
}
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision;
try {
@@ -326,6 +340,14 @@ final class ModdedNativeStructureStage {
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) {
BoundingBox footprint = start.getBoundingBox();
if (!current.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
footprint.maxZ())) {
continue;
}
NativeStructureOwnershipRecord ownership =
NativeStructureOwnershipRecovery.resolve(
current, world.getLevel(), structureId, structure, start);
@@ -18,6 +18,7 @@
package art.arcane.iris.modded;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.modded.api.ModdedDataType;
import art.arcane.iris.spi.IrisLogging;
@@ -25,6 +26,7 @@ import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.data.UnresolvedKeyLog;
@@ -59,6 +61,19 @@ public final class ModdedRegistries implements PlatformRegistries {
this.server = server;
}
@Override
public PlatformGenerationRegistry generationRegistry() {
MinecraftServer instance = server.get();
if (instance == null) {
throw new IllegalStateException("Minecraft server is not ready for generation registry capture.");
}
return new ModdedGenerationRegistry(
server,
"modded-generation-registry-v1",
"modded-generated-registry-json-v1|minecraft:" + BuildConstants.MINECRAFT_VERSION
);
}
@Override
public PlatformBlockState block(String key) {
return ModdedBlockResolution.get(key);
@@ -71,7 +71,7 @@ public final class ModdedRuntimeRegistry {
if (!seen.add(biomeId)) {
continue;
}
String biomeRef = ModdedWorldgenIds.biomeRef(pack, dimension.getLoadKey(), biomeId);
String biomeRef = data.customBiomeResourceKey(dimension, customBiome);
ResourceKey<Biome> biomeKey = ResourceKey.create(Registries.BIOME, Identifier.parse(biomeRef));
if (registry.get(biomeKey).isEmpty()) {
missing.add(biomeRef);
@@ -36,27 +36,31 @@ import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
final class ModdedSpawnTableMerger {
private final IrisModdedChunkGenerator generator;
private volatile SpawnTableState state = new SpawnTableState();
private final ConcurrentHashMap<SpawnBiomeKey, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables =
new ConcurrentHashMap<>();
private final Set<Integer> initializedRuntimeIdentities = ConcurrentHashMap.newKeySet();
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
this.generator = generator;
}
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
SpawnTableState active = state;
if (active.initialized) {
Engine current = generator.engineOrNull();
if (current == null) {
return;
}
synchronized (active) {
if (active != state || active.initialized) {
return;
}
Engine current = generator.engineOrNull();
if (current == null) {
int runtimeIdentity = current.getCacheID();
if (initializedRuntimeIdentities.contains(runtimeIdentity)) {
return;
}
synchronized (generator) {
if (initializedRuntimeIdentities.contains(runtimeIdentity)) {
return;
}
@@ -68,7 +72,7 @@ final class ModdedSpawnTableMerger {
hostDimension,
current.getData(),
hostDimension.getReachableBiomes(current),
active
runtimeIdentity
);
DimensionStackContext stackContext = current.getDimensionStackContext();
if (stackContext != null) {
@@ -82,11 +86,11 @@ final class ModdedSpawnTableMerger {
dimension,
terrainContext.getData(),
dimension.getReachableBiomes(terrainContext),
active
runtimeIdentity
);
}
}
active.initialized = true;
initializedRuntimeIdentities.add(runtimeIdentity);
}
}
}
@@ -96,7 +100,7 @@ final class ModdedSpawnTableMerger {
IrisDimension dimension,
IrisData data,
Iterable<IrisBiome> biomes,
SpawnTableState state
int runtimeIdentity
) {
for (IrisBiome irisBiome : biomes) {
if (irisBiome == null || !irisBiome.isCustom()) {
@@ -110,31 +114,43 @@ final class ModdedSpawnTableMerger {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder(
registry,
ModdedWorldgenIds.biomeRef(
data.getDataFolder().getName(),
dimension.getLoadKey(),
customBiome.getId()
)
data.customBiomeResourceKey(dimension, customBiome)
);
if (customHolder == null) {
continue;
}
state.vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
vanillaSpawnBiomes.putIfAbsent(
new SpawnBiomeKey(runtimeIdentity, customHolder.value()),
vanillaHolder
);
}
}
}
Holder<Biome> vanillaSpawnBiome(Biome biome) {
return state.vanillaSpawnBiomes.get(biome);
Engine current = generator.engineOrNull();
return current == null
? null
: vanillaSpawnBiomes.get(new SpawnBiomeKey(current.getCacheID(), biome));
}
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
Biome biome, MobCategory category,
Engine engine,
Biome biome,
MobCategory category,
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
SpawnTableKey key = new SpawnTableKey(biome, category);
return state.mergedSpawnTables.computeIfAbsent(
key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
SpawnTableKey key = new SpawnTableKey(engine.getCacheID(), biome, category);
return mergedSpawnTables.computeIfAbsent(
key,
ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns)
);
}
void evictRuntime(int runtimeIdentity) {
vanillaSpawnBiomes.keySet().removeIf(key -> key.runtimeIdentity() == runtimeIdentity);
mergedSpawnTables.keySet().removeIf(key -> key.runtimeIdentity() == runtimeIdentity);
initializedRuntimeIdentities.remove(runtimeIdentity);
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
@@ -150,16 +166,16 @@ final class ModdedSpawnTableMerger {
}
void resetVanillaSpawnBiomes() {
state = new SpawnTableState();
synchronized (generator) {
vanillaSpawnBiomes.clear();
mergedSpawnTables.clear();
initializedRuntimeIdentities.clear();
}
}
private record SpawnTableKey(Biome biome, MobCategory category) {
private record SpawnBiomeKey(int runtimeIdentity, Biome biome) {
}
private static final class SpawnTableState {
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables =
new ConcurrentHashMap<>();
private volatile boolean initialized;
private record SpawnTableKey(int runtimeIdentity, Biome biome, MobCategory category) {
}
}
@@ -223,7 +223,13 @@ public final class ModdedStartup {
index++;
long dimensionStartedAt = System.currentTimeMillis();
try {
ModdedDimensionManager.create(server, dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
ModdedDimensionManager.restorePersistent(
server,
dimension.id(),
dimension.pack(),
dimension.dimension(),
dimension.seed()
);
injected++;
ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms",
index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(),
@@ -18,21 +18,30 @@
package art.arcane.iris.modded;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.history.GenerationActivation;
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.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.modded.command.ModdedGuiHost;
import art.arcane.iris.spi.IrisPlatforms;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
@@ -45,13 +54,25 @@ public final class ModdedWorldEngines {
private ModdedWorldEngines() {
}
public static Engine get(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
public static Engine get(
ServerLevel level,
String pack,
String dimensionKey,
long seedOverride,
ModdedGenerationMode generationMode
) {
Engine existing = ENGINES.get(level);
if (existing != null) {
return existing;
}
return ENGINES.computeIfAbsent(level,
(ServerLevel l) -> createAndApplyWorldBoundary(l, pack, dimensionKey, seedOverride));
(ServerLevel l) -> createAndApplyWorldBoundary(
l,
pack,
dimensionKey,
seedOverride,
generationMode
));
}
public static Collection<Engine> activeEngines() {
@@ -81,8 +102,14 @@ public final class ModdedWorldEngines {
ModdedIrisLog.info("Iris engine evicted for {}", level.dimension().identifier());
}
static Engine prepareReplacement(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
return create(level, pack, dimensionKey, seedOverride);
static Engine prepareReplacement(
ServerLevel level,
String pack,
String dimensionKey,
long seedOverride,
ModdedGenerationMode generationMode
) {
return create(level, pack, dimensionKey, seedOverride, generationMode);
}
static void installReplacement(ServerLevel level, Engine replacement) {
@@ -103,9 +130,14 @@ public final class ModdedWorldEngines {
ModdedGuiHost.unbind(engine);
}
private static Engine createAndApplyWorldBoundary(ServerLevel level, String pack,
String dimensionKey, long seedOverride) {
Engine engine = create(level, pack, dimensionKey, seedOverride);
private static Engine createAndApplyWorldBoundary(
ServerLevel level,
String pack,
String dimensionKey,
long seedOverride,
ModdedGenerationMode generationMode
) {
Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode);
try {
engine.getPlatformHooks().applyWorldBoundary(engine);
return engine;
@@ -126,22 +158,178 @@ public final class ModdedWorldEngines {
}
}
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
private static Engine create(
ServerLevel level,
String pack,
String dimensionKey,
long seedOverride,
ModdedGenerationMode generationMode
) {
ModdedEngineBootstrap.bind();
File packDir = resolvePack(pack, dimensionKey);
PackValidationRegistry.requireLoadable(pack);
IrisData data = IrisData.openRuntime(packDir);
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
ModdedIrisLog.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
pack, packDir.getAbsolutePath(), dimensionKey, dimensionKey);
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + packDir.getAbsolutePath());
}
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
validateDimensionContract(pack, dimensionKey, dimension, level);
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
IrisWorld world = IrisWorld.builder()
ModdedGenerationMode requiredMode = Objects.requireNonNull(generationMode, "generationMode");
if (requiredMode == ModdedGenerationMode.TRANSIENT_STUDIO) {
return createTransientStudioEngine(level, pack, dimensionKey, seed);
}
ModdedGenerationHistoryStorage.ActivePack active = ModdedGenerationHistoryStorage.openOrAdopt(
level,
pack,
dimensionKey,
seed
);
try {
return createHistoryEngine(level, seed, active);
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history could not initialize runtime routing for '"
+ level.dimension().identifier() + "'.", failure);
}
}
private static Engine createHistoryEngine(
ServerLevel level,
long seed,
ModdedGenerationHistoryStorage.ActivePack opened
) throws IOException {
GenerationHistory history = opened.history();
long openedActivationId = history.activeActivation().activationId();
IrisEngine candidate = buildEngine(level, seed, opened);
boolean ready = false;
try {
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
history,
IrisBoundarySignatureSampler.INSTANCE,
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
);
router.preloadActiveRuntimes();
if (history.activeActivation().activationId() != openedActivationId) {
close(candidate);
ModdedGenerationHistoryStorage.ActivePack promoted =
ModdedGenerationHistoryStorage.resolveActive(history);
candidate = buildEngine(level, seed, promoted);
router = GenerationHistoryRuntimeRouter.attach(
candidate,
history,
IrisBoundarySignatureSampler.INSTANCE
);
router.preloadActiveRuntimes();
}
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
ready = true;
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
level.dimension().identifier(),
candidate.getData().getDataFolder().getAbsolutePath(),
candidate.getDimension().getLoadKey(),
seed,
candidate.getDimension().getMinHeight(),
candidate.getDimension().getMaxHeight(),
history.activeActivation().activationId());
return candidate;
} finally {
if (!ready) {
close(candidate);
}
}
}
private static IrisEngine buildEngine(
ServerLevel level,
long seed,
ModdedGenerationHistoryStorage.ActivePack active
) throws IOException {
File packDir = active.packRoot().toFile();
PackValidationRegistry.requireLoadable(active.packRoot());
IrisData data = IrisData.openRuntime(packDir);
boolean runtimeOwnsData = false;
try {
IrisDimension dimension = data.getDimensionLoader().load(active.dimensionKey());
if (dimension == null) {
ModdedIrisLog.error("Iris immutable generation pack at {} does not contain dimension '{}' (expected dimensions/{}.json).",
packDir.getAbsolutePath(), active.dimensionKey(), active.dimensionKey());
throw new IllegalStateException("Iris dimension '" + active.dimensionKey()
+ "' missing from immutable pack " + packDir.getAbsolutePath());
}
validateDimensionContract(active.dimensionContract(), dimension, level);
File worldFolder = DimensionType.getStorageFolder(
level.dimension(),
level.getServer().getWorldPath(LevelResource.ROOT)
).toFile();
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
GenerationHistory history = active.history();
GenerationActivation activation = history.activeActivation();
GenerationEpoch epoch = history.activeEpoch();
TransitionGenerationPlan transitionPlan = activation.isInitial()
? null
: history.transitionPlan(activation.activationId());
IrisEngine engine = new IrisEngine(
new EngineTarget(world, dimension, data),
IrisEngine.InitializationMode.RUNTIME,
history.paths().activationMantleRoot(activation.activationId()),
epoch.kernelVersion(),
transitionPlan);
runtimeOwnsData = true;
return engine;
} finally {
if (!runtimeOwnsData) {
data.close();
}
}
}
private static IrisEngine createTransientStudioEngine(
ServerLevel level,
String pack,
String dimensionKey,
long seed
) {
File packDir = resolvePack(pack, dimensionKey);
PackValidationRegistry.requireLoadable(packDir.toPath());
IrisData data = IrisData.openRuntime(packDir);
boolean runtimeOwnsData = false;
boolean ready = false;
IrisEngine engine = null;
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
if (dimension == null) {
throw new IllegalStateException("Transient Iris Studio pack does not contain dimension '"
+ dimensionKey + "'.");
}
File worldFolder = DimensionType.getStorageFolder(
level.dimension(),
level.getServer().getWorldPath(LevelResource.ROOT)
).toFile();
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
engine = new IrisEngine(
new EngineTarget(world, dimension, data),
IrisEngine.InitializationMode.STUDIO
);
runtimeOwnsData = true;
requireReady(engine, level, ModdedGenerationMode.TRANSIENT_STUDIO);
ready = true;
ModdedIrisLog.info("Iris transient Studio engine up for {}: pack={} dim={} seed={} height={}..{}",
level.dimension().identifier(),
packDir.getAbsolutePath(),
dimension.getLoadKey(),
seed,
dimension.getMinHeight(),
dimension.getMaxHeight());
return engine;
} finally {
if (!ready && engine != null) {
close(engine);
} else if (!runtimeOwnsData) {
data.close();
}
}
}
private static IrisWorld buildWorld(
ServerLevel level,
long seed,
IrisDimension dimension,
File worldFolder
) {
return IrisWorld.builder()
.platformIdentity(level.dimension().identifier().toString())
.name(level.dimension().identifier().toString().replace(':', '_'))
.seed(seed)
@@ -150,40 +338,52 @@ public final class ModdedWorldEngines {
.maxHeight(dimension.getMaxHeight())
.platformWorld(new ModdedPlatformWorld(level))
.build();
Engine engine = new IrisEngine(
new EngineTarget(world, dimension, data),
IrisEngine.InitializationMode.RUNTIME);
if (engine.isClosed() || engine.getComplex() == null) {
IllegalStateException failure = new IllegalStateException("Iris engine for "
+ level.dimension().identifier() + " did not initialize a ready biome complex");
try {
close(engine);
} catch (Throwable cleanupError) {
failure.addSuppressed(cleanupError);
}
throw failure;
}
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
return engine;
}
private static void validateDimensionContract(String pack, String dimensionKey,
IrisDimension dimension, ServerLevel level) {
private static void requireReady(
IrisEngine engine,
ServerLevel level,
ModdedGenerationMode generationMode
) {
if (engine.isClosed() || engine.getComplex() == null) {
throw new IllegalStateException("Iris engine for " + level.dimension().identifier()
+ " did not initialize a ready runtime");
}
if (generationMode.historyRequired()
&& engine.getGenerationHistoryRuntimeRouter().isEmpty()) {
throw new IllegalStateException("Persistent Iris engine for " + level.dimension().identifier()
+ " did not initialize a generation-history runtime router");
}
if (!generationMode.historyRequired()
&& (!engine.isStudio() || engine.getGenerationHistoryRuntimeRouter().isPresent())) {
throw new IllegalStateException("Transient Iris Studio engine for "
+ level.dimension().identifier() + " has an invalid history mode");
}
}
private static void validateDimensionContract(
GenerationEpoch.DimensionContract recorded,
IrisDimension dimension,
ServerLevel level
) {
GenerationEpoch.DimensionContract loaded = GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
recorded.dimensionTypeKey()
);
if (!recorded.equals(loaded)) {
throw new IllegalStateException("Immutable Iris dimension contract changed for '"
+ level.dimension().identifier() + "'.");
}
DimensionType actualType = level.dimensionType();
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
.map(key -> key.identifier().toString())
.orElse("<unregistered>");
String legacyTypeKey = "irisworldgen:" + dimension.getDimensionTypeKey();
String expectedTypeKey = legacyTypeKey.equals(actualTypeKey)
? legacyTypeKey
: ModdedWorldgenIds.dimensionTypeRef(pack, dimensionKey);
IrisDimensionRuntimeContract expected = new IrisDimensionRuntimeContract(
expectedTypeKey,
dimension.getMinHeight(),
dimension.getMaxHeight() - dimension.getMinHeight(),
dimension.getLogicalHeight());
recorded.dimensionTypeKey(),
recorded.minHeight(),
recorded.height(),
recorded.logicalHeight());
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
actualTypeKey,
actualType.minY(),
@@ -3,7 +3,9 @@ package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
public final class ModdedWorldgenIds {
private static final String NAMESPACE = "irisworldgen";
@@ -29,8 +31,33 @@ public final class ModdedWorldgenIds {
}
public static String biomeRef(Engine engine, String biome) {
return biomeRef(engine.getData().getDataFolder().getName(),
engine.getDimension().getLoadKey(), biome);
return engine.getData().customBiomeResourceKey(engine.getDimension(), biome);
}
public static List<String> legacyBiomeRefs(String pack, String dimension, String biome) {
String scoped = biomeRef(pack, dimension, biome);
String unscoped = dimension.toLowerCase(Locale.ROOT) + ":" + biome.toLowerCase(Locale.ROOT);
return scoped.equals(unscoped) ? List.of(scoped) : List.of(scoped, unscoped);
}
static Optional<ScopedDimension> scopedDimensionType(String dimensionTypeRef) {
String prefix = NAMESPACE + ":";
if (dimensionTypeRef == null || !dimensionTypeRef.startsWith(prefix)) {
return Optional.empty();
}
String[] parts = dimensionTypeRef.substring(prefix.length()).split("/");
if (parts.length != 5
|| !"packs".equals(parts[0])
|| !"dimensions".equals(parts[2])
|| !"dimension_type".equals(parts[4])) {
return Optional.empty();
}
String pack = decode(parts[1]);
String dimension = decode(parts[3]);
if (pack == null || dimension == null) {
return Optional.empty();
}
return Optional.of(new ScopedDimension(pack, dimension));
}
public static String displayName(String presetPath) {
@@ -100,4 +127,7 @@ public final class ModdedWorldgenIds {
}
return Character.toUpperCase(normalized.charAt(0)) + normalized.substring(1);
}
record ScopedDimension(String pack, String dimension) {
}
}
@@ -26,6 +26,7 @@ import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
@@ -109,7 +110,7 @@ final class ModdedCommandSuggestions {
}
static Set<String> reachableBiomeKeys(Engine engine) {
return reachableBiomeKeys(engine.getAllBiomes());
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
}
static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) {
@@ -123,7 +124,7 @@ final class ModdedCommandSuggestions {
}
static boolean isReachableBiome(Engine engine, String biomeKey) {
return isReachableBiome(engine.getAllBiomes(), biomeKey);
return GenerationFindCatalog.biome(engine, biomeKey) != null;
}
static boolean isReachableBiome(Iterable<IrisBiome> biomes, String biomeKey) {
@@ -135,7 +136,8 @@ final class ModdedCommandSuggestions {
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder);
return SharedSuggestionProvider.suggest(GenerationFindCatalog.regions(engine).stream()
.map(region -> region.getLoadKey()), builder);
}
} catch (Throwable e) {
warnTabFailure("region keys", context.getSource(), e);
@@ -148,7 +150,7 @@ final class ModdedCommandSuggestions {
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
return SharedSuggestionProvider.suggest(GenerationFindCatalog.objectKeys(engine), builder);
}
} catch (Throwable e) {
warnTabFailure("object keys", context.getSource(), e);
@@ -191,6 +193,7 @@ final class ModdedCommandSuggestions {
Collection<String> unregisteredIrisKeys = eligibleUnregisteredEditableKeys(
irisKeys, registeredKeys,
(String candidate) -> IrisStructureLocator.hasNativePlacement(engine, candidate));
nativeKeys.addAll(GenerationFindCatalog.retainedStructureKeys(engine));
return SharedSuggestionProvider.suggest(
combineStructureKeys(unregisteredIrisKeys, nativeKeys), builder);
} catch (Throwable e) {
@@ -29,8 +29,9 @@ import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.history.GenerationSemanticQueries;
import art.arcane.iris.engine.history.GenerationFindCatalog;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -86,15 +87,11 @@ final class ModdedLocateCommands {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8));
return 0;
}
IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim());
IrisBiome biome = GenerationFindCatalog.biome(engine, key);
if (biome == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
return 0;
}
if (!ModdedCommandSuggestions.isReachableBiome(engine, biome.getLoadKey())) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", biome.getLoadKey())));
return 0;
}
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
return 1;
}
@@ -111,15 +108,11 @@ final class ModdedLocateCommands {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9));
return 0;
}
IrisRegion region = engine.getData().getRegionLoader().load(key.trim());
IrisRegion region = GenerationFindCatalog.region(engine, key);
if (region == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key)));
return 0;
}
if (!engine.getDimension().getRegions().contains(region.getLoadKey())) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey())));
return 0;
}
locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
return 1;
}
@@ -132,7 +125,7 @@ final class ModdedLocateCommands {
return 0;
}
String key = keyRaw.trim();
if (!engine.hasObjectPlacement(key)) {
if (!GenerationFindCatalog.hasObjectPlacement(engine, key)) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
return 0;
}
@@ -158,10 +151,6 @@ final class ModdedLocateCommands {
return 0;
}
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
if (runtime == null) {
IrisModdedCommands.fail(source, "Hydrology is not active in this dimension.");
return 0;
}
HydrologyFeatureQuery query;
try {
query = HydrologyFeatureQuery.parse(type);
@@ -171,15 +160,19 @@ final class ModdedLocateCommands {
}
int originX = player.blockPosition().getX();
int originZ = player.blockPosition().getZ();
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
int requestedDistance = runtime == null
? 8192
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime == null
? requestedDistance
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
MinecraftServer server = source.getServer();
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
MultiBurst.burst.completeValueAsync(() -> runtime.nearestFeature(
query.types(), query.profileKey(), originX, originZ, maximumDistance,
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
engine, query, originX, originZ, maximumDistance,
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
.whenComplete((HydrologyFeatureRef feature, Throwable error) -> server.execute(() -> {
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
if (error != null) {
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
IrisModdedCommands.fail(source,
@@ -192,13 +185,12 @@ final class ModdedLocateCommands {
+ maximumDistance + " blocks.");
return;
}
int worldY = feature.y() + engine.getDimension().getMinHeight() + 2;
teleportToStructure(
source,
level,
player,
feature.x(),
worldY,
feature.y() + 2,
feature.z(),
type + " hydrology feature"
);
@@ -225,8 +217,9 @@ final class ModdedLocateCommands {
}
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
if (resolved.isEmpty()) {
if (!IrisStructureLocator.hasNativePlacement(engine, key)
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key)) {
if ((!IrisStructureLocator.hasNativePlacement(engine, key)
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key))
|| GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
@@ -247,6 +240,10 @@ final class ModdedLocateCommands {
if (!ModdedCommandSuggestions.isEligibleRegisteredStructure(
decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement,
reachable, nativeGenerationEnabled)) {
if (GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
IrisModdedCommands.fail(source, registeredStructureUnavailableMessage(
target.key(), target.availability(), decision,
nativePlacement, locatableNativePlacement,
@@ -312,7 +309,7 @@ final class ModdedLocateCommands {
Thread thread = new Thread(() -> {
try {
IrisStructureLocator.LocateResult result =
IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
GenerationSemanticQueries.nearestStructure(engine, key, blockX, blockZ, 1024);
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key))));
return;
@@ -491,7 +491,7 @@ public final class ModdedStudioCommands {
ModdedDimensionManager.Handle handle = null;
try {
replaceExistingStudio(server, owner, dimensionId);
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
handle = ModdedDimensionManager.createTransientStudio(server, dimensionId, pack, pack, seed);
STUDIOS.put(owner, dimensionId);
if (owner.equals(CONSOLE_OWNER)) {
completeConsoleOpen(source, dimensionId, pack, seed, handle);
@@ -78,6 +78,7 @@ public final class ModdedWorldCommands {
root.then(enableTree("enable"));
root.then(enableTree("create"));
root.then(updateTree());
root.then(replaceOverworldTree());
root.then(mainWorldTree());
@@ -110,6 +111,20 @@ public final class ModdedWorldCommands {
.executes(ModdedCommandTree.localized((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), false))));
}
private static LiteralArgumentBuilder<CommandSourceStack> updateTree() {
return Commands.literal("update")
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
.then(Commands.argument("pack", StringArgumentType.string())
.suggests(IrisModdedCommands.PACK_NAMES)
.executes(ModdedCommandTree.localized(
(CommandContext<CommandSourceStack> context) -> update(
context.getSource(),
dimensionArgument(context),
StringArgumentType.getString(context, "pack")
)
))));
}
private static LiteralArgumentBuilder<CommandSourceStack> deleteTree(String name) {
return Commands.literal(name)
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
@@ -197,6 +212,47 @@ public final class ModdedWorldCommands {
return 1;
}
private static int update(CommandSourceStack source, String targetDimension, String packRaw) {
String dimensionId;
try {
dimensionId = normalizeDimensionId(targetDimension);
} catch (IllegalArgumentException failure) {
IrisModdedCommands.fail(source, failure.getMessage());
return 0;
}
String[] packRef = parsePackRef(packRaw);
String pack = packRef[0];
String packDimension = packRef[1];
if (!validPackRef(source, pack, packDimension)
|| blockIfPackBroken(source, dimensionId, pack)
|| !loadPackDimension(source, pack, packDimension)) {
return 0;
}
try {
ModdedDimensionManager.UpdateResult result = ModdedDimensionManager.stagePersistentUpdate(
source.getServer(),
dimensionId,
pack,
packDimension
);
if (result.restartRequired()) {
IrisModdedCommands.ok(source, "Iris world update staged for " + dimensionId
+ " from " + pack + ":" + packDimension
+ ". Restart the server to activate it; the running world remains on its current pack.");
} else {
IrisModdedCommands.ok(source, "Iris world " + dimensionId
+ " already uses this immutable pack; no restart is required.");
}
return 1;
} catch (Throwable failure) {
ModdedIrisLog.error("Iris world update staging failed for {} (pack={} dim={})",
dimensionId, pack, packDimension, failure);
IrisModdedCommands.fail(source, "Iris world update failed for " + dimensionId
+ ": " + IrisLanguage.errorDetail(failure));
return 0;
}
}
private static int replaceOverworld(CommandSourceStack source, String packRaw, String seedRaw) {
MinecraftServer server = source.getServer();
Long seed = parseSeed(source, seedRaw);
@@ -18,6 +18,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
@@ -267,10 +268,11 @@ public class IrisModdedStructureParityTest {
.setInferredType(InferredType.SEA);
Set<String> keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
List.of(ocean, custom, shore, unsafeSea), "OverWorld");
List.of(ocean, custom, shore, unsafeSea),
customBiome -> "iris:biomes/" + customBiome.getId().toLowerCase(Locale.ROOT));
assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach",
"minecraft:the_void", "overworld:aurora"), keys);
"minecraft:the_void", "iris:biomes/aurora"), keys);
assertFalse(keys.contains("minecraft:desert"));
assertFalse(keys.contains("minecraft:plains"));
@@ -52,6 +52,19 @@ public class ModdedForcedDatapackTest {
assertEquals("iris:overworld", ModdedWorldgenIds.generatorIdentity("overworld"));
assertEquals("iris:other/overworld",
ModdedWorldgenIds.generatorIdentity("other:overworld"));
assertEquals(
new ModdedWorldgenIds.ScopedDimension("overworld", "overworld"),
ModdedWorldgenIds.scopedDimensionType(
ModdedWorldgenIds.dimensionTypeRef("overworld", "overworld")
).orElseThrow()
);
assertEquals(
List.of(
ModdedWorldgenIds.biomeRef("overworld", "overworld", "mist"),
"overworld:mist"
),
ModdedWorldgenIds.legacyBiomeRefs("overworld", "overworld", "mist")
);
}
@Test
@@ -148,6 +161,24 @@ public class ModdedForcedDatapackTest {
}
}
@Test
public void rejectsConflictingRegistryDefinitionsDuringMerge() throws IOException {
Path root = Files.createTempDirectory("iris-forced-pack-conflict");
try {
Path source = Files.createDirectories(root.resolve("source/data/iris/worldgen/biome"));
Path destination = Files.createDirectories(root.resolve("destination/data/iris/worldgen/biome"));
Files.writeString(source.resolve("shared.json"), "{\"temperature\":1}", StandardCharsets.UTF_8);
Files.writeString(destination.resolve("shared.json"), "{\"temperature\":2}", StandardCharsets.UTF_8);
assertThrows(
IOException.class,
() -> ModdedForcedDatapack.mergeDirectory(root.resolve("source"), root.resolve("destination"))
);
} finally {
deleteTree(root);
}
}
private List<String> directoryEntries(Path root) throws IOException {
try (Stream<Path> entries = Files.list(root)) {
return entries.map((Path path) -> path.getFileName().toString())
@@ -0,0 +1,180 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ModdedGenerationHistoryRoutingContractTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test
public void startupPublishesOnlyAnImmutableHistoryBoundRuntime() throws IOException {
String source = source("ModdedWorldEngines.java");
String create = method(source, "private static Engine create(");
String createHistoryEngine = method(source, "private static Engine createHistoryEngine(");
String buildEngine = method(source, "private static IrisEngine buildEngine(");
assertTrue(create.contains("ModdedGenerationHistoryStorage.openOrAdopt("));
assertFalse(create.contains("resolvePack("));
assertTrue(createHistoryEngine.contains("candidate.attachGenerationHistory("));
assertTrue(createHistoryEngine.contains("IrisBoundarySignatureSampler.INSTANCE"));
assertTrue(createHistoryEngine.contains("getGenerationTransitionWidthBlocks()"));
assertTrue(createHistoryEngine.contains("router.preloadActiveRuntimes();"));
assertBefore(createHistoryEngine, "close(candidate);", "ModdedGenerationHistoryStorage.resolveActive(history)");
assertTrue(createHistoryEngine.contains("GenerationHistoryRuntimeRouter.attach("));
assertTrue(buildEngine.contains("active.packRoot().toFile()"));
assertTrue(buildEngine.contains("history.paths().activationMantleRoot(activation.activationId())"));
assertTrue(buildEngine.contains("epoch.kernelVersion()"));
assertTrue(buildEngine.contains("transitionPlan"));
}
@Test
public void onlyExplicitTransientStudioEnginesBypassHistory() throws IOException {
String manager = source("ModdedDimensionManager.java");
String transientCreate = method(manager, "public static Handle createTransientStudio(");
String persistentCreate = method(manager, "public static Handle createPersistent(");
String engines = source("ModdedWorldEngines.java");
String studio = method(engines, "private static IrisEngine createTransientStudioEngine(");
String generator = source("IrisModdedChunkGenerator.java");
String bypass = method(generator, "boolean allowsGenerationHistoryBypass(");
String route = method(generator, "private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(");
assertTrue(transientCreate.contains("ModdedGenerationMode.TRANSIENT_STUDIO"));
assertTrue(persistentCreate.contains("ModdedGenerationMode.PERSISTENT_CREATE"));
assertTrue(studio.contains("IrisData.openRuntime(packDir)"));
assertTrue(studio.contains("IrisEngine.InitializationMode.STUDIO"));
assertFalse(studio.contains("ModdedGenerationHistoryStorage"));
assertFalse(studio.contains("attachGenerationHistory("));
assertTrue(bypass.contains("generationMode != ModdedGenerationMode.TRANSIENT_STUDIO"));
assertTrue(bypass.contains("irisEngine.isStudio()"));
assertTrue(bypass.contains("Only an explicitly transient Iris Studio engine"));
assertBefore(route, "allowsGenerationHistoryBypass(current)", "requireHistoryRouter(current, operation)");
}
@Test
public void everyCoordinateBearingChunkStageOpensItsRoutedRuntime() throws IOException {
String source = source("IrisModdedChunkGenerator.java");
assertRuntimeRoute(source, "public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(");
assertCoordinateRoute(source, "public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(");
assertRuntimeRoute(source, "public CompletableFuture<ChunkAccess> createBiomes(");
assertRuntimeRoute(source, "public void applyCarvers(");
assertRuntimeRoute(source, "public void buildSurface(");
assertRuntimeRoute(source, "public void applyBiomeDecoration(");
assertRuntimeRoute(source, "public void createStructures(");
assertRuntimeRoute(source, "public void createReferences(");
assertRuntimeRoute(source, "public void spawnOriginalMobs(");
assertCoordinateRoute(source, "public int getBaseHeight(");
assertCoordinateRoute(source, "public NoiseColumn getBaseColumn(");
}
@Test
public void asyncTerrainKeepsTheRouteUntilDurableSemanticClaim() throws IOException {
String source = source("IrisModdedChunkGenerator.java");
String fill = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise(");
String terrain = method(source, "private ChunkAccess generateTerrain(");
String completion = method(source, "private static CompletableFuture<ChunkAccess> closeRouteOnCompletion(");
assertBefore(fill, "openHistoryRoute(", "CompletableFuture.supplyAsync(");
assertTrue(fill.contains("generateTerrain(chunk, generationEngine, pos, air, route)"));
assertTrue(fill.contains("return closeRouteOnCompletion(pipeline, route);"));
assertTrue(terrain.contains("openHistoryRuntimeScope(route)"));
assertBefore(terrain, "generationEngine.generate(", "route.claimGeneratedSemantics();");
assertBefore(terrain, "route.claimGeneratedSemantics();", "return chunk;");
assertTrue(completion.contains("pipeline.whenComplete("));
assertTrue(completion.contains("route.close();"));
}
@Test
public void biomeQueriesRouteEachCoordinateAndKeepRuntimeSpecificCaches() throws IOException {
String source = source("IrisModdedBiomeSource.java");
String structure = method(source, "public Holder<Biome> getNoiseBiome(");
String visible = method(source, "Holder<Biome> getVisibleNoiseBiome(");
String batch = method(source, "public Set<Holder<Biome>> getBiomesWithin(");
String resolution = method(source, "private Holder<Biome> resolveVisibleBiome(");
assertTrue(structure.contains("openHistoryCoordinateScope("));
assertTrue(visible.contains("openHistoryCoordinateScope("));
assertTrue(batch.contains("quartX << 2, quartZ << 2"));
assertTrue(source.contains("int runtimeIdentity = engine.getCacheID();"));
assertTrue(source.contains("cache.get(runtimeIdentity, key)"));
assertTrue(source.contains("cache.put(runtimeIdentity, key, resolved)"));
assertBefore(resolution, "historicalPhysicalBiomeKeyAt(", "ModdedWorldgenIds.biomeRef(engine");
}
@Test
public void discreteStagesResumeIncompleteHistoricalChunksAndRejectTransitionOwnership() throws IOException {
String generator = source("IrisModdedChunkGenerator.java");
String discrete = method(generator, "private boolean allowsRoutedDiscreteGeneration(");
String decoration = method(generator, "public void applyBiomeDecoration(");
String structures = method(generator, "public void createStructures(");
String mobs = method(generator, "public void spawnOriginalMobs(");
String nativeStage = source("ModdedNativeStructureStage.java");
String adjust = method(nativeStage, "void adjustGeneratedStructures(");
String place = method(nativeStage, "void placeVanillaStructures(");
assertTrue(discrete.contains("chunk.getPersistedStatus().isOrAfter(stage)"));
assertFalse(discrete.contains("router.history().activeActivation().activationId()"));
assertFalse(discrete.contains("return true;"));
assertTrue(discrete.contains("allowsNewGenerationChunk("));
assertTrue(decoration.contains("ChunkStatus.FEATURES"));
assertTrue(structures.contains("ChunkStatus.STRUCTURE_STARTS"));
assertTrue(mobs.contains("ChunkStatus.SPAWN"));
assertBefore(decoration, "allowsRoutedDiscreteGeneration(", "nativeStructures.placeVanillaStructures(");
assertBefore(structures, "allowsRoutedDiscreteGeneration(", "super.createStructures(");
assertBefore(mobs, "allowsRoutedDiscreteGeneration(", "NaturalSpawner.spawnMobsForChunkGeneration(");
assertBefore(adjust, "allowsNewGenerationFootprint(", "recordWorldCheckStructureShift(");
assertTrue(place.contains("allowsNewGenerationFootprint("));
}
private static void assertRuntimeRoute(String source, String signature) {
String body = method(source, signature);
assertTrue(signature + " must open a generation-history route", body.contains("openHistoryRoute("));
assertTrue(signature + " must open its routed runtime", body.contains("openHistoryRuntimeScope(route)"));
}
private static void assertCoordinateRoute(String source, String signature) {
String body = method(source, signature);
assertTrue(signature + " must route its coordinate", body.contains("openHistoryCoordinateScope("));
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
}
private static String source(String fileName) throws IOException {
String sourceRoot = System.getProperty(SOURCE_ROOT_PROPERTY);
assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY,
sourceRoot != null && !sourceRoot.isBlank());
return Files.readString(Path.of(sourceRoot, "art", "arcane", "iris", "modded", fileName));
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -0,0 +1,362 @@
package art.arcane.iris.modded;
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.GenerationRegistryContract;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedGenerationHistoryStorageTest {
private static final String DIMENSION_FINGERPRINT = "11".repeat(32);
private static final String DEFINITION_FINGERPRINT = "22".repeat(32);
private static final int TRANSITION_WIDTH_BLOCKS = 256;
@Test
public void newDimensionPublishesAnImmutableWorldLocalPack() throws IOException {
Path root = Files.createTempDirectory("iris-modded-history-create");
try {
Path source = pack(root.resolve("global-pack"), "first");
ModdedGenerationHistoryStorage.Candidate candidate = candidate(source, registryContract());
GenerationHistory history = ModdedGenerationHistoryStorage.createOrStage(
root.resolve("world-dimension"),
source,
41L,
candidate,
ModdedGenerationHistoryStorageTest::union,
TRANSITION_WIDTH_BLOCKS
);
Path activePack = history.activePackRoot();
assertEquals("first", Files.readString(activePack.resolve("content.txt"), StandardCharsets.UTF_8));
Files.writeString(source.resolve("content.txt"), "mutated", StandardCharsets.UTF_8);
assertEquals("first", Files.readString(activePack.resolve("content.txt"), StandardCharsets.UTF_8));
assertEquals(candidate.packFingerprint(), history.activeEpoch().packFingerprint());
} finally {
deleteTree(root);
}
}
@Test
public void existingDimensionAdoptsItsSelectedPackOnlyOnce() throws IOException {
Path root = Files.createTempDirectory("iris-modded-history-adopt");
try {
Path source = pack(root.resolve("global-pack"), "adopted");
Path dimensionRoot = root.resolve("world-dimension");
AtomicInteger sourceReads = new AtomicInteger();
ModdedGenerationHistoryStorage.AdoptionSourceSupplier supplier = () -> {
sourceReads.incrementAndGet();
return new ModdedGenerationHistoryStorage.AdoptionSource(
source,
candidate(source, registryContract())
);
};
GenerationHistory adopted = ModdedGenerationHistoryStorage.restoreOrAdopt(
dimensionRoot,
91L,
supplier,
ModdedGenerationHistoryStorageTest::union
);
String epochId = adopted.activeEpoch().epochId();
Files.writeString(source.resolve("content.txt"), "changed", StandardCharsets.UTF_8);
GenerationHistory restored = ModdedGenerationHistoryStorage.restoreOrAdopt(
dimensionRoot,
91L,
supplier,
ModdedGenerationHistoryStorageTest::union
);
assertEquals(1, sourceReads.get());
assertEquals(epochId, restored.activeEpoch().epochId());
assertEquals("adopted", Files.readString(
restored.activePackRoot().resolve("content.txt"),
StandardCharsets.UTF_8
));
} finally {
deleteTree(root);
}
}
@Test
public void selectedUpdateStagesWithoutReplacingTheActivePack() throws IOException {
Path root = Files.createTempDirectory("iris-modded-history-stage");
try {
Path dimensionRoot = root.resolve("world-dimension");
Path first = pack(root.resolve("first-pack"), "first");
GenerationHistory created = ModdedGenerationHistoryStorage.createOrStage(
dimensionRoot,
first,
5L,
candidate(first, registryContract()),
ModdedGenerationHistoryStorageTest::union,
TRANSITION_WIDTH_BLOCKS
);
String activeEpoch = created.activeEpoch().epochId();
Path second = pack(root.resolve("second-pack"), "second");
GenerationHistory staged = ModdedGenerationHistoryStorage.createOrStage(
dimensionRoot,
second,
5L,
candidate(second, registryContract()),
ModdedGenerationHistoryStorageTest::union,
TRANSITION_WIDTH_BLOCKS
);
assertEquals(activeEpoch, staged.activeEpoch().epochId());
assertTrue(staged.pendingActivation().isPresent());
assertNotEquals(activeEpoch, staged.pendingActivation().orElseThrow().epochId());
assertEquals("first", Files.readString(
staged.activePackRoot().resolve("content.txt"),
StandardCharsets.UTF_8
));
assertEquals("second", Files.readString(
staged.packRoot(staged.pendingActivation().orElseThrow().activationId()).resolve("content.txt"),
StandardCharsets.UTF_8
));
} finally {
deleteTree(root);
}
}
@Test
public void unavailableUpdateRegistryDoesNotCreatePendingActivation() throws IOException {
Path root = Files.createTempDirectory("iris-modded-history-stage-registry");
try {
Path dimensionRoot = root.resolve("world-dimension");
Path first = pack(root.resolve("first-pack"), "first");
GenerationHistory history = ModdedGenerationHistoryStorage.createOrStage(
dimensionRoot,
first,
63L,
candidate(first, registryContract()),
ModdedGenerationHistoryStorageTest::union,
TRANSITION_WIDTH_BLOCKS
);
Path second = pack(root.resolve("second-pack"), "second");
assertThrows(IOException.class, () -> ModdedGenerationHistoryStorage.createOrStage(
dimensionRoot,
second,
63L,
candidate(second, registryContract()),
contracts -> GenerationRegistryContract.empty(),
TRANSITION_WIDTH_BLOCKS
));
assertTrue(history.pendingActivation().isEmpty());
assertEquals("first", Files.readString(
history.activePackRoot().resolve("content.txt"),
StandardCharsets.UTF_8
));
} finally {
deleteTree(root);
}
}
@Test
public void updateWithANewCustomBiomeKeyStagesBeforeThatKeyIsRegistered() throws IOException {
Path root = Files.createTempDirectory("iris-modded-history-new-biome");
try {
Path dimensionRoot = root.resolve("world-dimension");
Path first = pack(root.resolve("first-pack"), "first");
GenerationRegistryContract activeContract = registryContract("minecraft:plains");
ModdedGenerationHistoryStorage.createOrStage(
dimensionRoot,
first,
71L,
candidate(first, activeContract),
ModdedGenerationHistoryStorageTest::union,
TRANSITION_WIDTH_BLOCKS
);
Path second = pack(root.resolve("second-pack"), "second");
GenerationRegistryContract candidateContract = registryContract(
"iris:biomes/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
);
AtomicInteger validationCalls = new AtomicInteger();
GenerationHistory staged = ModdedGenerationHistoryStorage.createOrStage(
dimensionRoot,
second,
71L,
candidate(second, candidateContract),
contracts -> {
validationCalls.incrementAndGet();
assertEquals(1, contracts.size());
assertEquals(activeContract, contracts.iterator().next());
return union(contracts);
},
TRANSITION_WIDTH_BLOCKS
);
assertEquals(1, validationCalls.get());
assertTrue(staged.pendingActivation().isPresent());
assertEquals(
candidateContract,
staged.manifest()
.epoch(staged.pendingActivation().orElseThrow().epochId())
.orElseThrow()
.registryContract()
);
} finally {
deleteTree(root);
}
}
@Test
public void restoreRejectsASeedMismatchBeforeConsultingTheGlobalPack() throws IOException {
Path root = Files.createTempDirectory("iris-modded-history-seed");
try {
Path source = pack(root.resolve("global-pack"), "seeded");
Path dimensionRoot = root.resolve("world-dimension");
ModdedGenerationHistoryStorage.createOrStage(
dimensionRoot,
source,
123L,
candidate(source, registryContract()),
ModdedGenerationHistoryStorageTest::union,
TRANSITION_WIDTH_BLOCKS
);
AtomicInteger sourceReads = new AtomicInteger();
assertThrows(IOException.class, () -> ModdedGenerationHistoryStorage.restoreOrAdopt(
dimensionRoot,
124L,
() -> {
sourceReads.incrementAndGet();
return new ModdedGenerationHistoryStorage.AdoptionSource(
source,
candidate(source, registryContract())
);
},
ModdedGenerationHistoryStorageTest::union
));
assertEquals(0, sourceReads.get());
} finally {
deleteTree(root);
}
}
@Test
public void restoreRejectsUnavailableRetainedRegistryDefinitions() throws IOException {
Path root = Files.createTempDirectory("iris-modded-history-registry");
try {
Path source = pack(root.resolve("global-pack"), "registry");
Path dimensionRoot = root.resolve("world-dimension");
ModdedGenerationHistoryStorage.createOrStage(
dimensionRoot,
source,
12L,
candidate(source, registryContract()),
ModdedGenerationHistoryStorageTest::union,
TRANSITION_WIDTH_BLOCKS
);
assertThrows(IOException.class, () -> ModdedGenerationHistoryStorage.restoreOrAdopt(
dimensionRoot,
12L,
() -> {
throw new AssertionError("Existing history consulted the global pack");
},
contracts -> GenerationRegistryContract.empty()
));
} finally {
deleteTree(root);
}
}
private static ModdedGenerationHistoryStorage.Candidate candidate(
Path source,
GenerationRegistryContract registryContract
) throws IOException {
return new ModdedGenerationHistoryStorage.Candidate(
GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION),
dimensionContract(),
registryContract
);
}
private static GenerationEpoch.DimensionContract dimensionContract() {
return new GenerationEpoch.DimensionContract(
"surface",
"irisworldgen:packs/7061636b/dimensions/73757266616365/dimension_type",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
DIMENSION_FINGERPRINT
);
}
private static GenerationRegistryContract registryContract() {
return registryContract("minecraft:stone");
}
private static GenerationRegistryContract registryContract(String resourceKey) {
return GenerationRegistryContract.fromDefinitions(Map.of(
new GenerationRegistryContract.PhysicalResourceKey(
resourceKey.startsWith("iris:biomes/")
? "minecraft:worldgen/biome"
: "minecraft:block",
resourceKey
),
DEFINITION_FINGERPRINT
));
}
private static GenerationRegistryContract union(Collection<GenerationRegistryContract> contracts)
throws IOException {
TreeMap<GenerationRegistryContract.PhysicalResourceKey, String> definitions = new TreeMap<>();
for (GenerationRegistryContract contract : contracts) {
for (Map.Entry<GenerationRegistryContract.PhysicalResourceKey, String> entry
: contract.definitions().entrySet()) {
String previous = definitions.putIfAbsent(entry.getKey(), entry.getValue());
if (previous != null && !previous.equals(entry.getValue())) {
throw new IOException("Conflicting registry definition");
}
}
}
return GenerationRegistryContract.fromDefinitions(definitions);
}
private static Path pack(Path root, String content) throws IOException {
Files.createDirectories(root);
Files.writeString(root.resolve("content.txt"), content, StandardCharsets.UTF_8);
return root;
}
private static void deleteTree(Path root) throws IOException {
if (!Files.exists(root)) {
return;
}
try (Stream<Path> walk = Files.walk(root)) {
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
Files.deleteIfExists(path);
}
}
}
}
@@ -26,7 +26,9 @@ public class ModdedGenerationLeaseContractTest {
assertTrue(source.contains("catch (GenerationSessionException e)"));
assertTrue(source.contains("e.isExpectedTeardown()"));
assertTrue(source.contains("Iris structure biome key lookup was rejected during an engine transition"));
assertTrue(source.contains("source.clearCache()"));
assertTrue(source.contains("int runtimeIdentity = engine.getCacheID();"));
assertTrue(source.contains("cache.get(runtimeIdentity, key)"));
assertTrue(source.contains("cache.put(runtimeIdentity, key, resolved)"));
}
@Test
@@ -203,7 +205,7 @@ public class ModdedGenerationLeaseContractTest {
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
String start = method(jobSource, "public static boolean start(");
int taskConstruction = start.indexOf("PregenTask task = PregenTask.builder()");
int profileMutation = start.indexOf("PregenPerformanceProfile.apply(engine);");
int profileMutation = start.indexOf("PregenPerformanceProfile.apply(engine)");
assertTrue(taskConstruction >= 0);
assertTrue(profileMutation > taskConstruction);
@@ -14,15 +14,16 @@ public class ModdedLifecycleFailureContractTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test
public void existingDimensionRepointBindsBeforeHandlePublication() throws IOException {
public void existingDimensionStagesWithoutReplacingTheLiveEngine() throws IOException {
String managerSource = source("ModdedDimensionManager.java");
String create = method(managerSource, "public static Handle create(");
String create = method(managerSource, "private static Handle create(");
int existingStart = requiredIndex(create, "if (existing != null");
int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1);
int injectionStart = requiredIndex(create, "try {", presentStart);
assertRepointBeforePublication(create.substring(existingStart, presentStart));
assertRepointBeforePublication(create.substring(presentStart, injectionStart));
assertFalse(create.substring(existingStart, presentStart).contains("repointAndBind("));
assertFalse(create.substring(presentStart, injectionStart).contains("repointAndBind("));
assertBefore(create, "ModdedGenerationHistoryStorage.createOrStage(", "if (existing != null");
String generatorSource = source("IrisModdedChunkGenerator.java");
String repointAndBind = method(generatorSource, "void repointAndBind(");
@@ -40,6 +41,31 @@ public class ModdedLifecycleFailureContractTest {
assertFalse(installReplacement.contains("catch ("));
}
@Test
public void persistentPackUpdatesUseAnExplicitRestartBoundPath() throws IOException {
String managerSource = source("ModdedDimensionManager.java");
String createPersistent = method(managerSource, "public static Handle createPersistent(");
String update = method(managerSource, "public static UpdateResult stagePersistentUpdate(");
assertBefore(createPersistent, "if (previous != null)", "ModdedDimensionRegistryStore.put(");
assertTrue(createPersistent.contains("Use the Iris world update command"));
assertTrue(update.contains("ModdedGenerationHistoryStorage.createOrStage("));
assertTrue(update.contains("active.history().pendingActivation()"));
assertTrue(update.contains("restartRequired()"));
assertFalse(update.contains("inject("));
assertFalse(update.contains("repointAndBind("));
assertBefore(update, "ModdedDimensionRegistryStore.put(server, updated);",
"ModdedGenerationHistoryStorage.createOrStage(");
assertTrue(update.contains("ModdedDimensionRegistryStore.put(server, previous);"));
String commandsSource = source("command/ModdedWorldCommands.java");
assertTrue(commandsSource.contains("root.then(updateTree());"));
String command = method(commandsSource, "private static int update(");
assertTrue(command.contains("ModdedDimensionManager.stagePersistentUpdate("));
assertTrue(command.contains("Restart the server to activate it"));
assertTrue(command.contains("running world remains on its current pack"));
}
@Test
public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException {
String source = source("ModdedDimensionManager.java");
@@ -110,18 +136,13 @@ public class ModdedLifecycleFailureContractTest {
}
@Test
public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException {
String source = source("ModdedDimensionManager.java");
String loading = method(source, "private static IrisDimension loadPackDimension(");
public void persistentRestoreOpensHistoryBeforeConsultingTheGlobalPack() throws IOException {
String source = source("ModdedGenerationHistoryStorage.java");
String restore = method(source, "static GenerationHistory restoreOrAdopt(");
assertFalse(loading.contains("return null;"));
assertFalse(loading.contains("e.toString()"));
int catchIndex = loading.indexOf("catch (");
if (catchIndex >= 0) {
String failure = blockAt(loading, requiredIndex(loading, "{", catchIndex));
assertTrue(failure.contains("throw "));
assertTrue(failure.contains(", e);") || failure.contains("throw e;"));
}
assertBefore(restore, "GenerationHistory.openIfPresent(", "sourceSupplier\").get();");
assertTrue(restore.contains("if (existing.isPresent())"));
assertBefore(restore, "requireRegistryDefinitions(existing.get()", "return existing.get();");
}
@Test
@@ -249,12 +270,6 @@ public class ModdedLifecycleFailureContractTest {
assertBefore(failure, "rollback.restore(failure);", "throw ");
}
private static void assertRepointBeforePublication(String branch) {
assertBefore(branch, "repointAndBind(", "new Handle(");
assertBefore(branch, "repointAndBind(", "HANDLES.put(");
assertBefore(branch, "repointAndBind(", "return ");
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = requiredIndex(source, first);
int secondIndex = requiredIndex(source, second);
@@ -0,0 +1,28 @@
package art.arcane.iris.modded.command;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class ModdedRiverLocateContractTest {
@Test
public void riverLookupMergesRecordedAndEligibleActiveFeatures() throws IOException {
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"))
.resolve("art/arcane/iris/modded/command/ModdedLocateCommands.java");
String source = Files.readString(sourcePath);
int methodStart = source.indexOf("static int gotoRiver(");
int methodEnd = source.indexOf("static int gotoStructure(", methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("GenerationSemanticQueries.nearestRiver("));
assertTrue(method.contains("runtime == null"));
assertTrue(method.contains("feature.y() + 2"));
assertFalse(method.contains("runtime.nearestFeature("));
assertFalse(method.contains("feature.y() + engine.getDimension().getMinHeight()"));
}
}
@@ -0,0 +1,34 @@
package art.arcane.iris.modded.command;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class ModdedStructureHistoryContractTest {
@Test
public void retainedStructureNamesReachTheSemanticSearchOnBothAdmissionBranches() throws IOException {
Path root = Path.of(System.getProperty("iris.moddedCommonSources"));
String source = Files.readString(root.resolve("art/arcane/iris/modded/command/ModdedLocateCommands.java"));
int start = source.indexOf("static int gotoStructure(");
int end = source.indexOf("static String registeredStructureUnavailableMessage(", start);
String admission = source.substring(start, end);
String retainedCheck = "GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)";
int firstCheck = admission.indexOf(retainedCheck);
assertTrue(firstCheck >= 0);
assertTrue(admission.indexOf(retainedCheck, firstCheck + retainedCheck.length()) >= 0);
int locateStart = source.indexOf("private static void locateIrisStructure(");
int locateEnd = source.indexOf("private static void runNativeStructureLocate(", locateStart);
String locate = source.substring(locateStart, locateEnd);
assertTrue(locate.contains("GenerationSemanticQueries.nearestStructure("));
assertFalse(locate.contains("IrisStructureLocator.locate("));
String suggestions = Files.readString(root.resolve("art/arcane/iris/modded/command/ModdedCommandSuggestions.java"));
assertTrue(suggestions.contains("GenerationFindCatalog.retainedStructureKeys(engine)"));
}
}
@@ -21,8 +21,8 @@ public class ModdedStudioTeleportContractTest {
assertTrue(open.contains("TRANSITIONS.submit("));
assertFalse(open.contains("orTimeout("));
assertFalse(open.contains("deadlineNanos"));
assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.create(");
assertBefore(execute, "ModdedDimensionManager.create(", "ModdedDimensionManager.teleportAsync(");
assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.createTransientStudio(");
assertBefore(execute, "ModdedDimensionManager.createTransientStudio(", "ModdedDimensionManager.teleportAsync(");
assertFalse(execute.contains("deadlineNanos"));
assertFalse(source.contains("player.teleportTo(studio"));
}
@@ -8,6 +8,7 @@ import art.arcane.iris.engine.framework.NativeStructureOwnershipBundle;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -429,6 +430,14 @@ public class NativeStructureReferenceRepairTest {
return data;
}
@Override
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
int blockX,
int blockZ
) {
return null;
}
@Override
public boolean isClosing() {
return false;
+3
View File
@@ -26,6 +26,8 @@ plugins {
alias(libs.plugins.shadow)
}
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
@@ -132,6 +134,7 @@ dependencies {
"art.arcane:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(),
libs.paralithic,
libs.asm,
libs.lru,
libs.caffeine,
libs.dom4j,
+4
View File
@@ -426,6 +426,8 @@ tasks.named('neoforgeJar').configure {
}
List<String> commonModdedArtifactEntries = [
'META-INF/iris/generation-kernels/catalog.tsv',
'META-INF/iris/generation-kernels/abi-1.seal',
'art/arcane/iris/engine/IrisEngine.class',
'art/arcane/iris/spi/IrisPlatform.class',
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
@@ -530,6 +532,8 @@ tasks.register('verifyModdedArtifacts') {
// the two plugin descriptors and the slimjar manifests. Nothing in the reference graph points at
// them, so a shadowJar exclude that drops one is silent until a server hits that path.
List<String> requiredBukkitArtifactEntries = [
'META-INF/iris/generation-kernels/catalog.tsv',
'META-INF/iris/generation-kernels/abi-1.seal',
'plugin.yml',
'paper-plugin.yml',
'slimjar.dat',
+1
View File
@@ -21,6 +21,7 @@ repositories {
dependencies {
implementation(gradleApi())
implementation('org.ow2.asm:asm:9.10.1')
implementation('org.apache.commons:commons-compress:1.28.0')
implementation('com.github.VolmitSoftware:NMSTools:c88961416f')
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
testImplementation('junit:junit:4.13.2')
+46 -22
View File
@@ -1,18 +1,22 @@
import org.apache.commons.compress.archivers.zip.ExtraFieldUtils;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.gradle.api.GradleException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import java.util.zip.ZipFile;
public final class JarCompactor {
private static final int BUFFER_BYTES = 64 * 1024;
@@ -45,31 +49,50 @@ public final class JarCompactor {
private static void rewrite(Path source, Path destination) throws IOException {
byte[] buffer = new byte[BUFFER_BYTES];
try (InputStream rawInput = new BufferedInputStream(Files.newInputStream(source));
ZipInputStream input = new ZipInputStream(rawInput);
OutputStream rawOutput = new BufferedOutputStream(Files.newOutputStream(destination));
ZipOutputStream output = new ZipOutputStream(rawOutput)) {
try (ZipFile input = new ZipFile(source.toFile());
ZipArchiveOutputStream output = new ZipArchiveOutputStream(destination)) {
if (!output.isSeekable()) {
throw new IOException("Jar compaction requires seekable output.");
}
output.setLevel(9);
ZipEntry entry;
while ((entry = input.getNextEntry()) != null) {
output.setUseZip64(Zip64Mode.Never);
output.setCreateUnicodeExtraFields(ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER);
Set<String> names = new HashSet<>();
Enumeration<? extends ZipEntry> entries = input.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
ZipEntry compacted = copyMetadata(entry);
output.putNextEntry(compacted);
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
output.write(buffer, 0, read);
}
if (!names.add(entry.getName())) {
throw new IOException("Duplicate jar entry: " + entry.getName());
}
output.closeEntry();
output.putArchiveEntry(copyMetadata(entry));
copyEntry(input, output, entry, buffer);
output.closeArchiveEntry();
}
}
}
private static ZipEntry copyMetadata(ZipEntry source) {
ZipEntry target = new ZipEntry(source.getName());
private static void copyEntry(ZipFile input, ZipArchiveOutputStream output, ZipEntry entry, byte[] buffer)
throws IOException {
CRC32 crc = new CRC32();
long size = 0L;
try (InputStream content = input.getInputStream(entry)) {
int read;
while ((read = content.read(buffer)) != -1) {
output.write(buffer, 0, read);
crc.update(buffer, 0, read);
size += read;
}
}
if (size != entry.getSize() || crc.getValue() != entry.getCrc()) {
throw new IOException("Jar entry failed size or CRC verification: " + entry.getName());
}
}
private static ZipArchiveEntry copyMetadata(ZipEntry source) throws IOException {
ZipArchiveEntry target = new ZipArchiveEntry(source.getName());
target.setMethod(ZipEntry.DEFLATED);
if (source.getTime() >= 0L) {
target.setTime(source.getTime());
@@ -78,7 +101,8 @@ public final class JarCompactor {
target.setComment(source.getComment());
}
if (source.getExtra() != null) {
target.setExtra(source.getExtra());
target.setExtraFields(ExtraFieldUtils.parse(source.getExtra(), true,
ZipArchiveEntry.ExtraFieldParsingMode.BEST_EFFORT));
}
return target;
}
@@ -0,0 +1,463 @@
package art.arcane.iris.buildtools;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class GenerationKernelSeal {
public static final String FORMAT = "iris-generation-kernel-seal-v1";
private static final Pattern HASH = Pattern.compile("[0-9a-f]{64}");
private static final long MAXIMUM_SEAL_BYTES = 8L * 1024L * 1024L;
private static final int MAXIMUM_ENTRIES = 30_000;
private GenerationKernelSeal() {
}
public static void main(String[] arguments) throws IOException {
if (arguments.length < 4 || arguments.length % 2 != 0 || !arguments[0].equals("verify-dependencies")) {
throw new IllegalArgumentException("Expected verify-dependencies <seal> <identity> <artifact> pairs.");
}
Map<String, Path> dependencies = new TreeMap<>();
for (int index = 2; index < arguments.length; index += 2) {
if (dependencies.putIfAbsent(arguments[index], Path.of(arguments[index + 1])) != null) {
throw new IllegalArgumentException("Duplicate resolved generation dependency.");
}
}
verifyDependencySubset(Path.of(arguments[1]), dependencies);
}
public static void verifyDependencySubset(Path seal, Map<String, Path> dependencies) throws IOException {
SourceManifest expected = read(seal);
for (Map.Entry<String, String> dependency : dependencyHashes(dependencies).entrySet()) {
if (!Objects.equals(expected.dependencies().get(dependency.getKey()), dependency.getValue())) {
throw new IOException("Resolved platform dependency does not match generation ABI " + expected.abi()
+ ": " + dependency.getKey());
}
}
}
public static void verifyCatalog(Path catalog, Map<Integer, SourceManifest> manifests) throws IOException {
if (!Files.isRegularFile(catalog) || Files.size(catalog) > 16 * 1024) {
throw new IOException("Missing or oversized generation kernel catalog.");
}
String source = Files.readString(catalog, StandardCharsets.UTF_8);
String[] lines = source.split("\n", -1);
if (!source.endsWith("\n") || source.indexOf('\r') >= 0 || lines.length < 4 || lines.length > 128
|| !lines[0].equals("iris-generation-kernel-catalog-v1")) {
throw new IOException("Invalid generation kernel catalog.");
}
try {
String[] current = lines[1].split("\t", -1);
if (current.length != 4 || !current[0].equals("current")) {
throw new IllegalArgumentException("Missing current generation version.");
}
int currentAbi = positive(current[1]);
AlgorithmVersion algorithms = new AlgorithmVersion(positive(current[2]), positive(current[3]));
SourceManifest selected = manifests.get(currentAbi);
if (selected == null || !selected.algorithms().contains(algorithms)) {
throw new IllegalArgumentException("Current generation version has no matching sealed factory.");
}
TreeSet<Integer> registered = new TreeSet<>();
int previous = 0;
for (int index = 2; index < lines.length - 1; index++) {
String[] kernel = lines[index].split("\t", -1);
if (kernel.length != 2 || !kernel[0].equals("kernel")) {
throw new IllegalArgumentException("Invalid kernel registration.");
}
int abi = positive(kernel[1]);
if (abi <= previous) {
throw new IllegalArgumentException("Kernel registrations must be unique and sorted.");
}
previous = abi;
registered.add(abi);
}
if (!registered.equals(manifests.keySet())) {
throw new IllegalArgumentException("Kernel catalog must register exactly the retained sealed kernels.");
}
} catch (IllegalArgumentException failure) {
throw new IOException("Invalid generation kernel catalog.", failure);
}
}
private static int positive(String value) {
if (!value.matches("[1-9][0-9]{0,8}")) {
throw new IllegalArgumentException("Invalid generation version.");
}
return Integer.parseInt(value);
}
public static SourceManifest capture(CaptureOptions options, Map<String, Path> dependencies) throws IOException {
CaptureOptions required = Objects.requireNonNull(options, "options");
Map<String, String> sourceHashes = sources(required.repository(), required.roots(), required.exclusions());
if (sourceHashes.isEmpty()) {
throw new IOException("Generation kernel source scope is empty.");
}
return new SourceManifest(required.abi(), required.factoryClass(), required.algorithms(), required.roots(), required.exclusions(),
sourceHashes, dependencyHashes(dependencies));
}
public static void verify(Path repository, Path seal, Map<String, Path> dependencies) throws IOException {
SourceManifest expected = read(seal);
SourceManifest actual = capture(new CaptureOptions(repository, expected.abi(), expected.factoryClass(), expected.algorithms(),
expected.roots(), expected.exclusions()), dependencies);
List<String> failures = new ArrayList<>();
differences("source", expected.sources(), actual.sources(), failures);
differences("dependency", expected.dependencies(), actual.dependencies(), failures);
if (!failures.isEmpty()) {
throw new IOException("Generation ABI " + expected.abi() + " no longer matches its sealed implementation:\n"
+ String.join("\n", failures)
+ "\nRetain this kernel's implementation and add a separately versioned kernel for changed generation.");
}
}
public static void writeNew(Path destination, SourceManifest manifest) throws IOException {
Files.createDirectories(destination.toAbsolutePath().getParent());
Files.writeString(destination, encode(manifest), StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
}
public static SourceManifest read(Path path) throws IOException {
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > MAXIMUM_SEAL_BYTES) {
throw new IOException("Missing or oversized generation kernel seal: " + path);
}
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
if (lines.size() < 4 || !FORMAT.equals(lines.getFirst()) || lines.size() > MAXIMUM_ENTRIES) {
throw new IOException("Invalid generation kernel seal: " + path);
}
int abi = 0;
String factory = null;
List<String> roots = new ArrayList<>();
List<AlgorithmVersion> algorithms = new ArrayList<>();
List<String> exclusions = new ArrayList<>();
Map<String, String> sources = new TreeMap<>();
Map<String, String> dependencies = new TreeMap<>();
try {
for (int index = 1; index < lines.size(); index++) {
String[] entry = lines.get(index).split("\t", -1);
if (entry.length < 2 || entry.length > 3) {
throw new IllegalArgumentException("Invalid seal entry.");
}
switch (entry[0]) {
case "abi" -> {
if (abi != 0 || entry.length != 2) {
throw new IllegalArgumentException("Duplicate or invalid ABI.");
}
abi = Integer.parseInt(entry[1]);
}
case "factory" -> {
if (factory != null || entry.length != 2) {
throw new IllegalArgumentException("Duplicate or invalid factory.");
}
factory = entry[1];
}
case "scope" -> addPath(roots, entry);
case "algorithm" -> {
if (entry.length != 3) {
throw new IllegalArgumentException("Invalid algorithm version.");
}
algorithms.add(new AlgorithmVersion(Integer.parseInt(entry[1]), Integer.parseInt(entry[2])));
}
case "exclude" -> addPath(exclusions, entry);
case "source" -> addHash(sources, entry, true);
case "dependency" -> addHash(dependencies, entry, false);
default -> throw new IllegalArgumentException("Unknown generation kernel seal entry.");
}
}
SourceManifest manifest = new SourceManifest(abi, factory, algorithms, roots, exclusions, sources, dependencies);
if (!Files.readString(path, StandardCharsets.UTF_8).equals(encode(manifest))) {
throw new IllegalArgumentException("Generation kernel seal is not canonical.");
}
return manifest;
} catch (RuntimeException failure) {
throw new IOException("Invalid generation kernel seal: " + path, failure);
}
}
public static String fingerprint(SourceManifest manifest) {
return hash(encode(manifest).getBytes(StandardCharsets.UTF_8));
}
public static String encode(SourceManifest manifest) {
StringBuilder text = new StringBuilder(FORMAT).append('\n');
text.append("abi\t").append(manifest.abi()).append('\n');
text.append("factory\t").append(manifest.factoryClass()).append('\n');
for (AlgorithmVersion algorithm : manifest.algorithms()) {
text.append("algorithm\t").append(algorithm.rng()).append('\t').append(algorithm.seed()).append('\n');
}
for (String scope : manifest.roots()) {
text.append("scope\t").append(scope).append('\n');
}
for (String exclusion : manifest.exclusions()) {
text.append("exclude\t").append(exclusion).append('\n');
}
appendHashes(text, "source", manifest.sources());
appendHashes(text, "dependency", manifest.dependencies());
return text.toString();
}
public static String artifactHash(Path artifact) throws IOException {
if (!Files.isRegularFile(artifact)) {
throw new IOException("Missing generation dependency artifact: " + artifact);
}
MessageDigest digest = digest();
try (ZipFile zip = new ZipFile(artifact.toFile())) {
hashRuntimeManifest(zip, digest);
Map<String, ZipEntry> entries = new TreeMap<>();
for (ZipEntry entry : zip.stream().toList()) {
String name = entry.getName();
if (entry.isDirectory() || metadata(name)) {
continue;
}
if (entries.putIfAbsent(name, entry) != null) {
throw new IOException("Duplicate dependency artifact entry: " + name);
}
}
for (Map.Entry<String, ZipEntry> entry : entries.entrySet()) {
update(digest, entry.getKey());
try (InputStream input = zip.getInputStream(entry.getValue())) {
MessageDigest content = digest();
byte[] buffer = new byte[16 * 1024];
int read;
while ((read = input.read(buffer)) != -1) {
content.update(buffer, 0, read);
}
update(digest, HexFormat.of().formatHex(content.digest()));
}
}
}
return HexFormat.of().formatHex(digest.digest());
}
private static void hashRuntimeManifest(ZipFile zip, MessageDigest digest) throws IOException {
ZipEntry entry = zip.getEntry("META-INF/MANIFEST.MF");
if (entry == null) {
return;
}
Manifest manifest;
try (InputStream input = zip.getInputStream(entry)) {
manifest = new Manifest(input);
}
List<String> runtimeAttributes = List.of("Multi-Release", "Automatic-Module-Name", "Class-Path", "Sealed",
"Agent-Class", "Premain-Class", "Launcher-Agent-Class", "Boot-Class-Path",
"Can-Redefine-Classes", "Can-Retransform-Classes", "Can-Set-Native-Method-Prefix");
for (String name : runtimeAttributes) {
String value = manifest.getMainAttributes().getValue(name);
if (value != null) {
update(digest, "manifest/" + name);
update(digest, value);
}
}
for (Map.Entry<String, Attributes> section : new TreeMap<>(manifest.getEntries()).entrySet()) {
String sealed = section.getValue().getValue("Sealed");
if (sealed != null) {
update(digest, "manifest/section/" + section.getKey());
update(digest, sealed);
}
}
}
private static Map<String, String> sources(Path repository, List<String> roots, List<String> exclusions)
throws IOException {
Path base = repository.toAbsolutePath().normalize();
Map<String, String> hashes = new TreeMap<>();
for (String root : roots) {
Path sourceRoot = base.resolve(root).normalize();
if (!sourceRoot.startsWith(base) || !Files.exists(sourceRoot, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Missing generation source scope: " + root);
}
try (Stream<Path> walk = Files.walk(sourceRoot)) {
for (Path source : walk.toList()) {
String relative = base.relativize(source).toString().replace('\\', '/');
if (excluded(relative, exclusions)) {
continue;
}
if (Files.isSymbolicLink(source)) {
throw new IOException("Generation source must not be a symbolic link: " + relative);
}
if (Files.isRegularFile(source) && (source.equals(sourceRoot)
|| source.getFileName().toString().endsWith(".java"))) {
String content = Files.readString(source, StandardCharsets.UTF_8).replace("\r\n", "\n");
hashes.put(relative, hash(content.getBytes(StandardCharsets.UTF_8)));
}
}
}
}
return hashes;
}
private static Map<String, String> dependencyHashes(Map<String, Path> dependencies) throws IOException {
Map<String, String> hashes = new TreeMap<>();
for (Map.Entry<String, Path> dependency : Objects.requireNonNull(dependencies, "dependencies").entrySet()) {
String key = token(dependency.getKey());
hashes.put(key, artifactHash(dependency.getValue()));
}
if (hashes.isEmpty()) {
throw new IOException("Generation kernel dependency scope is empty.");
}
return hashes;
}
private static void differences(String type, Map<String, String> expected, Map<String, String> actual,
List<String> failures) {
TreeSet<String> keys = new TreeSet<>(expected.keySet());
keys.addAll(actual.keySet());
for (String key : keys) {
if (!Objects.equals(expected.get(key), actual.get(key))) {
String change = !expected.containsKey(key) ? "added" : !actual.containsKey(key) ? "deleted" : "changed";
failures.add(type + " " + change + ": " + key);
}
}
}
private static boolean excluded(String path, List<String> exclusions) {
for (String exclusion : exclusions) {
if (path.equals(exclusion) || path.startsWith(exclusion + "/")) {
return true;
}
}
return false;
}
private static boolean metadata(String name) {
return name.equals("META-INF/MANIFEST.MF") || name.startsWith("META-INF/maven/")
|| name.startsWith("META-INF/") && (name.endsWith(".SF") || name.endsWith(".RSA") || name.endsWith(".DSA"));
}
private static void addPath(List<String> paths, String[] entry) {
if (entry.length != 2 || paths.contains(entry[1])) {
throw new IllegalArgumentException("Duplicate or invalid source scope.");
}
paths.add(relative(entry[1]));
}
private static void addHash(Map<String, String> hashes, String[] entry, boolean path) {
if (entry.length != 3 || !HASH.matcher(entry[2]).matches()) {
throw new IllegalArgumentException("Invalid sealed hash.");
}
String key = path ? relative(entry[1]) : token(entry[1]);
if (hashes.putIfAbsent(key, entry[2]) != null) {
throw new IllegalArgumentException("Duplicate sealed entry.");
}
}
private static void appendHashes(StringBuilder text, String type, Map<String, String> hashes) {
for (Map.Entry<String, String> entry : new TreeMap<>(hashes).entrySet()) {
text.append(type).append('\t').append(entry.getKey()).append('\t').append(entry.getValue()).append('\n');
}
}
private static String relative(String value) {
String path = token(value);
if (path.startsWith("/") || path.contains("\\") || path.contains(":")
|| !Path.of(path).normalize().toString().replace('\\', '/').equals(path)
|| path.equals("..") || path.startsWith("../")) {
throw new IllegalArgumentException("Invalid generation source path: " + path);
}
return path;
}
private static String token(String value) {
String required = Objects.requireNonNull(value, "seal token");
if (required.isBlank() || required.chars().anyMatch(Character::isWhitespace)) {
throw new IllegalArgumentException("Invalid generation seal token.");
}
return required;
}
private static List<String> paths(Collection<String> values) {
TreeSet<String> sorted = new TreeSet<>();
for (String value : values) {
if (!sorted.add(relative(value))) {
throw new IllegalArgumentException("Duplicate generation source scope.");
}
}
return List.copyOf(sorted);
}
private static String hash(byte[] bytes) {
return HexFormat.of().formatHex(digest().digest(bytes));
}
private static MessageDigest digest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException(failure);
}
}
private static void update(MessageDigest digest, String value) {
digest.update(value.getBytes(StandardCharsets.UTF_8));
digest.update((byte) '\n');
}
public record CaptureOptions(Path repository, int abi, String factoryClass, List<AlgorithmVersion> algorithms, List<String> roots,
List<String> exclusions) {
public CaptureOptions {
Objects.requireNonNull(repository, "repository");
if (abi < 1) {
throw new IllegalArgumentException("Generation ABI must be positive.");
}
factoryClass = token(factoryClass);
TreeSet<AlgorithmVersion> sortedAlgorithms = new TreeSet<>(algorithms);
if (sortedAlgorithms.isEmpty() || sortedAlgorithms.size() != algorithms.size()) {
throw new IllegalArgumentException("Generation algorithm versions must be nonempty and unique.");
}
algorithms = List.copyOf(sortedAlgorithms);
roots = paths(roots);
exclusions = paths(exclusions);
if (roots.isEmpty()) {
throw new IllegalArgumentException("Generation source scope must not be empty.");
}
}
}
public record SourceManifest(int abi, String factoryClass, List<AlgorithmVersion> algorithms, List<String> roots, List<String> exclusions,
Map<String, String> sources, Map<String, String> dependencies) {
public SourceManifest {
CaptureOptions options = new CaptureOptions(Path.of("."), abi, factoryClass, algorithms, roots, exclusions);
algorithms = options.algorithms();
roots = options.roots();
exclusions = options.exclusions();
sources = Map.copyOf(sources);
dependencies = Map.copyOf(dependencies);
if (sources.isEmpty() || dependencies.isEmpty() || sources.size() + dependencies.size() > MAXIMUM_ENTRIES) {
throw new IllegalArgumentException("Invalid generation kernel seal entry count.");
}
}
}
public record AlgorithmVersion(int rng, int seed) implements Comparable<AlgorithmVersion> {
public AlgorithmVersion {
if (rng < 1 || seed < 1) {
throw new IllegalArgumentException("Generation algorithm versions must be positive.");
}
}
@Override
public int compareTo(AlgorithmVersion other) {
int byRng = Integer.compare(rng, other.rng);
return byRng == 0 ? Integer.compare(seed, other.seed) : byRng;
}
}
}
@@ -0,0 +1,192 @@
import art.arcane.iris.buildtools.GenerationKernelSeal;
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.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationKernelSealTest {
@Rule
public final TemporaryFolder temporary = new TemporaryFolder();
@Test
public void verifiesUnchangedSourceAndCanonicalDependencyContents() throws Exception {
Fixture fixture = fixture();
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
artifact(fixture.dependency(), "same code", "another version", 1_700_000_000_000L);
assertEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
assertEquals(GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())),
GenerationKernelSeal.fingerprint(GenerationKernelSeal.capture(fixture.options(), fixture.dependencies())));
}
@Test
public void rejectsChangedAddedAndDeletedGenerationSources() throws Exception {
Fixture fixture = fixture();
Path source = fixture.root().resolve("generation/Noise.java");
Files.writeString(source, "class Noise { int value = 2; }");
assertTrue(assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
.getMessage().contains("source changed"));
Files.writeString(source, "class Noise { int value = 1; }");
Path added = fixture.root().resolve("generation/Added.java");
Files.writeString(added, "class Added {}");
assertTrue(assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
.getMessage().contains("source added"));
Files.delete(added);
Files.delete(source);
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
}
@Test
public void rejectsChangedAddedAndDeletedDependencies() throws Exception {
Fixture fixture = fixture();
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
artifact(fixture.dependency(), "changed code", "same version", 0L);
assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
assertTrue(assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
.getMessage().contains("dependency changed"));
artifact(fixture.dependency(), "same code", "same version", 0L);
assertThrows(IOException.class, () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(),
Map.of("math", fixture.dependency(), "new-math", fixture.dependency())));
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), Map.of()));
}
@Test
public void rejectsMultiReleaseManifestChanges() throws Exception {
Fixture fixture = fixture();
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
artifact(fixture.dependency(), "same code", "same version\r\nMulti-Release: true", 0L);
assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
}
@Test
public void rejectsAgentEntrypointAndCapabilityChanges() throws Exception {
Fixture fixture = fixture();
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
for (String attribute : List.of("Agent-Class", "Premain-Class", "Launcher-Agent-Class", "Boot-Class-Path",
"Can-Redefine-Classes", "Can-Retransform-Classes", "Can-Set-Native-Method-Prefix")) {
artifact(fixture.dependency(), "same code", "same version\r\n" + attribute + ": changed", 0L);
assertNotEquals(attribute, original, GenerationKernelSeal.artifactHash(fixture.dependency()));
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
}
}
@Test
public void sealsExplicitAgentBuildInputsAndExecutableSources() throws Exception {
Fixture fixture = fixture();
Path agentSource = fixture.root().resolve("agent/src/main/java/Installer.java");
Path agentBuild = fixture.root().resolve("agent/build.gradle");
Files.createDirectories(agentSource.getParent());
Files.writeString(agentSource, "class Installer {}");
Files.writeString(agentBuild, "plugins { id 'java' }");
GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions(
fixture.root(), 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)),
List.of("generation", "agent/build.gradle", "agent/src/main/java"), List.of("generation/kernels"));
Path seal = fixture.root().resolve("agent.seal");
GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, fixture.dependencies()));
GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies());
Files.writeString(agentBuild, "plugins { id 'application' }");
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies()));
Files.writeString(agentBuild, "plugins { id 'java' }");
Files.writeString(agentSource, "class Installer { boolean defers; }");
assertThrows(IOException.class,
() -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies()));
}
@Test
public void checksTheActualPlatformDependencySubset() throws Exception {
Fixture fixture = fixture();
GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies());
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(),
Map.of("unknown-library", fixture.dependency())));
artifact(fixture.dependency(), "platform override", "same version", 0L);
assertThrows(IOException.class,
() -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies()));
}
@Test
public void catalogCannotSelectAnUnsealedAlgorithmTuple() throws Exception {
Fixture fixture = fixture();
Path catalog = fixture.root().resolve("catalog.tsv");
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\n");
Map<Integer, GenerationKernelSeal.SourceManifest> manifests = Map.of(1, GenerationKernelSeal.read(fixture.seal()));
GenerationKernelSeal.verifyCatalog(catalog, manifests);
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t2\t1\nkernel\t1\n");
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyCatalog(catalog, manifests));
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\nkernel\t2\n");
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyCatalog(catalog, manifests));
}
@Test
public void permitsSeparatelyScopedFutureKernelsWithoutChangingRetainedSeal() throws Exception {
Fixture fixture = fixture();
Path future = fixture.root().resolve("generation/kernels/v2/NewMath.java");
Files.createDirectories(future.getParent());
Files.writeString(future, "class NewMath {}");
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
GenerationKernelSeal.SourceManifest next = GenerationKernelSeal.capture(new GenerationKernelSeal.CaptureOptions(
fixture.root(), 2, "example.KernelV2", List.of(new GenerationKernelSeal.AlgorithmVersion(2, 3)),
List.of("generation/kernels/v2"), List.of()), fixture.dependencies());
assertNotEquals(GenerationKernelSeal.fingerprint(next),
GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())));
assertThrows(IOException.class,
() -> GenerationKernelSeal.writeNew(fixture.seal(), next));
}
private Fixture fixture() throws Exception {
Path root = temporary.newFolder().toPath();
Files.createDirectories(root.resolve("generation"));
Files.writeString(root.resolve("generation/Noise.java"), "class Noise { int value = 1; }");
Path dependency = root.resolve("math.jar");
artifact(dependency, "same code", "same version", 0L);
GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions(
root, 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)),
List.of("generation"), List.of("generation/kernels"));
Path seal = root.resolve("abi-1.seal");
Map<String, Path> dependencies = Map.of("math", dependency);
GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, dependencies));
return new Fixture(root, seal, dependency, dependencies, options);
}
private static void artifact(Path path, String content, String version, long timestamp) throws IOException {
try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(path))) {
ZipEntry metadata = new ZipEntry("META-INF/MANIFEST.MF");
metadata.setTime(timestamp);
output.putNextEntry(metadata);
output.write(("Manifest-Version: 1.0\r\nImplementation-Version: " + version + "\r\n\r\n")
.getBytes(StandardCharsets.UTF_8));
output.closeEntry();
ZipEntry code = new ZipEntry("example/Math.class");
code.setTime(timestamp);
output.putNextEntry(code);
output.write(content.getBytes(StandardCharsets.UTF_8));
output.closeEntry();
}
}
private record Fixture(Path root, Path seal, Path dependency, Map<String, Path> dependencies,
GenerationKernelSeal.CaptureOptions options) {
}
}
@@ -1,16 +1,33 @@
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.gradle.api.GradleException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.Enumeration;
import java.util.HexFormat;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class JarCompactorTest {
@Rule
@@ -36,4 +53,128 @@ public class JarCompactorTest {
jar.getJarEntry("example/value.txt")).readAllBytes());
}
}
@Test
public void preservesJarIdentityAndProducesDeterministicSeekableOutput() throws Exception {
File artifact = temporaryFolder.newFile("identity.jar");
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact));
InputStream classResource = getClass().getResourceAsStream("/JarCompactorTest.class")) {
output.setLevel(9);
writeEntry(output, "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\r\nMulti-Release: true\r\n\r\n"
.getBytes(StandardCharsets.UTF_8));
writeEntry(output, "JarCompactorTest.class", classResource.readAllBytes());
writeEntry(output, "META-INF/iris/generation-kernels/abi-1.seal", "registry fixture".getBytes(StandardCharsets.UTF_8));
writeEntry(output, "example/caf\u00e9.txt", "unicode name".getBytes(StandardCharsets.UTF_8));
for (int index = 0; index < 100; index++) {
writeEntry(output, "example/item-" + index + ".txt", ("value-" + index).getBytes(StandardCharsets.UTF_8));
}
}
Map<String, EntryIdentity> original = identity(artifact);
long originalBytes = artifact.length();
JarCompactor.compact(artifact);
assertEquals(original, identity(artifact));
assertTrue(artifact.length() < originalBytes);
try (JarFile jar = new JarFile(artifact)) {
assertEquals("true", jar.getManifest().getMainAttributes().getValue("Multi-Release"));
assertEquals(0xCA, jar.getInputStream(jar.getJarEntry("JarCompactorTest.class")).read());
}
try (ZipFile zip = ZipFile.builder().setFile(artifact).get()) {
Enumeration<ZipArchiveEntry> entries = zip.getEntries();
while (entries.hasMoreElements()) {
assertFalse(entries.nextElement().getGeneralPurposeBit().usesDataDescriptor());
}
}
byte[] first = Files.readAllBytes(artifact.toPath());
JarCompactor.compact(artifact);
assertArrayEquals(first, Files.readAllBytes(artifact.toPath()));
}
@Test
public void rejectsMalformedArchivesWithoutReplacingTheSource() throws Exception {
File artifact = temporaryFolder.newFile("invalid.jar");
byte[] original = "not a zip archive".getBytes(StandardCharsets.UTF_8);
Files.write(artifact.toPath(), original);
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
assertArrayEquals(original, Files.readAllBytes(artifact.toPath()));
assertEquals(1, temporaryFolder.getRoot().listFiles().length);
}
@Test
public void rejectsCorruptPayloadWithoutReplacingTheSource() throws Exception {
File artifact = temporaryFolder.newFile("corrupt.jar");
byte[] payload = "checked-payload".getBytes(StandardCharsets.UTF_8);
CRC32 crc = new CRC32();
crc.update(payload);
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) {
JarEntry entry = new JarEntry("value.txt");
entry.setMethod(ZipEntry.STORED);
entry.setSize(payload.length);
entry.setCrc(crc.getValue());
output.putNextEntry(entry);
output.write(payload);
output.closeEntry();
}
byte[] corrupt = Files.readAllBytes(artifact.toPath());
int payloadOffset = new String(corrupt, StandardCharsets.ISO_8859_1).indexOf("checked-payload");
assertTrue(payloadOffset > 0);
corrupt[payloadOffset] ^= 1;
Files.write(artifact.toPath(), corrupt);
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
assertArrayEquals(corrupt, Files.readAllBytes(artifact.toPath()));
}
@Test
public void rejectsDuplicateEntriesWithoutReplacingTheSource() throws Exception {
File artifact = temporaryFolder.newFile("duplicate.jar");
try (ZipArchiveOutputStream output = new ZipArchiveOutputStream(artifact)) {
for (int index = 0; index < 2; index++) {
output.putArchiveEntry(new ZipArchiveEntry("duplicate.txt"));
output.write(index);
output.closeArchiveEntry();
}
}
byte[] original = Files.readAllBytes(artifact.toPath());
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
assertArrayEquals(original, Files.readAllBytes(artifact.toPath()));
}
private static void writeEntry(JarOutputStream output, String name, byte[] content) throws Exception {
JarEntry entry = new JarEntry(name);
entry.setTime(315_532_800_000L);
entry.setComment("preserved entry comment");
entry.setExtra(new byte[]{(byte) 0xFE, (byte) 0xCA, 1, 0, 7});
output.putNextEntry(entry);
output.write(content);
output.closeEntry();
}
private static Map<String, EntryIdentity> identity(File artifact) throws Exception {
Map<String, EntryIdentity> entries = new TreeMap<>();
try (JarFile jar = new JarFile(artifact)) {
Enumeration<JarEntry> contents = jar.entries();
while (contents.hasMoreElements()) {
JarEntry entry = contents.nextElement();
byte[] bytes;
try (InputStream input = jar.getInputStream(entry)) {
bytes = input.readAllBytes();
}
entries.put(entry.getName(), new EntryIdentity(
HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)),
entry.getSize(), entry.getCrc(), entry.getTime(), entry.getComment(),
entry.getExtra() == null ? "" : HexFormat.of().formatHex(entry.getExtra())));
}
}
return entries;
}
private record EntryIdentity(String sha256, long size, long crc, long time, String comment, String extra) {
}
}
+8
View File
@@ -4,6 +4,14 @@ plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation('junit:junit:4.13.2')
}
tasks.named('jar', Jar).configure {
manifest.attributes(
'Agent-Class': 'art.arcane.iris.util.project.agent.Installer',
@@ -1,9 +1,41 @@
package art.arcane.iris.util.project.agent;
import java.lang.instrument.Instrumentation;
import java.io.Closeable;
import java.io.IOException;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
public class Installer {
private static volatile Instrumentation instrumentation;
private static final Map<ClassLoader, Boolean> retainedClassLoaders = new IdentityHashMap<>();
public static synchronized void retainClassLoader(ClassLoader loader) {
ClassLoader requiredLoader = Objects.requireNonNull(loader, "Plugin class loader");
if (!(requiredLoader instanceof Closeable)) {
throw new IllegalArgumentException("Plugin class loader is not closeable");
}
retainedClassLoaders.putIfAbsent(requiredLoader, false);
}
public static synchronized boolean deferClassLoaderClose(ClassLoader loader) {
if (!retainedClassLoaders.containsKey(loader)) {
return false;
}
retainedClassLoaders.put(loader, true);
return true;
}
public static void releaseClassLoader(ClassLoader loader) throws IOException {
boolean closeRequested;
synchronized (Installer.class) {
closeRequested = Boolean.TRUE.equals(retainedClassLoaders.remove(loader));
}
if (closeRequested) {
((Closeable) loader).close();
}
}
public static Instrumentation getInstrumentation() {
Instrumentation instrumentation = Installer.instrumentation;
@@ -26,4 +58,4 @@ public class Installer {
return;
Installer.instrumentation = instrumentation;
}
}
}
@@ -0,0 +1,164 @@
package art.arcane.iris.util.project.agent;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ClassLoaderCloseDeferralTest {
@Test
public void unarmedPluginDisableClosesNormally() throws Exception {
TestLoader loader = new TestLoader(new URL[0]);
loader.close();
Installer.releaseClassLoader(loader);
assertEquals(1, loader.closes.get());
}
@Test
public void holdingOnePluginDoesNotHoldAnotherEqualLoader() throws Exception {
TestLoader iris = new TestLoader(new URL[0]);
TestLoader other = new TestLoader(new URL[0]);
Installer.retainClassLoader(iris);
try {
other.close();
iris.close();
assertEquals(1, other.closes.get());
assertEquals(0, iris.closes.get());
} finally {
Installer.releaseClassLoader(iris);
}
assertEquals(1, iris.closes.get());
}
@Test
public void deferredJarRemainsReadableUntilCleanupReleasesIt() throws Exception {
Path jar = Files.createTempFile("iris-loader-close", ".jar");
try {
try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(jar))) {
output.putNextEntry(new JarEntry("late-cleanup.txt"));
output.write("available".getBytes(StandardCharsets.UTF_8));
output.closeEntry();
}
TestLoader loader = new TestLoader(new URL[]{jar.toUri().toURL()});
Installer.retainClassLoader(loader);
try {
loader.close();
loader.close();
try (InputStream input = loader.getResourceAsStream("late-cleanup.txt")) {
assertEquals("available", new String(input.readAllBytes(), StandardCharsets.UTF_8));
}
} finally {
Installer.releaseClassLoader(loader);
}
Installer.releaseClassLoader(loader);
assertEquals(1, loader.closes.get());
assertNull(loader.getResourceAsStream("late-cleanup.txt"));
} finally {
Files.delete(jar);
}
}
@Test
public void cancellingBeforePaperRequestsCloseDoesNotCloseTheLoader() throws Exception {
TestLoader loader = new TestLoader(new URL[0]);
Installer.retainClassLoader(loader);
Installer.releaseClassLoader(loader);
assertEquals(0, loader.closes.get());
loader.close();
assertEquals(1, loader.closes.get());
}
@Test
public void concurrentReleaseClosesOnceOutsideLifecycleLock() throws Exception {
TestLoader loader = new TestLoader(new URL[0]);
loader.closeEntered = new CountDownLatch(1);
loader.finishClose = new CountDownLatch(1);
Installer.retainClassLoader(loader);
loader.close();
AtomicReference<Throwable> failure = new AtomicReference<>();
Thread first = new Thread(() -> release(loader, failure), "first-loader-release");
first.start();
try {
assertTrue(loader.closeEntered.await(5L, TimeUnit.SECONDS));
Thread second = new Thread(() -> release(loader, failure), "second-loader-release");
second.start();
second.join(5000L);
assertFalse(second.isAlive());
assertEquals(1, loader.closes.get());
} finally {
loader.finishClose.countDown();
first.join(5000L);
}
assertFalse(first.isAlive());
assertNull(failure.get());
}
private static void release(TestLoader loader, AtomicReference<Throwable> failure) {
try {
Installer.releaseClassLoader(loader);
} catch (Throwable exception) {
failure.compareAndSet(null, exception);
}
}
private static final class TestLoader extends URLClassLoader {
private final AtomicInteger closes = new AtomicInteger();
private CountDownLatch closeEntered;
private CountDownLatch finishClose;
private TestLoader(URL[] urls) {
super(urls, ClassLoader.getPlatformClassLoader());
}
@Override
public void close() throws IOException {
if (Installer.deferClassLoaderClose(this)) {
return;
}
assertFalse(Thread.holdsLock(Installer.class));
closes.incrementAndGet();
if (closeEntered != null) {
closeEntered.countDown();
try {
if (!finishClose.await(5L, TimeUnit.SECONDS)) {
throw new IOException("Loader close was not released");
}
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new IOException(exception);
}
}
super.close();
}
@Override
public boolean equals(Object other) {
return other instanceof TestLoader;
}
@Override
public int hashCode() {
return 1;
}
}
}
+3
View File
@@ -128,6 +128,7 @@ dependencies {
testImplementation('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(libs.paper.api)
testImplementation(libs.log4j.core)
testRuntimeOnly(libs.paper.api)
testImplementation(libs.multiverseCore)
}
@@ -520,3 +521,5 @@ tasks.register('bukkitPurityRatchet') {
tasks.named('check').configure {
dependsOn('bukkitPurityRatchet')
}
apply from: rootProject.file('gradle/generation-kernel-seals.gradle')
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
iris-generation-kernel-catalog-v1
current 1 1 1
kernel 1
1 iris-generation-kernel-catalog-v1
2 current 1 1 1
3 kernel 1
@@ -9,8 +9,14 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.nms.datapack.IDataFixer;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import art.arcane.iris.engine.history.GenerationEpoch;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContract.PhysicalResourceKey;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisCustomBiomeAliasResolver;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
@@ -40,13 +46,14 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public final class IrisDatapackCompiler {
private static final int INPUT_FINGERPRINT_SCHEMA = 2;
private static final int INPUT_FINGERPRINT_SCHEMA = 3;
private static final int INPUT_BUFFER_BYTES = 64 * 1024;
private static final int WORLD_PACK_SCAN_DEPTH = 8;
private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
@@ -90,6 +97,7 @@ public final class IrisDatapackCompiler {
) throws IOException {
LinkedHashMap<Path, File> roots = new LinkedHashMap<>();
collectInstalledPackRoots(dataDirectory.resolve("packs"), roots, validateWholePack);
collectCanonicalWorldPackRoot(serverRoot, roots, validateWholePack);
collectWorldPackRoots(serverRoot.resolve("dimensions"), roots, validateWholePack);
return new ArrayList<>(roots.values());
}
@@ -179,14 +187,18 @@ public final class IrisDatapackCompiler {
Objects.requireNonNull(packRoots, "packRoots");
Objects.requireNonNull(fixer, "fixer");
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
Set<String> installedBiomeKeys = new LinkedHashSet<>();
Map<String, Set<String>> biomeTags = new TreeMap<>();
for (File packRoot : packRoots) {
PackDirectoryResolver.requireSafePackTree(packRoot);
if (!hasDimensions(packRoot.toPath())) {
continue;
}
BiomeAliasContext aliasContext = biomeAliasContext(packRoot.toPath());
IrisData data = IrisData.openDatapackCompiler(packRoot);
try {
if (aliasContext.retainedContract() != null) {
data.bindGenerationRegistryContract(aliasContext.retainedContract());
}
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
String[] possibleKeys = loader.getPossibleKeys();
if (possibleKeys == null || possibleKeys.length == 0) {
@@ -199,14 +211,16 @@ public final class IrisDatapackCompiler {
}
collectRegistryRequirements(
requirements,
installedBiomeKeys,
biomeTags,
dimension,
fixer);
fixer,
aliasContext);
}
} finally {
data.close();
}
}
addBiomeTagRequirements(requirements, biomeTags);
return Map.copyOf(requirements);
}
@@ -216,23 +230,35 @@ public final class IrisDatapackCompiler {
) throws IOException {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer");
IrisData data = Objects.requireNonNull(
requiredDimension.getLoader(),
"Iris dimension loader"
);
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
Map<String, Set<String>> biomeTags = new TreeMap<>();
BiomeAliasContext aliasContext = BiomeAliasContext.contentAddressedOnly();
collectRegistryRequirements(
requirements,
new LinkedHashSet<>(),
biomeTags,
requiredDimension,
requiredFixer);
requiredFixer,
aliasContext);
collectDimensionStackRegistryRequirements(
requirements,
biomeTags,
requiredDimension,
requiredFixer);
requiredFixer,
aliasContext);
addBiomeTagRequirements(requirements, biomeTags);
return Map.copyOf(requirements);
}
private static void collectDimensionStackRegistryRequirements(
Map<String, String> requirements,
Map<String, Set<String>> biomeTags,
IrisDimension hostDimension,
IDataFixer fixer
IDataFixer fixer,
BiomeAliasContext aliasContext
) throws IOException {
if (!hostDimension.hasDimensionStack()) {
return;
@@ -255,21 +281,31 @@ public final class IrisDatapackCompiler {
}
collectRegistryRequirements(
requirements,
new LinkedHashSet<>(),
biomeTags,
sourceDimension,
fixer);
fixer,
aliasContext);
}
}
private static void collectRegistryRequirements(
Map<String, String> requirements,
Set<String> installedBiomeKeys,
Map<String, Set<String>> biomeTags,
IrisDimension dimension,
IDataFixer fixer
) {
requirements.put(
"dimension_type/iris:" + dimension.getDimensionTypeKey(),
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
IDataFixer fixer,
BiomeAliasContext aliasContext
) throws IOException {
String dimensionTypeResourceKey = "iris:" + dimension.getDimensionTypeKey();
String dimensionTypeSource = aliasContext.generatedSource(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
dimensionTypeResourceKey,
dimension.getDimensionType().toJson(fixer),
fixer
);
putRegistryRequirement(
requirements,
"dimension_type/" + dimensionTypeResourceKey,
fingerprintContent(dimensionTypeSource));
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
@@ -281,13 +317,25 @@ public final class IrisDatapackCompiler {
if (customBiome == null) {
continue;
}
String biomeKey = dimension.getCustomBiomeKey(customBiome.getId());
if (!installedBiomeKeys.add(biomeKey)) {
continue;
String json = customBiome.generateJson(fixer, contentGate);
String currentPhysicalKey = GenerationRegistryContractFactory.customBiomeResourceKey(
dimension.getLoadKey(),
customBiome,
contentGate
);
String physicalKey = aliasContext.physicalResourceKey(
dimension,
biome,
customBiome,
currentPhysicalKey
);
LinkedHashSet<String> resourceKeys = new LinkedHashSet<>();
resourceKeys.add(physicalKey);
Collection<String> aliases = aliasContext.aliases(dimension, biome, customBiome, physicalKey);
if (!aliases.isEmpty() && physicalKey.equals(dimension.getLoader()
.customBiomeResourceKey(dimension, customBiome.getId()))) {
resourceKeys.addAll(aliases);
}
requirements.put(
"worldgen/biome/" + biomeKey,
fingerprintContent(customBiome.generateJson(fixer, contentGate)));
TreeSet<String> tags = new TreeSet<>();
for (String tag : customBiome.getEffectiveTags(derivativeKey)) {
String normalizedTag = normalizeRegistryKey(tag);
@@ -295,13 +343,48 @@ public final class IrisDatapackCompiler {
tags.add(normalizedTag);
}
}
requirements.put(
"worldgen/biome_tags/" + biomeKey,
fingerprintContent(String.join("\n", tags)));
for (String resourceKey : resourceKeys) {
String emittedJson = aliasContext.generatedSource(
GenerationRegistryContractFactory.BIOME_REGISTRY,
resourceKey,
json,
fixer
);
putRegistryRequirement(
requirements,
"worldgen/biome/" + resourceKey,
fingerprintContent(emittedJson)
);
biomeTags.computeIfAbsent(resourceKey, ignored -> new TreeSet<>()).addAll(tags);
}
}
}
}
private static void addBiomeTagRequirements(
Map<String, String> requirements,
Map<String, Set<String>> biomeTags
) throws IOException {
for (Map.Entry<String, Set<String>> entry : biomeTags.entrySet()) {
putRegistryRequirement(
requirements,
"worldgen/biome_tags/" + entry.getKey(),
fingerprintContent(String.join("\n", entry.getValue()))
);
}
}
private static void putRegistryRequirement(
Map<String, String> requirements,
String resourceKey,
String contentFingerprint
) throws IOException {
String previous = requirements.putIfAbsent(resourceKey, contentFingerprint);
if (previous != null && !previous.equals(contentFingerprint)) {
throw new IOException("Conflicting Iris datapack registry resource '" + resourceKey + "'.");
}
}
private static String normalizeRegistryKey(String key) {
if (key == null || key.isBlank()) {
return null;
@@ -359,8 +442,12 @@ public final class IrisDatapackCompiler {
if (!hasDimensions(packRoot.toPath())) {
continue;
}
BiomeAliasContext aliasContext = biomeAliasContext(packRoot.toPath());
IrisData data = IrisData.openDatapackCompiler(packRoot);
try {
if (aliasContext.retainedContract() != null) {
data.bindGenerationRegistryContract(aliasContext.retainedContract());
}
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
String[] possibleKeys = loader.getPossibleKeys();
if (possibleKeys == null || possibleKeys.length == 0) {
@@ -379,11 +466,22 @@ public final class IrisDatapackCompiler {
.add(new DimensionCandidate(
dimension,
packRoot.toPath().toAbsolutePath().normalize(),
dimension.getDimensionType().toJson(fixer)
aliasContext.generatedSource(
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
"iris:" + dimension.getDimensionTypeKey(),
dimension.getDimensionType().toJson(fixer),
fixer
)
));
KSet<String> seenBiomes = biomes.computeIfAbsent(dimension.getLoadKey(), ignored -> new KSet<>());
dimension.installBiomes(fixer, dimension::getLoader, datapackRoots, seenBiomes);
dimension.installDimensionType(fixer, datapackRoots);
dimension.installBiomes(
fixer,
dimension::getLoader,
datapackRoots,
seenBiomes,
aliasContext
);
dimension.installDimensionType(fixer, datapackRoots, aliasContext);
installedDimensions++;
dimensionCount++;
}
@@ -501,6 +599,23 @@ public final class IrisDatapackCompiler {
}
}
private static void collectCanonicalWorldPackRoot(
Path worldRoot,
Map<Path, File> roots,
boolean validateWholePack
) throws IOException {
if (Files.isSymbolicLink(worldRoot)
|| !Files.isDirectory(worldRoot, LinkOption.NOFOLLOW_LINKS)) {
return;
}
List<Path> candidates = new ArrayList<>();
collectWorldRootPackRoots(worldRoot, candidates);
candidates.sort(Comparator.comparing(Path::toString));
for (Path candidate : candidates) {
addPackRoot(candidate, roots, validateWholePack);
}
}
private static void collectNamespaceWorldPackRoots(
Path namespace,
List<Path> candidates
@@ -510,18 +625,12 @@ public final class IrisDatapackCompiler {
public FileVisitResult preVisitDirectory(
Path directory,
BasicFileAttributes attributes
) {
) throws IOException {
if (!directory.equals(namespace)
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
Path irisRoot = directory.resolve("iris");
Path candidate = irisRoot.resolve("pack");
if (!Files.isSymbolicLink(irisRoot)
&& !Files.isSymbolicLink(candidate)
&& Files.isDirectory(candidate, LinkOption.NOFOLLOW_LINKS)
&& hasDimensions(candidate)) {
candidates.add(candidate);
if (collectWorldRootPackRoots(directory, candidates)) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
@@ -529,6 +638,35 @@ public final class IrisDatapackCompiler {
});
}
private static boolean collectWorldRootPackRoots(
Path worldRoot,
List<Path> candidates
) throws IOException {
Path irisRoot = worldRoot.resolve("iris");
Path generationRoot = irisRoot.resolve("generation");
if (!Files.isSymbolicLink(irisRoot)
&& (Files.exists(generationRoot, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(generationRoot))) {
GenerationHistory history = GenerationHistory.open(worldRoot);
for (GenerationEpoch epoch : history.manifest().epochs()) {
Path retainedPack = history.paths().packRoot(epoch.epochId());
if (hasDimensions(retainedPack)) {
candidates.add(retainedPack);
}
}
return true;
}
Path legacyPack = irisRoot.resolve("pack");
if (!Files.isSymbolicLink(irisRoot)
&& !Files.isSymbolicLink(legacyPack)
&& Files.isDirectory(legacyPack, LinkOption.NOFOLLOW_LINKS)
&& hasDimensions(legacyPack)) {
candidates.add(legacyPack);
return true;
}
return false;
}
private static List<Path> visibleDirectories(Path root) throws IOException {
try (Stream<Path> entries = Files.list(root)) {
return entries
@@ -562,6 +700,43 @@ public final class IrisDatapackCompiler {
roots.putIfAbsent(identity, normalized.toFile());
}
private static BiomeAliasContext biomeAliasContext(Path packRoot) throws IOException {
Path normalizedRoot = packRoot.toAbsolutePath().normalize();
Path rootParent = normalizedRoot.getParent();
if (rootParent == null || !"pack".equals(normalizedRoot.getFileName().toString())) {
return BiomeAliasContext.contentAddressedOnly();
}
if ("iris".equals(rootParent.getFileName().toString())) {
return BiomeAliasContext.legacyPack();
}
Path epochsRoot = rootParent.getParent();
Path generationRoot = epochsRoot == null ? null : epochsRoot.getParent();
Path irisRoot = generationRoot == null ? null : generationRoot.getParent();
Path dimensionRoot = irisRoot == null ? null : irisRoot.getParent();
if (epochsRoot == null
|| generationRoot == null
|| irisRoot == null
|| dimensionRoot == null
|| !"epochs".equals(epochsRoot.getFileName().toString())
|| !"generation".equals(generationRoot.getFileName().toString())
|| !"iris".equals(irisRoot.getFileName().toString())) {
return BiomeAliasContext.contentAddressedOnly();
}
String epochId = rootParent.getFileName().toString();
GenerationHistory history = GenerationHistory.open(dimensionRoot);
GenerationEpoch epoch = history.manifest().epoch(epochId).orElseThrow(
() -> new IOException("Retained Iris pack is not referenced by its generation history: "
+ normalizedRoot)
);
Path expectedRoot = history.paths().packRoot(epochId).toAbsolutePath().normalize();
if (!expectedRoot.equals(normalizedRoot)) {
throw new IOException("Retained Iris pack does not match its generation history: " + normalizedRoot);
}
return BiomeAliasContext.retained(epoch.registryContract());
}
private static boolean hasDimensions(Path root) {
Path dimensions = root.resolve("dimensions");
if (Files.isSymbolicLink(dimensions)
@@ -704,6 +879,75 @@ public final class IrisDatapackCompiler {
) {
}
private record BiomeAliasContext(
boolean retainAllLegacyAliases,
GenerationRegistryContract retainedContract
) implements IrisCustomBiomeAliasResolver {
private static BiomeAliasContext contentAddressedOnly() {
return new BiomeAliasContext(false, null);
}
private static BiomeAliasContext legacyPack() {
return new BiomeAliasContext(true, null);
}
private static BiomeAliasContext retained(GenerationRegistryContract registryContract) {
return new BiomeAliasContext(
false,
Objects.requireNonNull(registryContract, "registryContract")
);
}
@Override
public String physicalResourceKey(
IrisDimension dimension,
IrisBiome biome,
IrisBiomeCustom customBiome,
String currentPhysicalResourceKey
) throws IOException {
if (retainedContract == null) {
return currentPhysicalResourceKey;
}
return dimension.getLoader().customBiomeResourceKey(dimension, customBiome);
}
@Override
public Collection<String> aliases(
IrisDimension dimension,
IrisBiome biome,
IrisBiomeCustom customBiome,
String physicalResourceKey
) {
String alias = dimension.getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT);
if (retainAllLegacyAliases) {
return List.of(alias);
}
if (retainedContract == null) {
return List.of();
}
PhysicalResourceKey key = new PhysicalResourceKey(
GenerationRegistryContractFactory.BIOME_REGISTRY,
alias
);
return retainedContract.definitions().containsKey(key) ? List.of(alias) : List.of();
}
@Override
public String generatedSource(
String registryKey,
String resourceKey,
String currentSource,
IDataFixer fixer
) throws IOException {
if (retainedContract == null) {
return currentSource;
}
PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey);
return GenerationRegistryContractFactory.renderGeneratedSource(retainedContract, key, fixer);
}
}
public static final class DimensionHeight {
private final IDataFixer fixer;
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
@@ -349,6 +349,11 @@ public class IrisSettings {
public static class IrisSettingsGenerator {
public String defaultWorldType = "overworld";
public boolean preventLeafDecay = true;
public int generationTransitionWidthBlocks = 256;
public int getGenerationTransitionWidthBlocks() {
return Math.max(16, Math.min(generationTransitionWidthBlocks, 8_192));
}
}
@Data
@@ -1,5 +1,6 @@
package art.arcane.iris.core;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
@@ -404,10 +405,6 @@ public final class IrisWorldStorage {
return Optional.ofNullable(NamespacedKey.fromString(namespace + ":" + key));
}
public static File packRoot(NamespacedKey key) {
return new File(dimensionRoot(key), "iris/pack");
}
public static File requireFrozenDimensionRoot(
File worldContainer,
File levelRoot,
@@ -485,26 +482,28 @@ public final class IrisWorldStorage {
return configuredRoot;
}
public static File requireFrozenPackRoot(File dimensionRoot) {
public static File requireActiveGenerationPackRoot(File dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toPath()
.toAbsolutePath()
.normalize();
Path irisRoot = root.resolve("iris");
Path packRoot = irisRoot.resolve("pack");
for (Path path : new Path[]{root, irisRoot, packRoot}) {
if (Files.isSymbolicLink(path)) {
throw new IllegalStateException("Frozen Iris pack path contains a symbolic link: " + path);
}
if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)
&& !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Frozen Iris pack path is not a directory: " + path);
}
try {
return GenerationHistory.open(root).activePackRoot().toFile();
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
}
if (!Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalStateException("Frozen Iris pack snapshot is missing: " + packRoot);
}
public static File requireActiveGenerationPackRoot(File dimensionRoot, long expectedWorldSeed) {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toPath()
.toAbsolutePath()
.normalize();
try {
return GenerationHistory.open(root, expectedWorldSeed).activePackRoot().toFile();
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
}
return packRoot.toFile();
}
private static boolean isExistingSafeDimensionRoot(Path storageRoot, Path dimensionRoot) {
@@ -367,7 +367,7 @@ public class IrisWorlds {
configuredWorldName,
worldKey
);
return dimensionRoot.map(IrisWorldStorage::requireFrozenPackRoot);
return dimensionRoot.map(IrisWorldStorage::requireActiveGenerationPackRoot);
}
private static void warnUnusableStorage(String worldIdentity, IllegalStateException failure) {
@@ -376,7 +376,7 @@ public class IrisWorlds {
}
IrisLogging.error("Iris world %s has unusable world storage and is excluded: %s",
worldIdentity, failure.getMessage());
IrisLogging.error("Restore its iris/pack snapshot, or drop the world with /iris remove world=%s delete=true.",
IrisLogging.error("Restore its iris/generation history, or drop the world with /iris remove world=%s delete=true.",
worldIdentity);
}
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
keys.add(dimension.getCustomBiomeKey(j.getId()));
keys.add(dimension.getLoader().customBiomeResourceKey(dimension, j));
}
}
}
@@ -3,6 +3,7 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.WorldSlotKey;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.history.GenerationHistoryPaths;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
@@ -229,8 +230,20 @@ public final class BukkitWorldConfiguration {
}
Path resolvedRoot = dimensionRoot.toPath().toAbsolutePath().normalize();
try {
IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot);
} catch (IllegalStateException unusablePack) {
GenerationHistoryPaths historyPaths = GenerationHistoryPaths.forDimension(resolvedRoot);
boolean generationHistoryExists = Files.exists(
historyPaths.generationRoot(),
LinkOption.NOFOLLOW_LINKS
) || Files.isSymbolicLink(historyPaths.generationRoot());
boolean adoptableLegacyPack = !generationHistoryExists
&& Files.isDirectory(historyPaths.legacyPackRoot(), LinkOption.NOFOLLOW_LINKS)
&& !Files.isSymbolicLink(historyPaths.legacyPackRoot());
if (adoptableLegacyPack) {
return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot,
IrisWorldStorageState.PRESENT, null);
}
// Only a folder that owns something can be corrupted by a vanilla generator writing into it.
return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot,
hasWorldData(resolvedRoot)
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.core.pack.PackDirectoryResolver;
import java.io.IOException;
@@ -94,7 +95,7 @@ public final class WorldReplacementFilesystem {
State state = inspect(requiredPaths);
if (state.stagePresent()) {
requireSafeTree(requiredPaths.stage(), "replacement stage");
requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.stage()), expectedFingerprint);
if (originalTargetPresent) {
Path retainedWorld = state.targetPresent()
? requiredPaths.target()
@@ -118,7 +119,7 @@ public final class WorldReplacementFilesystem {
throw new IOException("Replacement backup state does not match the original target state.");
}
requireSafeTree(requiredPaths.stage(), "replacement stage");
requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.stage()), expectedFingerprint);
move(requiredPaths.stage(), requiredPaths.target());
state = inspect(requiredPaths);
}
@@ -130,11 +131,11 @@ public final class WorldReplacementFilesystem {
throw new IOException("Replacement publication lost or unexpectedly created its backup.");
}
requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
if (originalTargetPresent) {
preservePaperWorldMetadata(requiredPaths.backup(), requiredPaths.target());
requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
}
}
@@ -224,7 +225,7 @@ public final class WorldReplacementFilesystem {
throw new IOException("Committed replacement storage does not contain one exact target directory.");
}
requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
}
public static void validatePublishedTarget(
@@ -240,7 +241,7 @@ public final class WorldReplacementFilesystem {
throw new IOException("Published replacement storage does not match its retained-world contract.");
}
requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
}
public static String fingerprintPack(Path packRoot) throws IOException {
@@ -279,6 +280,10 @@ public final class WorldReplacementFilesystem {
return HexFormat.of().formatHex(digest.digest());
}
private static Path activePackRoot(Path worldRoot) throws IOException {
return GenerationHistory.open(worldRoot).activePackRoot();
}
private static FingerprintEntry fingerprintEntry(Path root, Path path) {
Path relative = root.relativize(path);
return new FingerprintEntry(path, relative, relative.toString());
@@ -39,9 +39,14 @@ import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.core.structure.authoring.StructureRecoveryResult;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
@@ -74,6 +79,7 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.context.IrisContext;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.Nullable;
@@ -87,7 +93,9 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -137,6 +145,17 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient List<Engine> engines = new ArrayList<>();
private final transient Map<IrisDimension, Map<String, String>> dimensionCustomBiomeResourceKeys =
Collections.synchronizedMap(new IdentityHashMap<>());
private transient volatile String generationPackFingerprint;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private transient volatile GenerationRegistryContract generationRegistryContract;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
private transient volatile Engine soleEngine;
private IrisData(File dataFolder) {
@@ -170,6 +189,90 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return Optional.ofNullable(dataLoaders.get(dataFolder));
}
public String customBiomeResourceKey(IrisDimension dimension, IrisBiomeCustom customBiome) {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
if (resolver != null) {
try {
return resolver.resolve(
requiredDimension.getLoadKey(),
requiredBiome
);
} catch (IOException failure) {
throw new IllegalStateException("Immutable Iris epoch has no registry mapping for custom biome '"
+ requiredDimension.getLoadKey() + ":" + requiredBiome.getId() + "'.", failure);
}
}
return GenerationRegistryContractFactory.customBiomeResourceKey(
requiredDimension.getLoadKey(),
requiredBiome,
getContentGate()
);
}
public String customBiomeResourceKey(IrisDimension dimension, String customBiomeId) {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
String logicalKey = requiredDimension.getLoadKey().toLowerCase(Locale.ROOT)
+ ":" + Objects.requireNonNull(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT);
String physicalKey = customBiomeResourceKeys(requiredDimension).get(logicalKey);
if (physicalKey == null) {
throw new IllegalArgumentException("Unknown custom biome '" + logicalKey + "'.");
}
return physicalKey;
}
public String physicalBiomeResourceKey(IrisDimension dimension, String resourceKey) {
if (resourceKey == null || resourceKey.isBlank()) {
return resourceKey;
}
String normalizedKey = resourceKey.trim().toLowerCase(Locale.ROOT);
if (!normalizedKey.contains(":")) {
normalizedKey = "minecraft:" + normalizedKey;
}
return customBiomeResourceKeys(Objects.requireNonNull(dimension, "dimension"))
.getOrDefault(normalizedKey, normalizedKey);
}
public String generationPackFingerprint() {
String fingerprint = generationPackFingerprint;
if (fingerprint != null) {
return fingerprint;
}
synchronized (this) {
fingerprint = generationPackFingerprint;
if (fingerprint != null) {
return fingerprint;
}
try {
fingerprint = GenerationPackFingerprint.compute(
dataFolder.toPath(),
GenerationPackFingerprint.CURRENT_VERSION
);
} catch (IOException failure) {
throw new IllegalStateException("Unable to fingerprint Iris generation pack at "
+ dataFolder + ".", failure);
}
generationPackFingerprint = fingerprint;
return fingerprint;
}
}
public synchronized void bindGenerationRegistryContract(GenerationRegistryContract contract) {
GenerationRegistryContract required = Objects.requireNonNull(contract, "contract");
if (generationRegistryContract != null && !generationRegistryContract.equals(required)) {
throw new IllegalStateException("Iris data is already bound to another generation registry contract.");
}
generationRegistryContract = required;
customBiomeResourceResolver = new GenerationRegistryContractFactory.CustomBiomeResourceResolver(
required,
PlatformGenerationRegistry::contentAddressedCustomBiomeResourceKey
);
synchronized (dimensionCustomBiomeResourceKeys) {
dimensionCustomBiomeResourceKeys.clear();
}
}
public static boolean invalidateLoadedStructureResources(File dataFolder) {
Path requested = dataFolderIdentity(Objects.requireNonNull(
dataFolder,
@@ -273,7 +376,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
try {
if (nearest != null) {
T t = nearest.load(type, key, false);
if (t != null) {
if (t != null || nearest.generationRegistryContract != null) {
return t;
}
}
@@ -516,6 +619,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
StructureGraphCatalog.invalidate(this);
IrisObjectScale.invalidate(this);
contentGate = null;
generationPackFingerprint = null;
synchronized (dimensionCustomBiomeResourceKeys) {
dimensionCustomBiomeResourceKeys.clear();
}
compatReport.clear();
closed = false;
possibleSnippets = new KMap<>();
@@ -831,4 +938,34 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
}
IrisLogging.debug("Loaded Prefetch Cache to reduce generation disk use.");
}
private Map<String, String> customBiomeResourceKeys(IrisDimension dimension) {
synchronized (dimensionCustomBiomeResourceKeys) {
Map<String, String> cached = dimensionCustomBiomeResourceKeys.get(dimension);
if (cached != null) {
return cached;
}
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
Map<String, String> resolved = new LinkedHashMap<>();
List<IrisBiome> registryBiomes = new ArrayList<>(dimension.getAllBiomes(() -> this));
registryBiomes.addAll(dimension.getReachableBiomes(() -> this));
for (IrisBiome biome : registryBiomes) {
if (biome == null || !biome.isCustom()) {
continue;
}
for (IrisBiomeCustom customBiome : biome.getCustomDerivitives()) {
if (customBiome == null || customBiome.getId() == null || customBiome.getId().isBlank()) {
continue;
}
String logicalKey = namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT);
String physicalKey = customBiomeResourceKey(dimension, customBiome);
resolved.merge(logicalKey, physicalKey,
(first, second) -> first.compareTo(second) <= 0 ? first : second);
}
}
Map<String, String> immutable = Map.copyOf(resolved);
dimensionCustomBiomeResourceKeys.put(dimension, immutable);
return immutable;
}
}
}
@@ -172,15 +172,13 @@ public final class BukkitRuntimeMessages {
+ " (dimension blockFallbacks, block backup)."
);
public static final LinesKey COMMAND_DEVELOPER_UPDATE_WORLD_WARNING = LinesKey.of(
"iris.bukkit.runtime.commanddeveloper.update_world_warning",
C.RED + "You should always make a backup before using this",
C.YELLOW + "Issues caused by this can be, but are not limited to:",
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
C.YELLOW + " - Structures not spawning again when regenerating",
C.YELLOW + " - Caves not lining up",
C.YELLOW + " - Terrain layers not lining up",
C.RED + "Now that you are aware of the risks, and have made a back-up:",
"iris.bukkit.runtime.commanddeveloper.stage_world_generation_update_warning",
C.RED + "Back up the complete world before staging this update.",
C.YELLOW + "Existing chunks will not be regenerated or rewritten.",
C.YELLOW + "New chunks use the staged pack after restart and blend across the frozen boundary.",
C.YELLOW + "The world seed, height, environment, and dimension type cannot change.",
C.YELLOW + "Every backup must retain the complete iris/generation directory.",
C.RED + "To stage the update and request a clean restart:",
C.RED + "/iris developer update-world {world} {pack} confirm=true"
);
public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW = TextKey.of(
@@ -60,9 +60,9 @@ public final class DirectorCommandMessages {
"iris.director.commanddeveloper.param.center_chunk_z",
"Center chunk Z"
);
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE = TextKey.of(
"iris.director.commanddeveloper.director.update_pack_world_unsafe",
"Update the pack of a world (UNSAFE!)"
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_STAGE_WORLD_GENERATION_UPDATE = TextKey.of(
"iris.director.commanddeveloper.director.stage_world_generation_update",
"Stage a world generation update"
);
public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_UPDATE = TextKey.of(
"iris.director.commanddeveloper.param.world_update",
@@ -889,7 +889,7 @@ public final class DirectorCommandMessages {
COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER,
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X,
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z,
COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE,
COMMAND_DEVELOPER_DIRECTOR_STAGE_WORLD_GENERATION_UPDATE,
COMMAND_DEVELOPER_PARAM_WORLD_UPDATE,
COMMAND_DEVELOPER_PARAM_PACK_INSTALL_INTO_WORLD,
COMMAND_DEVELOPER_PARAM_MAKE_SURE_MAKE_BACKUP_READ_WARNINGS_FIRST,
@@ -31,6 +31,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureVolume;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList;
@@ -57,9 +58,14 @@ import java.awt.Color;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public interface INMSBinding {
default PlatformGenerationRegistry generationRegistry() {
throw new UnsupportedOperationException(
"The active NMS binding does not expose generation registry definitions."
);
}
boolean hasTile(Material material);
boolean hasTile(Location l);
@@ -273,8 +279,18 @@ public interface INMSBinding {
default void uninjectBukkit() {
}
default boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
return true;
default void deferPluginClassLoaderClose() {
}
default void releasePluginClassLoaderClose() {
}
default boolean isServerStopping() {
return false;
}
default ServerShutdownBoundary createServerShutdownBoundary() {
return new ServerShutdownBoundary(() -> true, Thread.currentThread());
}
KMap<Material, List<BlockProperty>> getBlockProperties();
@@ -7,33 +7,32 @@ import java.util.function.BooleanSupplier;
public final class ServerShutdownBoundary {
private static final long MAX_JOIN_SLICE_MILLIS = 1000L;
private ServerShutdownBoundary() {
private final BooleanSupplier boundaryReached;
private final Thread serverThread;
public ServerShutdownBoundary(BooleanSupplier boundaryReached, Thread serverThread) {
this.boundaryReached = Objects.requireNonNull(boundaryReached, "Server shutdown boundary");
this.serverThread = Objects.requireNonNull(serverThread, "Server thread");
this.boundaryReached.getAsBoolean();
}
public static boolean await(
BooleanSupplier boundaryReached,
Thread serverThread,
long timeout,
TimeUnit unit
) {
BooleanSupplier reached = Objects.requireNonNull(boundaryReached, "Server shutdown boundary");
Thread activeServerThread = Objects.requireNonNull(serverThread, "Server thread");
public boolean await(long timeout, TimeUnit unit) {
TimeUnit activeUnit = Objects.requireNonNull(unit, "Server shutdown timeout unit");
if (reached.getAsBoolean()) {
if (boundaryReached.getAsBoolean()) {
return true;
}
if (activeServerThread == Thread.currentThread()) {
if (serverThread == Thread.currentThread()) {
return false;
}
long timeoutNanos = Math.max(0L, activeUnit.toNanos(timeout));
long started = System.nanoTime();
boolean interrupted = false;
while (!reached.getAsBoolean()) {
while (!boundaryReached.getAsBoolean()) {
long remaining = timeoutNanos - (System.nanoTime() - started);
if (remaining <= 0L || !activeServerThread.isAlive()) {
if (remaining <= 0L || !serverThread.isAlive()) {
restoreInterrupt(interrupted);
return reached.getAsBoolean();
return boundaryReached.getAsBoolean();
}
long joinMillis = Math.max(
@@ -41,7 +40,7 @@ public final class ServerShutdownBoundary {
Math.min(MAX_JOIN_SLICE_MILLIS, TimeUnit.NANOSECONDS.toMillis(remaining))
);
try {
activeServerThread.join(joinMillis);
serverThread.join(joinMillis);
} catch (InterruptedException e) {
interrupted = true;
}
@@ -7,6 +7,7 @@ import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.message.Message;
import java.util.List;
@@ -21,14 +22,15 @@ public class LogFilterSVC implements IrisService, Filter {
// so a mutable static here was both a CME hazard and grew by three entries per enable.
private static final List<String> FILTERS = List.of(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID);
private boolean installed = false;
private LoggerConfig installedLogger;
public void onEnable() {
if (installed) {
if (installedLogger != null) {
return;
}
((Logger) LogManager.getRootLogger()).addFilter(this);
installed = true;
LoggerConfig logger = ((Logger) LogManager.getRootLogger()).get();
logger.addFilter(this);
installedLogger = logger;
}
public void initialize() {
@@ -41,11 +43,12 @@ public class LogFilterSVC implements IrisService, Filter {
}
public void onDisable() {
if (!installed) {
LoggerConfig logger = installedLogger;
if (logger == null) {
return;
}
((Logger) LogManager.getRootLogger()).get().removeFilter(this);
installed = false;
logger.removeFilter(this);
installedLogger = null;
}
public boolean isStarted() {
@@ -69,10 +69,27 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
public void dereference() {
IrisData.dereference();
threads.removeIf((i) -> !i.isAlive());
services.removeIf(ExecutorService::isShutdown);
services.removeIf(ExecutorService::isTerminated);
updateCaches();
}
public boolean hasActiveResources() {
if (dereferencer != null && dereferencer.isAlive()) {
return true;
}
for (Thread thread : threads) {
if (thread.isAlive()) {
return true;
}
}
for (ExecutorService executor : services) {
if (!executor.isTerminated()) {
return true;
}
}
return false;
}
@Override
public void onEnable() {
/*
@@ -19,6 +19,7 @@
package art.arcane.iris.core.service;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisStartupValidation;
@@ -46,6 +47,14 @@ import art.arcane.iris.core.runtime.WorldDeletionQueue;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.history.GenerationActivation;
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.GenerationHistoryPaths;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
@@ -166,18 +175,197 @@ public class StudioSVC implements IrisService {
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
}
public IrisDimension installIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) {
return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), false);
public IrisDimension installIntoWorld(
VolmitSender sender,
IrisDimension dimension,
File dimensionRoot,
long worldSeed
) {
return publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, false);
}
public IrisDimension replaceIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) {
return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), true);
public IrisDimension replaceIntoWorld(
VolmitSender sender,
IrisDimension dimension,
File dimensionRoot,
long worldSeed
) {
return publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, true);
}
public IrisDimension installIntoTransientWorld(
VolmitSender sender,
IrisDimension dimension,
File dimensionRoot
) {
return installIntoDirectory(sender, dimension, new File(dimensionRoot, "iris/pack"), false);
}
public IrisDimension replaceIntoPackDirectory(VolmitSender sender, IrisDimension dimension, File folder) {
return installIntoDirectory(sender, dimension, folder, true);
}
private IrisDimension publishGenerationHistory(
VolmitSender sender,
IrisDimension dimension,
File dimensionRoot,
long worldSeed,
boolean stageUpdate
) {
if (J.isPrimaryThread()) {
sender.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD
));
return null;
}
String dimensionKey = dimension.getLoadKey();
try {
Path source = resolveSafePackSource(dimension.getLoader().getDataFolder());
GenerationCandidate candidate = captureGenerationCandidate(source, dimensionKey);
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toPath()
.toAbsolutePath()
.normalize();
if (!stageUpdate && !Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
Path parent = Objects.requireNonNull(root.getParent(), "dimensionRoot parent");
Files.createDirectories(parent);
Files.createDirectory(root);
}
GenerationHistory history;
long previousActivationId = 0L;
boolean restartRequired = false;
if (stageUpdate) {
history = openOrAdoptGenerationHistory(root, dimensionKey, worldSeed);
previousActivationId = history.activeActivation().activationId();
GenerationActivation activation = history.stageUpdate(
source,
candidate.packFingerprint(),
candidate.dimensionContract(),
candidate.registryContract(),
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
);
restartRequired = activation.activationId() != previousActivationId;
} else {
history = GenerationHistory.create(
root,
source,
candidate.packFingerprint(),
worldSeed,
candidate.dimensionContract(),
candidate.registryContract()
);
}
Path installedPack = stageUpdate && history.pendingActivation().isPresent()
? history.packRoot(history.pendingActivation().orElseThrow().activationId())
: history.activePackRoot();
IrisDimension installed = loadInstalledDimension(installedPack, dimensionKey);
if (restartRequired) {
ServerConfigurator.restart("An Iris generation epoch update is pending activation.");
}
return installed;
} catch (Throwable failure) {
IrisLogging.reportError("Failed to publish generation history for dimension '"
+ dimensionKey + "' into " + dimensionRoot.getPath(), failure);
sender.sendMessage(IrisLanguage.text(
BukkitRuntimeMessages.STUDIO_S_V_C_PACK_INSTALL_FAILED,
MessageArgument.untrusted("dimension", dimensionKey),
MessageArgument.untrusted("error", errorDetail(failure))
));
return null;
}
}
private static GenerationHistory openOrAdoptGenerationHistory(
Path dimensionRoot,
String dimensionKey,
long worldSeed
) throws IOException {
Optional<GenerationHistory> current = GenerationHistory.openIfPresent(dimensionRoot, worldSeed);
if (current.isPresent()) {
return current.get();
}
Path legacyPack = GenerationHistoryPaths.forDimension(dimensionRoot).legacyPackRoot();
PackValidationResult validation = validatePublishedPack(legacyPack);
if (!validation.isLoadable()) {
throw new BrokenPackException(legacyPack.toString(), validation.getBlockingErrors());
}
GenerationCandidate legacy = captureGenerationCandidate(
legacyPack,
dimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
);
return GenerationHistory.adoptLegacyPack(
dimensionRoot,
legacy.packFingerprint(),
worldSeed,
legacy.dimensionContract(),
legacy.registryContract()
);
}
private static GenerationCandidate captureGenerationCandidate(Path packRoot, String dimensionKey)
throws IOException {
return captureGenerationCandidate(
packRoot,
dimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.CONTENT_ADDRESSED_ONLY
);
}
private static GenerationCandidate captureGenerationCandidate(
Path packRoot,
String dimensionKey,
GenerationRegistryContractFactory.CustomBiomeAliasPolicy aliasPolicy
) throws IOException {
String fingerprint = GenerationPackFingerprint.compute(
packRoot,
GenerationPackFingerprint.CURRENT_VERSION
);
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
if (dimension == null) {
throw new IOException("Generation pack does not contain a loadable dimension '"
+ dimensionKey + "'.");
}
String dimensionTypeKey = IrisPlatforms.get()
.registries()
.generationRegistry()
.dimensionTypeResourceKey(
data.getDataFolder().getName(),
dimension.getLoadKey(),
dimension.getDimensionTypeKey()
);
GenerationEpoch.DimensionContract dimensionContract = GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
dimensionTypeKey
);
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
data,
dimension,
fingerprint,
aliasPolicy
);
return new GenerationCandidate(fingerprint, dimensionContract, registryContract);
} finally {
data.close();
}
}
private static IrisDimension loadInstalledDimension(Path packRoot, String dimensionKey) throws IOException {
PackValidationResult validation = validatePublishedPack(packRoot);
if (!validation.isLoadable()) {
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
}
IrisDimension installed = IrisData.get(packRoot.toFile()).getDimensionLoader().load(dimensionKey, false);
if (installed == null) {
throw new IOException("Immutable generation pack does not contain dimension '"
+ dimensionKey + "'.");
}
return installed;
}
private IrisDimension installIntoDirectory(
VolmitSender sender,
IrisDimension dimension,
@@ -1534,6 +1722,13 @@ public class StudioSVC implements IrisService {
RESTART
}
private record GenerationCandidate(
String packFingerprint,
GenerationEpoch.DimensionContract dimensionContract,
GenerationRegistryContract registryContract
) {
}
public IrisProject getActiveProject() {
return activeProject;
}
@@ -263,8 +263,10 @@ public class IrisCreator {
IrisDimension installedDimension = resolvedDimension;
if (!studio() || benchmark) {
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
installedDimension = IrisServices.get(StudioSVC.class)
.installIntoWorld(sender, resolvedDimension, dimensionRoot);
StudioSVC studioService = IrisServices.get(StudioSVC.class);
installedDimension = benchmark
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
if (installedDimension == null) {
throw new IrisException("Failed to install dimension pack for " + dimension());
}
@@ -28,6 +28,7 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisWorldStorage;
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.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
@@ -57,7 +58,6 @@ import org.jetbrains.annotations.ApiStatus;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -74,7 +74,6 @@ import art.arcane.volmlib.util.localization.MessageArgument;
public class IrisToolbelt {
@ApiStatus.Internal
public static Map<String, Boolean> toolbeltConfiguration = new HashMap<>();
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
/**
* Finds an installed dimension or returns null.
@@ -582,26 +581,7 @@ public class IrisToolbelt {
}
public static boolean isServerStopping() {
Method method = BUKKIT_IS_STOPPING_METHOD;
if (method != null) {
try {
Object value = method.invoke(null);
if (value instanceof Boolean) {
return (Boolean) value;
}
} catch (Throwable ignored) {
}
}
return !BukkitPlatform.hasPlugin() || !BukkitPlatform.plugin().isEnabled();
}
private static Method resolveBukkitIsStoppingMethod() {
try {
return Bukkit.class.getMethod("isStopping");
} catch (Throwable ignored) {
return null;
}
return INMS.isBound() && INMS.get().isServerStopping();
}
public static void beginWorldMaintenance(World world, String reason) {
@@ -23,6 +23,7 @@ import art.arcane.iris.core.WorldCreatorCompat;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import org.bukkit.NamespacedKey;
@@ -31,6 +32,7 @@ import org.bukkit.WorldCreator;
import org.bukkit.generator.ChunkGenerator;
import java.io.File;
import java.io.IOException;
public class IrisWorldCreator {
private String name;
@@ -100,15 +102,42 @@ public class IrisWorldCreator {
.seed(seed)
.worldFolder(worldFolder)
.build();
ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio
? dim.getLoader().getDataFolder() :
new File(w.worldFolder(), "iris/pack"), dimensionName);
GenerationHistory generationHistory = persistent
? requireGenerationHistory(w.worldFolder(), seed)
: null;
File packRoot = generationHistory != null
? requireActivePack(generationHistory)
: studio
? dim.getLoader().getDataFolder()
: new File(w.worldFolder(), "iris/pack");
ChunkGenerator g = new BukkitChunkGenerator(
w,
studio,
packRoot,
dimensionName,
generationHistory);
return creator.environment(environment)
.generateStructures(true)
.generator(g).seed(seed);
}
private static GenerationHistory requireGenerationHistory(File dimensionRoot, long seed) {
try {
return GenerationHistory.open(dimensionRoot.toPath(), seed);
} catch (IOException failure) {
throw new IllegalStateException("Iris generation history is unusable at " + dimensionRoot + ".", failure);
}
}
private static File requireActivePack(GenerationHistory history) {
try {
return history.activePackRoot().toFile();
} catch (IOException failure) {
throw new IllegalStateException("Iris active generation pack is unusable.", failure);
}
}
private World.Environment findEnvironment() {
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment());
@@ -121,10 +121,10 @@ public final class DimensionTerrainContext implements DataProvider {
engine.getDimension(),
engine.getData(),
engine.getHeight(),
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getUnblendedNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
fluidHeightStream,
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
natural ? complex.getUnblendedNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
complex.getRegionStream(),
complex.getRockStream(),
fluidBlockSampler,
@@ -20,7 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.protocol.IrisProtocolServer;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.EngineTarget;
@@ -53,32 +53,40 @@ final class EngineHotloader {
void hotloadComplex() {
synchronized (engine.lifecycleLock) {
engine.requireRunning("rebuild the biome complex");
requireMutableGenerationRuntime("rebuild the biome complex");
engine.awaitNativeStructureBootstrap("complex hotload");
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previous = engine.runtime;
GenerationRuntime previousGeneration = previous.generation();
IrisComplex nextComplex = null;
try {
engine.sealForTransition("complex hotload", false);
engine.prepareRuntimeHotload();
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
RuntimeAssembly assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(),
previousGeneration.target(),
previousGeneration.mantleStorageDirectory(),
previousGeneration.runtimeKernel(),
previousGeneration.transitionPlan());
engine.runtimeAssembly.set(assembly);
EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
assembly.complex = new IrisComplex(engine);
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
nextComplex = assembly.complex;
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext();
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
next = previous.withComplex(
GenerationRuntime nextGeneration = previousGeneration.withComplex(
assembly.cacheId,
assembly.complex,
assembly.upperContext,
assembly.dimensionStackContext,
biomeMaxes);
next = previous.withGeneration(nextGeneration);
} finally {
engine.runtimeAssembly.remove();
}
Throwable retirementFailure = runCleanup(null, previous.complex()::close);
Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close);
if (retirementFailure != null) {
retirementFailure = runCleanup(retirementFailure, nextComplex::close);
nextComplex = null;
@@ -91,7 +99,7 @@ final class EngineHotloader {
engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission();
} catch (Throwable e) {
if (nextComplex != null && nextComplex != previous.complex()) {
if (nextComplex != null && nextComplex != previousGeneration.complex()) {
Throwable cleanupFailure = runCleanup(null, nextComplex::close);
if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure);
@@ -108,6 +116,7 @@ final class EngineHotloader {
void hotloadSilently() {
synchronized (engine.lifecycleLock) {
engine.requireRunning("hotload");
requireMutableGenerationRuntime("hotload");
engine.awaitNativeStructureBootstrap("hotload");
engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previousRuntime = engine.runtime;
@@ -183,6 +192,13 @@ final class EngineHotloader {
}
}
private void requireMutableGenerationRuntime(String operation) {
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
throw new IllegalStateException("Cannot " + operation
+ " while immutable Iris generation history is attached; stage a world update and restart instead.");
}
}
/**
* Never throws. It is called from the hotload failure handler, where a frame-cap
* {@code IllegalStateException} out of the codec (a long pack key or exception message overruns
@@ -19,48 +19,26 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import java.util.concurrent.CompletableFuture;
import java.util.Objects;
/**
* Immutable snapshot of everything an {@link IrisEngine} publishes as a single unit.
* Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}.
*/
record EngineRuntime(
int cacheId,
EngineTarget target,
IrisComplex complex,
UpperDimensionContext upperContext,
DimensionStackContext dimensionStackContext,
GenerationRuntime generation,
EngineEffects effects,
EngineMode mode,
EngineWorldManager worldManager,
CompletableFuture<Long> hash32,
BiomeMaxes biomeMaxes
EngineWorldManager worldManager
) {
EngineRuntime withComplex(
int nextCacheId,
IrisComplex nextComplex,
UpperDimensionContext nextUpperContext,
DimensionStackContext nextDimensionStackContext,
BiomeMaxes nextBiomeMaxes
) {
return new EngineRuntime(
nextCacheId,
target,
nextComplex,
nextUpperContext,
nextDimensionStackContext,
effects,
mode,
worldManager,
hash32,
nextBiomeMaxes);
EngineRuntime {
Objects.requireNonNull(generation, "generation runtime");
Objects.requireNonNull(effects, "engine effects");
Objects.requireNonNull(worldManager, "engine world manager");
}
record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) {
EngineRuntime withGeneration(GenerationRuntime nextGeneration) {
return new EngineRuntime(nextGeneration, effects, worldManager);
}
}
@@ -20,24 +20,22 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
import art.arcane.iris.engine.modifier.IrisCustomModifier;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.GenerationKernelRegistry;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionMode;
import art.arcane.iris.engine.object.IrisDimensionModeType;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.project.context.IrisContext;
@@ -45,6 +43,9 @@ import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.M;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
@@ -64,22 +65,56 @@ final class EngineRuntimeBuilder {
}
EngineRuntime buildRuntime() {
return buildRuntime(engine.getTarget());
return buildRuntime(
engine.getTarget(),
engine.getInitialMantleStorageDirectory(),
engine.getInitialKernelVersion(),
engine.getInitialTransitionPlan(),
null);
}
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), runtimeTarget);
GenerationRuntime active = requireRuntime("hotload the engine runtime").generation();
return buildRuntime(
runtimeTarget,
active.mantleStorageDirectory(),
active.kernelVersion(),
active.transitionPlan(),
active.mantle());
}
private EngineRuntime buildRuntime(
EngineTarget runtimeTarget,
Path mantleStorageDirectory,
GenerationKernelRegistry.Version kernelVersion,
TransitionGenerationPlan transitionPlan,
EngineMantle transferredMantle
) {
RuntimeAssembly assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(),
runtimeTarget,
mantleStorageDirectory,
selectRuntimeKernel(kernelVersion),
transitionPlan);
engine.runtimeAssembly.set(assembly);
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Engine " + assembly.cacheId);
long started = M.ms();
assembly.complex = new IrisComplex(engine);
if (transferredMantle == null) {
assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory);
assembly.ownsMantle = true;
} else {
assembly.mantle = transferredMantle;
}
IrisLogging.debug("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.dimensionStackContext = buildDimensionStackContext();
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.upperContext = buildUpperContext();
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine);
@@ -89,21 +124,16 @@ final class EngineRuntimeBuilder {
IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms");
assembly.hash32 = new CompletableFuture<>();
started = M.ms();
engine.getMantle().hotload();
assembly.mantle.hotload();
IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.mode = createMode();
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
staticObjects.apply(engine, x, z, blocks));
if (assembly.dimensionStackContext != null) {
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
dimensionStack.actuate(x, z, blocks, multicore, context));
IrisCustomModifier stackCustom = new IrisCustomModifier(engine);
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
stackCustom.modify(x, z, blocks, multicore, context));
}
assembly.mode = assembly.runtimeKernel.createMode(engine);
assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode);
assembly.runtimeKernel.registerDimensionStack(
engine,
assembly.mode,
assembly.dimensionStackContext
);
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
started = M.ms();
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
@@ -112,7 +142,7 @@ final class EngineRuntimeBuilder {
}
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
BiomeMaxes biomeMaxes = computeBiomeMaxes();
return assembly.freeze(biomeMaxes);
return assembly.freezeRuntime(biomeMaxes);
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
if (cleanupFailure != e) {
@@ -124,8 +154,60 @@ final class EngineRuntimeBuilder {
}
}
GenerationRuntime buildDetachedGenerationRuntime(EngineTarget runtimeTarget, Path mantleStorageDirectory) {
return buildDetachedGenerationRuntime(
runtimeTarget,
mantleStorageDirectory,
GenerationKernelRegistry.standard().current(),
null);
}
GenerationRuntime buildDetachedGenerationRuntime(
EngineTarget runtimeTarget,
Path mantleStorageDirectory,
GenerationKernelRegistry.Version kernelVersion,
TransitionGenerationPlan transitionPlan
) {
RuntimeAssembly assembly = new RuntimeAssembly(
RuntimeAssembly.nextRuntimeId(),
runtimeTarget,
mantleStorageDirectory,
selectRuntimeKernel(kernelVersion),
transitionPlan);
engine.runtimeAssembly.set(assembly);
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId);
assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory);
assembly.ownsMantle = true;
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
assembly.hash32 = CompletableFuture.completedFuture(computePackHash(runtimeTarget.getData()));
assembly.mantle.hotload();
assembly.mode = assembly.runtimeKernel.createMode(engine);
assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode);
assembly.runtimeKernel.registerDimensionStack(
engine,
assembly.mode,
assembly.dimensionStackContext
);
return assembly.freezeGeneration(computeBiomeMaxes());
} catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
if (cleanupFailure != e) {
e.addSuppressed(cleanupFailure);
}
throw new IllegalStateException("Failed to build a detached Iris generation runtime.", e);
} finally {
engine.runtimeAssembly.remove();
}
}
void publishRuntime(EngineRuntime next, EngineRuntime previous) {
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(previous, null);
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(
previous,
next.generation().mantle(),
null);
if (retirementFailure != null) {
retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure);
engine.lifecycleState = LifecycleState.FAILED;
@@ -136,7 +218,7 @@ final class EngineRuntimeBuilder {
}
engine.runtime = next;
engine.publishedTarget = next.target();
engine.publishedTarget = next.generation().target();
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
@@ -163,33 +245,11 @@ final class EngineRuntimeBuilder {
throw new IllegalStateException("Failed to start the Iris world manager.", e);
}
scheduleRuntimeTasks(next);
IrisLogging.debug("Engine Setup Complete " + next.cacheId());
IrisLogging.debug("Engine Setup Complete " + next.generation().cacheId());
}
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
try {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
try {
File[] roots = engine.getData().getLoaders()
.values()
.stream()
.map(ResourceLoader::getFolderName)
.map(name -> new File(engine.getData().getDataFolder(), name))
.filter(File::exists)
.filter(File::isDirectory)
.toArray(File[]::new);
engineRuntime.hash32().complete(IO.hashRecursiveMeta(roots));
} catch (Throwable e) {
engineRuntime.hash32().completeExceptionally(e);
throw propagate(e);
}
})) {
throw new IllegalStateException("Iris background task admission closed before pack hashing.");
}
} catch (Throwable e) {
engineRuntime.hash32().completeExceptionally(e);
IrisLogging.reportError(e);
}
schedulePackHash(engineRuntime.generation());
try {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) {
throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh.");
@@ -199,71 +259,34 @@ final class EngineRuntimeBuilder {
}
}
UpperDimensionContext buildUpperContext() {
IrisDimension dim = engine.getDimension();
if (!dim.hasUpperDimension()) {
return null;
}
String upperKey = dim.getUpperDimension();
IrisDimension upperDim = upperKey.equals(dim.getLoadKey())
? dim
: IrisData.loadAnyDimension(upperKey, engine.getData());
if (upperDim != null) {
UpperDimensionContext ctx = UpperDimensionContext.create(engine, upperDim);
IrisLogging.info("Upper dimension enabled: " + upperKey
+ (ctx.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)"));
return ctx;
}
IrisLogging.warn("Upper dimension '" + upperKey + "' could not be resolved, skipping upper terrain.");
return null;
}
DimensionStackContext buildDimensionStackContext() {
IrisDimension dimension = engine.getDimension();
if (!dimension.hasDimensionStack()) {
return null;
}
if (dimension.hasUpperDimension()) {
throw new IllegalStateException("Dimension '" + dimension.getLoadKey()
+ "' cannot enable both dimensionStack and upperDimension.");
}
DimensionStackContext context = DimensionStackContext.create(engine, dimension.getDimensionStack());
IrisLogging.info("Dimension stack enabled: "
+ String.join(" -> ", dimension.getDimensionStack().getDimensions()));
return context;
}
private EngineMode createMode() {
Throwable configuredFailure = null;
private void schedulePackHash(GenerationRuntime generationRuntime) {
try {
IrisDimensionMode configuredMode = engine.getDimension().getMode();
if (configuredMode == null) {
configuredMode = new IrisDimensionMode();
engine.getDimension().setMode(configuredMode);
if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
try {
generationRuntime.hash32().complete(computePackHash(generationRuntime.data()));
} catch (Throwable e) {
generationRuntime.hash32().completeExceptionally(e);
throw propagate(e);
}
})) {
throw new IllegalStateException("Iris background task admission closed before pack hashing.");
}
EngineMode configured = configuredMode.create(engine);
if (configured == null) {
throw new IllegalStateException("Dimension mode factory returned null");
}
return configured;
} catch (Throwable e) {
configuredFailure = e;
generationRuntime.hash32().completeExceptionally(e);
IrisLogging.reportError(e);
if (engine.getModeFallbackLogged().compareAndSet(false, true)) {
IrisLogging.warn("Failed to initialize configured dimension mode for " + engine.getDimension().getLoadKey() + ", falling back to OVERWORLD mode.");
}
}
}
try {
EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(engine);
if (fallback == null) {
throw new IllegalStateException("OVERWORLD mode factory returned null");
}
return fallback;
} catch (Throwable fallbackFailure) {
fallbackFailure.addSuppressed(configuredFailure);
throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure);
}
private long computePackHash(IrisData data) {
File[] roots = data.getLoaders()
.values()
.stream()
.map(ResourceLoader::getFolderName)
.map(name -> new File(data.getDataFolder(), name))
.filter(File::exists)
.filter(File::isDirectory)
.toArray(File[]::new);
return IO.hashRecursiveMeta(roots);
}
BiomeMaxes computeBiomeMaxes() {
@@ -317,11 +340,27 @@ final class EngineRuntimeBuilder {
return current;
}
static GenerationKernelRegistry.RuntimeKernel selectRuntimeKernel(
GenerationKernelRegistry.Version version
) {
try {
return GenerationKernelRegistry.standard().select(version);
} catch (IOException failure) {
throw new IllegalArgumentException("Unsupported executable Iris generation kernel "
+ version + ".", failure);
}
}
static final class RuntimeAssembly {
private static final AtomicInteger RUNTIME_IDS = new AtomicInteger();
final int cacheId;
final EngineTarget target;
final Path mantleStorageDirectory;
final GenerationKernelRegistry.Version kernelVersion;
final GenerationKernelRegistry.RuntimeKernel runtimeKernel;
final SeedManager seedManager;
final TransitionGenerationPlan transitionPlan;
static int nextRuntimeId() {
return RUNTIME_IDS.incrementAndGet();
@@ -331,29 +370,76 @@ final class EngineRuntimeBuilder {
DimensionStackContext dimensionStackContext;
EngineEffects effects;
EngineMode mode;
EngineMantle mantle;
EngineWorldManager worldManager;
CompletableFuture<Long> hash32;
boolean ownsMantle;
RuntimeAssembly(int cacheId, EngineTarget target) {
this.cacheId = cacheId;
this.target = target;
}
EngineRuntime freeze(BiomeMaxes biomeMaxes) {
if (complex == null || effects == null || mode == null || worldManager == null || hash32 == null) {
throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime.");
}
return new EngineRuntime(
this(
cacheId,
target,
null,
selectRuntimeKernel(GenerationKernelRegistry.standard().current()),
null);
}
RuntimeAssembly(int cacheId, EngineTarget target, Path mantleStorageDirectory) {
this(
cacheId,
target,
mantleStorageDirectory,
selectRuntimeKernel(GenerationKernelRegistry.standard().current()),
null);
}
RuntimeAssembly(
int cacheId,
EngineTarget target,
Path mantleStorageDirectory,
GenerationKernelRegistry.RuntimeKernel runtimeKernel,
TransitionGenerationPlan transitionPlan
) {
this.cacheId = cacheId;
this.target = target;
this.mantleStorageDirectory = mantleStorageDirectory == null
? null
: IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
this.runtimeKernel = Objects.requireNonNull(runtimeKernel, "executable generation kernel");
this.seedManager = this.runtimeKernel.createSeedManager(target.getWorld().getRawWorldSeed());
this.kernelVersion = this.runtimeKernel.version();
this.transitionPlan = transitionPlan;
}
GenerationRuntime freezeGeneration(BiomeMaxes biomeMaxes) {
if (complex == null || mode == null || mantle == null || mantleStorageDirectory == null || hash32 == null) {
throw new IllegalStateException("Cannot publish an incomplete Iris generation runtime.");
}
return new GenerationRuntime(
cacheId,
target,
target.getData(),
target.getDimension(),
kernelVersion,
runtimeKernel,
seedManager,
transitionPlan,
complex,
upperContext,
dimensionStackContext,
effects,
mode,
worldManager,
mantle,
mantleStorageDirectory,
hash32,
biomeMaxes);
}
EngineRuntime freezeRuntime(BiomeMaxes biomeMaxes) {
if (effects == null || worldManager == null) {
throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime.");
}
return new EngineRuntime(freezeGeneration(biomeMaxes), effects, worldManager);
}
}
}
@@ -22,8 +22,10 @@ import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
@@ -38,7 +40,6 @@ final class EngineShutdownSequence {
private final IrisEngine engine;
private boolean runtimeReleased;
private boolean targetReleased;
private boolean mantleReleased;
private boolean engineDataReleased;
private boolean preservationReleased;
@@ -47,6 +48,17 @@ final class EngineShutdownSequence {
}
void close() {
if (!engine.beginShutdown()) {
return;
}
Throwable routerFailure = runCleanup(null, engine::closeAttachedGenerationHistoryRuntimeRouter);
if (routerFailure != null) {
synchronized (engine.lifecycleLock) {
engine.lifecycleState = LifecycleState.FAILED;
}
reportIncompleteClose(routerFailure);
return;
}
Throwable failure;
synchronized (engine.lifecycleLock) {
if (engine.closed) {
@@ -122,14 +134,10 @@ final class EngineShutdownSequence {
if (runtimeReleased) {
failure = releaseTarget(failure);
}
if (targetReleased) {
failure = releaseMantle(failure);
}
if (prefetchFailure == null
&& engineDataFailure == null
&& runtimeReleased
&& targetReleased
&& mantleReleased) {
&& targetReleased) {
failure = releaseEngineDataForShutdown(failure);
}
if (engineDataReleased) {
@@ -140,7 +148,6 @@ final class EngineShutdownSequence {
if (failure == null
&& runtimeReleased
&& targetReleased
&& mantleReleased
&& engineDataReleased
&& preservationReleased) {
engine.closed = true;
@@ -185,10 +192,10 @@ final class EngineShutdownSequence {
() -> NativeStructureOwnershipStore.close(engine));
cleanupFailure = appendFailure(cleanupFailure, ownershipFailure);
if (ownershipFailure == null) {
cleanupFailure = closeDetachedGenerationRuntimes(cleanupFailure);
cleanupFailure = closeRuntime(engine.runtime, cleanupFailure);
engine.runtime = null;
cleanupFailure = runCleanup(cleanupFailure, engine.getTarget()::close);
cleanupFailure = runCleanup(cleanupFailure, engine.getMantle()::close);
cleanupFailure = runCleanup(cleanupFailure, engine.publishedTarget::close);
cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData);
engine.closed = true;
cleanupFailure = runCleanup(cleanupFailure, () -> {
@@ -232,18 +239,69 @@ final class EngineShutdownSequence {
assembly.hash32.cancel(true);
}
});
if (assembly.mantle != null && assembly.ownsMantle) {
assembly.ownsMantle = false;
failure = runCleanup(failure, assembly.mantle::saveAllNow);
failure = runCleanup(failure, assembly.mantle::close);
}
return failure;
}
Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) {
return closeRuntime(engineRuntime, null, failure);
}
Throwable closeRuntime(
EngineRuntime engineRuntime,
EngineMantle retainedMantle,
Throwable failure
) {
if (engineRuntime == null) {
return failure;
}
failure = runCleanup(failure, engineRuntime.worldManager()::close);
failure = runCleanup(failure, engineRuntime.effects()::close);
failure = runCleanup(failure, engineRuntime.mode()::close);
failure = runCleanup(failure, engineRuntime.complex()::close);
failure = runCleanup(failure, () -> engineRuntime.hash32().cancel(true));
return closeGenerationRuntime(engineRuntime.generation(), retainedMantle, failure);
}
Throwable closeGenerationRuntime(GenerationRuntime generationRuntime, Throwable failure) {
return closeGenerationRuntime(generationRuntime, null, failure);
}
Throwable closeGenerationRuntime(
GenerationRuntime generationRuntime,
EngineMantle retainedMantle,
Throwable failure
) {
if (generationRuntime == null) {
return failure;
}
failure = runCleanup(failure, generationRuntime.mode()::close);
failure = runCleanup(failure, generationRuntime.complex()::close);
failure = runCleanup(failure, () -> generationRuntime.hash32().cancel(true));
if (generationRuntime.mantle() != retainedMantle) {
failure = runCleanup(failure, generationRuntime.mantle()::saveAllNow);
failure = runCleanup(failure, generationRuntime.mantle()::close);
}
return failure;
}
Throwable closeDetachedGenerationRuntime(GenerationRuntime generationRuntime, Throwable failure) {
IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding(
engine,
generationRuntime);
try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) {
failure = closeGenerationRuntime(generationRuntime, failure);
} catch (Throwable scopeFailure) {
failure = appendFailure(failure, scopeFailure);
}
return closeDetachedTarget(generationRuntime.target(), failure);
}
Throwable closeDetachedTarget(EngineTarget target, Throwable failure) {
failure = runCleanup(failure, () -> target.getData().unregisterEngine(engine));
failure = runCleanup(failure, target::close);
failure = runCleanup(failure, target.getData()::close);
return failure;
}
@@ -251,6 +309,10 @@ final class EngineShutdownSequence {
if (runtimeReleased) {
return failure;
}
Throwable detachedFailure = closeDetachedGenerationRuntimes(null);
if (detachedFailure != null) {
return appendFailure(failure, detachedFailure);
}
Throwable runtimeFailure = closeRuntime(engine.runtime, null);
if (runtimeFailure != null) {
return appendFailure(failure, runtimeFailure);
@@ -260,11 +322,23 @@ final class EngineShutdownSequence {
return failure;
}
private Throwable closeDetachedGenerationRuntimes(Throwable failure) {
GenerationRuntime[] detachedRuntimes = engine.detachedGenerationRuntimes.toArray(new GenerationRuntime[0]);
for (GenerationRuntime detached : detachedRuntimes) {
Throwable detachedFailure = closeDetachedGenerationRuntime(detached, null);
failure = appendFailure(failure, detachedFailure);
if (detachedFailure == null) {
engine.detachedGenerationRuntimes.remove(detached);
}
}
return failure;
}
private Throwable releaseTarget(Throwable failure) {
if (targetReleased) {
return failure;
}
Throwable targetFailure = runCleanup(null, engine.getTarget()::close);
Throwable targetFailure = runCleanup(null, engine.publishedTarget::close);
if (targetFailure != null) {
return appendFailure(failure, targetFailure);
}
@@ -272,18 +346,6 @@ final class EngineShutdownSequence {
return failure;
}
private Throwable releaseMantle(Throwable failure) {
if (mantleReleased) {
return failure;
}
Throwable mantleFailure = runCleanup(null, engine.getMantle()::close);
if (mantleFailure != null) {
return appendFailure(failure, mantleFailure);
}
mantleReleased = true;
return failure;
}
private Throwable releaseEngineDataForShutdown(Throwable failure) {
if (engineDataReleased) {
return failure;
@@ -0,0 +1,101 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.GenerationKernelRegistry;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisDimension;
import java.nio.file.Path;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
record GenerationRuntime(
int cacheId,
EngineTarget target,
IrisData data,
IrisDimension dimension,
GenerationKernelRegistry.Version kernelVersion,
GenerationKernelRegistry.RuntimeKernel runtimeKernel,
SeedManager seedManager,
TransitionGenerationPlan transitionPlan,
IrisComplex complex,
UpperDimensionContext upperContext,
DimensionStackContext dimensionStackContext,
EngineMode mode,
EngineMantle mantle,
Path mantleStorageDirectory,
CompletableFuture<Long> hash32,
BiomeMaxes biomeMaxes
) {
GenerationRuntime {
Objects.requireNonNull(target, "generation target");
Objects.requireNonNull(data, "generation data");
Objects.requireNonNull(dimension, "generation dimension");
Objects.requireNonNull(kernelVersion, "generation kernel version");
Objects.requireNonNull(runtimeKernel, "executable generation kernel");
Objects.requireNonNull(seedManager, "generation seed manager");
if (!kernelVersion.equals(runtimeKernel.version())) {
throw new IllegalArgumentException("Generation kernel version does not match its executable runtime.");
}
Objects.requireNonNull(complex, "generation complex");
Objects.requireNonNull(mode, "generation mode");
Objects.requireNonNull(mantle, "generation mantle");
mantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
Objects.requireNonNull(hash32, "generation pack hash");
Objects.requireNonNull(biomeMaxes, "generation biome maximums");
if (target.getData() != data || target.getDimension() != dimension) {
throw new IllegalArgumentException("Generation target, data, and dimension must describe one runtime.");
}
}
GenerationRuntime withComplex(
int nextCacheId,
IrisComplex nextComplex,
UpperDimensionContext nextUpperContext,
DimensionStackContext nextDimensionStackContext,
BiomeMaxes nextBiomeMaxes
) {
return new GenerationRuntime(
nextCacheId,
target,
data,
dimension,
kernelVersion,
runtimeKernel,
seedManager,
transitionPlan,
nextComplex,
nextUpperContext,
nextDimensionStackContext,
mode,
mantle,
mantleStorageDirectory,
hash32,
nextBiomeMaxes);
}
record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) {
}
}
@@ -0,0 +1,55 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.engine;
import art.arcane.iris.engine.IrisEngine.GenerationRuntimeBinding;
import art.arcane.iris.engine.IrisEngine.GenerationRuntimeScope;
final class GenerationRuntimeScopeState {
private final ThreadLocal<GenerationRuntimeBinding> current = new ThreadLocal<>();
GenerationRuntimeBinding current() {
return current.get();
}
GenerationRuntimeScope open(GenerationRuntimeBinding binding) {
Thread owner = Thread.currentThread();
GenerationRuntimeBinding previous = current.get();
current.set(binding);
return new GenerationRuntimeScope(this, owner, previous, binding);
}
void close(
Thread owner,
GenerationRuntimeBinding previous,
GenerationRuntimeBinding installed
) {
if (Thread.currentThread() != owner) {
throw new IllegalStateException("Iris generation runtime scope closed from a different thread.");
}
if (current.get() != installed) {
throw new IllegalStateException("Iris generation runtime scopes must close in LIFO order.");
}
if (previous == null) {
current.remove();
return;
}
current.set(previous);
}
}

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