Merged generation history

- Integrated immutable generation epochs after dimension stacking
- Preserved historical terrain, biome, structure, and locate behavior
- Validated combined Bukkit and modded generation contracts
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-04 15:07:18 -04:00
commit 20ad49b9d2
263 files changed
+39624 -1849

No files matched your search

@@ -8,9 +8,11 @@ import art.arcane.iris.engine.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout; import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext; import art.arcane.iris.engine.DimensionTerrainContext;
import art.arcane.iris.engine.data.cache.AtomicCache; 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.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
@@ -40,6 +42,7 @@ import org.bukkit.craftbukkit.CraftWorld;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@@ -65,14 +68,11 @@ public class CustomBiomeSource extends BiomeSource {
private final Registry<Biome> biomeRegistry; private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>(); private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final Holder<Biome> fallbackBiome; private final Holder<Biome> fallbackBiome;
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<RuntimeNoiseKey, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<RuntimeNoiseKey, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<RuntimeColumnKey, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<RuntimeColumnKey, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
private volatile KMap<String, Holder<Biome>> customBiomes; private final ConcurrentHashMap<Integer, RuntimeBiomeState> runtimeBiomeStates = new ConcurrentHashMap<>();
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
private volatile IrisDimension cacheDimension;
private volatile int cacheRuntimeId;
public CustomBiomeSource(long seed, Engine engine, World world) { public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine; this.engine = engine;
@@ -80,10 +80,10 @@ public class CustomBiomeSource extends BiomeSource {
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null); this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null); this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry); this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome); if (engine instanceof IrisEngine irisEngine) {
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine); irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
this.cacheDimension = engine.getDimension(); }
this.cacheRuntimeId = engine.getCacheID(); runtimeBiomeState();
} }
private static List<Holder<Biome>> getAllBiomes( private static List<Holder<Biome>> getAllBiomes(
@@ -133,7 +133,7 @@ public class CustomBiomeSource extends BiomeSource {
if (irisBiome.isCustom()) { if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder( Holder<Biome> customHolder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), customBiome.getId()); customRegistry, engine, ownedBiome.dimension(), customBiome.getId());
if (customHolder == null) { if (customHolder == null) {
throw new IllegalStateException("Iris custom visible biome '" throw new IllegalStateException("Iris custom visible biome '"
+ customBiomeKey(ownedBiome.dimension(), customBiome.getId()) + customBiomeKey(ownedBiome.dimension(), customBiome.getId())
@@ -257,7 +257,7 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) { if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime"); throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
} }
ensureCachesCurrent(); runtimeBiomeState();
World world = BukkitWorldBinding.world(engine.getWorld()); World world = BukkitWorldBinding.world(engine.getWorld());
if (world == null) { if (world == null) {
throw new IllegalStateException("Iris biome source has no bound Bukkit world"); 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()) { for (IrisBiomeCustom j : i.getCustomDerivitives()) {
String key = customBiomeKey(ownedBiome.dimension(), j.getId()); String key = customBiomeKey(ownedBiome.dimension(), j.getId());
Holder<Biome> holder = resolveCustomBiomeHolder( Holder<Biome> holder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), j.getId()); customRegistry, engine, ownedBiome.dimension(), j.getId());
if (holder == null) { if (holder == null) {
if (fallback != null) { if (fallback != null) {
m.put(key, fallback); m.put(key, fallback);
@@ -318,7 +318,7 @@ public class CustomBiomeSource extends BiomeSource {
} }
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveCustomBiomeHolder( Holder<Biome> customHolder = resolveCustomBiomeHolder(
customRegistry, ownedBiome.dimension(), customBiome.getId()); customRegistry, engine, ownedBiome.dimension(), customBiome.getId());
if (customHolder != null) { if (customHolder != null) {
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
} }
@@ -340,8 +340,7 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) { if (!isRuntimeAvailable()) {
return null; return null;
} }
ensureCachesCurrent(); return runtimeBiomeState().vanillaSpawnBiomes().get(biome.value());
return vanillaSpawnBiomes.get(biome.value());
} }
} }
@@ -354,7 +353,7 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) { if (!isRuntimeAvailable()) {
return null; return null;
} }
ensureCachesCurrent(); runtimeBiomeState();
int quartX = QuartPos.fromBlock(blockX); int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ); int quartZ = QuartPos.fromBlock(blockZ);
int sampleX = QuartPos.toBlock(quartX); int sampleX = QuartPos.toBlock(quartX);
@@ -390,15 +389,17 @@ public class CustomBiomeSource extends BiomeSource {
@Override @Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome"); int blockX = x << 2;
if (lease == null) { int blockZ = z << 2;
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition"); try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
} openHistoryCoordinateScope(blockX, blockZ, "bukkit_structure_biome");
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { 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()) { if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime"); throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
} }
ensureCachesCurrent();
return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z); return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
} }
} }
@@ -409,7 +410,8 @@ public class CustomBiomeSource extends BiomeSource {
} }
boolean cacheable = isBiomeCacheable(x, z); boolean cacheable = isBiomeCacheable(x, z);
long cacheKey = packNoiseKey(x, y, z); RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
engine.getCacheID(), packNoiseKey(x, y, z));
if (cacheable) { if (cacheable) {
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey); Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
if (cachedHolder != null) { if (cachedHolder != null) {
@@ -455,7 +457,6 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) { if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime"); throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime");
} }
ensureCachesCurrent();
return findNaturalSurfaceBiomeHorizontal( return findNaturalSurfaceBiomeHorizontal(
x, y, z, searchRadius, quartStep, allowed, random); x, y, z, searchRadius, quartStep, allowed, random);
} }
@@ -533,7 +534,6 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) { if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime"); throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
} }
ensureCachesCurrent();
int minQuartY = QuartPos.fromBlock(y - radius); int minQuartY = QuartPos.fromBlock(y - radius);
boolean monumentQuery = radius == 29 boolean monumentQuery = radius == 29
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight(); && y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
@@ -556,20 +556,27 @@ public class CustomBiomeSource extends BiomeSource {
} }
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) { private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z); int blockX = x << 2;
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey); int blockZ = z << 2;
if (surfaceHolder != null) { try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
return surfaceHolder; 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( private Pair<BlockPos, Holder<Biome>> findNaturalSurfaceBiomeHorizontal(
@@ -609,21 +616,27 @@ public class CustomBiomeSource extends BiomeSource {
} }
private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) { private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) {
long columnKey = packColumnKey(x, z); int blockX = x << 2;
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey); int blockZ = z << 2;
if (cachedHolder != null) { try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
return cachedHolder; 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) { 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) { public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome"); try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
if (lease == null) { x << 2, z << 2, "bukkit_visible_biome");
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition"); GenerationSessionLease lease = requireGenerationLease(
} "bukkit_visible_biome",
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { "Iris visible biome lookup was rejected during an engine transition");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
prepareVisibleBiomeBatch(); prepareVisibleBiomeBatch();
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler); return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
} }
@@ -685,7 +699,7 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) { if (!isRuntimeAvailable()) {
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime"); throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
} }
ensureCachesCurrent(); runtimeBiomeState();
} }
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease( Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
@@ -717,7 +731,8 @@ public class CustomBiomeSource extends BiomeSource {
IrisDimensionCarvingResolver.State resolverState, IrisDimensionCarvingResolver.State resolverState,
boolean cacheable boolean cacheable
) { ) {
long cacheKey = packNoiseKey(x, y, z); RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
engine.getCacheID(), packNoiseKey(x, y, z));
if (cacheable) { if (cacheable) {
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey); Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
if (cachedHolder != null) { 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() { private boolean isRuntimeAvailable() {
return !engine.isClosed() && engine.getComplex() != null; 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(); IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID(); int runtimeId = engine.getCacheID();
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) { RuntimeBiomeState existing = runtimeBiomeStates.get(runtimeId);
return; if (existing != null) {
if (existing.dimension() != dimension) {
throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions.");
}
return existing;
} }
synchronized (this) { synchronized (runtimeBiomeStates) {
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) { existing = runtimeBiomeStates.get(runtimeId);
return; 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( KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
biomeCustomRegistry, engine, fallbackBiome); biomeCustomRegistry, engine, fallbackBiome);
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes( Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
biomeCustomRegistry, biomeRegistry, engine); biomeCustomRegistry, biomeRegistry, engine);
noiseBiomeCache.clear(); RuntimeBiomeState created = new RuntimeBiomeState(
structureBiomeCache.clear(); dimension,
surfaceStructureBiomeCache.clear(); refreshedCustomBiomes,
naturalSurfaceStructureBiomeCache.clear(); refreshedSpawnBiomes);
customBiomes = refreshedCustomBiomes; runtimeBiomeStates.put(runtimeId, created);
vanillaSpawnBiomes = refreshedSpawnBiomes; return created;
cacheDimension = dimension;
cacheRuntimeId = runtimeId;
} }
} }
@@ -830,6 +884,14 @@ public class CustomBiomeSource extends BiomeSource {
IrisDimensionCarvingResolver.State resolverState, IrisDimensionCarvingResolver.State resolverState,
DimensionStackLayout stackLayout 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( BiomeResolution resolution = resolveBiomeResolution(
x, y, z, resolverState, true, stackLayout); x, y, z, resolverState, true, stackLayout);
if (resolution == null) { if (resolution == null) {
@@ -858,7 +920,7 @@ public class CustomBiomeSource extends BiomeSource {
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) { private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
if (customBiome != null) { if (customBiome != null) {
Holder<Biome> holder = customBiomes.get(customBiomeKey( Holder<Biome> holder = runtimeBiomeState().customBiomes().get(customBiomeKey(
resolution.dimension, customBiome.getId())); resolution.dimension, customBiome.getId()));
if (holder != null) { if (holder != null) {
return holder; return holder;
@@ -872,6 +934,18 @@ public class CustomBiomeSource extends BiomeSource {
return resolveBiomeResolution(x, y, z, null, false); 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( private BiomeResolution resolveBiomeResolution(
int x, int x,
int y, int y,
@@ -1008,17 +1082,19 @@ public class CustomBiomeSource extends BiomeSource {
private static Holder<Biome> resolveCustomBiomeHolder( private static Holder<Biome> resolveCustomBiomeHolder(
Registry<Biome> customRegistry, Registry<Biome> customRegistry,
Engine engine,
IrisDimension dimension, IrisDimension dimension,
String customBiomeId String customBiomeId
) { ) {
if (customRegistry == null || dimension == null if (customRegistry == null || engine == null || dimension == null
|| customBiomeId == null || customBiomeId.isBlank()) { || customBiomeId == null || customBiomeId.isBlank()) {
return null; return null;
} }
Identifier resourceLocation = Identifier.tryParse(customBiomeKey(dimension, customBiomeId)); String physicalKey = engine.getData().customBiomeResourceKey(dimension, customBiomeId);
Identifier resourceLocation = Identifier.tryParse(physicalKey);
if (resourceLocation == null) { if (resourceLocation == null) {
return null; throw new IllegalStateException("Invalid Iris custom biome resource key '" + physicalKey + "'.");
} }
Biome biome = customRegistry.getValue(resourceLocation); Biome biome = customRegistry.getValue(resourceLocation);
if (biome == null) { if (biome == null) {
@@ -1170,4 +1246,17 @@ public class CustomBiomeSource extends BiomeSource {
private record OwnedBiome(IrisDimension dimension, IrisBiome biome) { 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 static final String CYCLE_MARKER = "Feature order cycle found";
private final Engine engine; private final Engine engine;
private volatile FeatureTable featureTable; private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
private volatile IrisDimension inertDimension;
private volatile int settledRuntimeId;
ImportedFeatureStage(Engine engine) { ImportedFeatureStage(Engine engine) {
this.engine = engine; this.engine = engine;
@@ -76,7 +74,8 @@ final class ImportedFeatureStage {
* does today. * does today.
*/ */
BiomeGenerationSettings generationSettings(Holder<Biome> biome) { BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
FeatureTable table = featureTable; RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) { if (table == null) {
return 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 * 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. * 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) { void prepare(WorldGenLevel level) {
IrisDimension dimension = engine.getDimension(); IrisDimension dimension = engine.getDimension();
int runtimeId = engine.getCacheID(); int runtimeId = engine.getCacheID();
if (settled(dimension, runtimeId)) { if (settled(runtimeStates.get(runtimeId), dimension)) {
return; return;
} }
synchronized (this) { synchronized (this) {
if (settled(dimension, runtimeId)) { if (settled(runtimeStates.get(runtimeId), dimension)) {
return; return;
} }
build(level, dimension); runtimeStates.put(runtimeId, build(level, dimension));
settledRuntimeId = runtimeId;
} }
} }
private boolean settled(IrisDimension dimension, int runtimeId) { void evictRuntime(int runtimeId) {
if (settledRuntimeId != runtimeId) { runtimeStates.remove(runtimeId);
return false;
}
FeatureTable current = featureTable;
if (current != null && current.dimension() == dimension) {
return true;
}
return inertDimension == dimension;
} }
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; IrisImportedFeatureControl control;
try { try {
control = NativeFeatureGenerationPolicy.control(engine); control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) { } catch (RuntimeException error) {
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: " IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
+ error); + error);
markInert(dimension); return new RuntimeFeatureState(dimension, null);
return;
} }
if (!control.shouldGenerateFeatures()) { if (!control.shouldGenerateFeatures()) {
markInert(dimension); return new RuntimeFeatureState(dimension, null);
return;
} }
FeatureTable built; FeatureTable built;
try { try {
@@ -138,23 +133,15 @@ final class ImportedFeatureStage {
} catch (Throwable error) { } catch (Throwable error) {
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey() IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
+ ": feature table construction failed: " + error); + ": feature table construction failed: " + error);
markInert(dimension); return new RuntimeFeatureState(dimension, null);
return;
} }
if (built == null) { if (built == null) {
markInert(dimension); return new RuntimeFeatureState(dimension, null);
return;
} }
featureTable = built;
inertDimension = null;
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size() IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size() + " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
+ " custom-biome derivative maps"); + " custom-biome derivative maps");
} return new RuntimeFeatureState(dimension, built);
private void markInert(IrisDimension dimension) {
featureTable = null;
inertDimension = dimension;
} }
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control, private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
@@ -217,7 +204,7 @@ final class ImportedFeatureStage {
continue; continue;
} }
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
addKey(keys, engine.getDimension().getCustomBiomeKey(customBiome.getId())); addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
} }
} }
return keys; return keys;
@@ -249,7 +236,10 @@ final class ImportedFeatureStage {
continue; continue;
} }
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
derivatives.put(engine.getDimension().getCustomBiomeKey(customBiome.getId()), derivative); derivatives.put(
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
derivative
);
} }
} }
return derivatives; return derivatives;
@@ -280,7 +270,8 @@ final class ImportedFeatureStage {
* control is disabled or the table degraded. * control is disabled or the table degraded.
*/ */
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) { void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
FeatureTable table = featureTable; RuntimeFeatureState state = currentState();
FeatureTable table = state == null ? null : state.featureTable();
if (table == null) { if (table == null) {
return; return;
} }
@@ -467,4 +458,7 @@ final class ImportedFeatureStage {
List<FeatureSorter.StepFeatureData> steps, List<FeatureSorter.StepFeatureData> steps,
Map<String, Holder<Biome>> derivatives, boolean filtered) { 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.GenerationSessionLease;
import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; 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.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.platform.BukkitChunkGenerator; import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
@@ -101,6 +103,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
@@ -133,8 +136,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
private final int runtimeHeight; private final int runtimeHeight;
private final int runtimeSeaLevel; private final int runtimeSeaLevel;
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>(); private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
private volatile IrisDimension spawnTableDimension;
private volatile int spawnTableRuntimeId;
private final ImportedFeatureStage importedFeatures; private final ImportedFeatureStage importedFeatures;
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>(); private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
private volatile ReachableStructureCache reachableStructureCache; private volatile ReachableStructureCache reachableStructureCache;
@@ -158,9 +159,25 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
this.runtimeMinY = level.getMinY(); this.runtimeMinY = level.getMinY();
this.runtimeHeight = level.getHeight(); this.runtimeHeight = level.getHeight();
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight(); this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
if (engine instanceof IrisEngine irisEngine) {
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
}
installNativeStructureVolumeIndex(level); 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) { private void installNativeStructureVolumeIndex(ServerLevel level) {
WeakReference<ServerLevel> levelReference = new WeakReference<>(level); WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this); WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
@@ -186,7 +203,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) { if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) {
return null; 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)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
HolderSet<Structure> reachable = filterReachableStructures(level, holders); HolderSet<Structure> reachable = filterReachableStructures(level, holders);
NativeStructureVanillaLocator.Candidate nativeCandidate = NativeStructureVanillaLocator.Candidate nativeCandidate =
@@ -365,9 +387,16 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|| runtimeLevel.getChunkSource().getGeneratorState() != structureState) { || runtimeLevel.getChunkSource().getGeneratorState() != structureState) {
return; return;
} }
ChunkPos chunkPos = access.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures"); 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"); GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (!allowsRoutedDiscreteGeneration(access, ChunkStatus.STRUCTURE_STARTS)) {
return;
}
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts()); Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject( Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
@@ -399,15 +428,28 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (!start.isValid() || previousStarts.get(structure) == start) { if (!start.isValid() || previousStarts.get(structure) == start) {
continue; continue;
} }
if (configuredStarts.containsKey(structure)) {
continue;
}
Identifier id = registry.getKey(structure); Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString(); String structureId = id == null ? null : id.toString();
if (structureId == null) { if (structureId == null) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z()); "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()); boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision; IrisNativeStructureDecision decision;
try { try {
@@ -634,7 +676,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
if (runtimeLevel.getChunkSource().getGenerator() != this) { if (runtimeLevel.getChunkSource().getGenerator() != this) {
return; return;
} }
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references"); 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"); GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
NativeStructureReferenceRepair.createReferences( NativeStructureReferenceRepair.createReferences(
@@ -644,7 +690,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) { 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"); 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"); GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
customBiomeSource.prepareVisibleBiomeBatch(); customBiomeSource.prepareVisibleBiomeBatch();
@@ -659,31 +709,63 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) { 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 @Override
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) { 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 @Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) { public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
ChunkPos chunkPos = ichunkaccess.getPos();
BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage( BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(
ichunkaccess.getPos(), chunkPos,
"bukkit_nms_chunk_pipeline"); "bukkit_nms_chunk_pipeline");
GenerationSessionLease lease; GenerationHistoryRuntimeRouter.RuntimeRoute route;
try { try {
lease = requireGenerationLease("bukkit_nms_chunk_pipeline"); route = openHistoryRoute(chunkPos.x(), chunkPos.z(), "bukkit_nms_chunk_pipeline");
} catch (RuntimeException | Error failure) { } catch (RuntimeException | Error failure) {
stage.close(); stage.close();
throw failure; throw failure;
} }
GenerationSessionLease lease;
try { try {
CompletableFuture<ChunkAccess> pipeline = delegate lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess) } 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 -> { .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); primeWorldgenHeightmaps(filled);
return filled; return filled;
} }
@@ -691,24 +773,78 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>(); CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
pipeline.whenComplete((filled, failure) -> { pipeline.whenComplete((filled, failure) -> {
boolean cancelled = isCancellationFailure(failure); boolean cancelled = isCancellationFailure(failure);
lease.close(); Throwable completionFailure = closeNoisePipelineResources(
stage.close(); failure, lease, route, stage);
if (failure == null) { if (completionFailure == null) {
completion.complete(filled); completion.complete(filled);
} else if (cancelled) { } else if (cancelled) {
completion.cancel(false); completion.cancel(false);
} else { } else {
completion.completeExceptionally(failure); completion.completeExceptionally(completionFailure);
} }
}); });
return completion; return completion;
} catch (RuntimeException | Error failure) { } catch (RuntimeException | Error failure) {
lease.close(); lease.close();
closeHistoryRoute(route, failure);
stage.close(); stage.close();
throw failure; 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) { private static boolean isCancellationFailure(Throwable failure) {
Throwable current = failure; Throwable current = failure;
while (current instanceof CompletionException && current.getCause() != null) { while (current instanceof CompletionException && current.getCause() != null) {
@@ -739,6 +875,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) { 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); Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
if (vanillaSpawnBiome == null) { if (vanillaSpawnBiome == null) {
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition); return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
@@ -759,18 +909,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
return explicitSpawns; return explicitSpawns;
} }
IrisDimension spawnDimension = engine.getDimension();
int spawnRuntimeId = engine.getCacheID(); int spawnRuntimeId = engine.getCacheID();
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) { SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), enumcreaturetype);
synchronized (mergedSpawnTables) {
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
mergedSpawnTables.clear();
spawnTableDimension = spawnDimension;
spawnTableRuntimeId = spawnRuntimeId;
}
}
}
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns)); return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
} }
@@ -786,19 +926,26 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) { public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
ChunkPos chunkPos = ichunkaccess.getPos();
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration"); 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"); GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)) {
// feature-order cycle is reported once and degraded to features-off. // Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
importedFeatures.prepare(generatoraccessseed); // feature-order cycle is reported once and degraded to features-off.
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager); importedFeatures.prepare(generatoraccessseed);
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager); addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the // Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
// dimension set importedFeatures.enabled. // addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
importedFeatures.run(generatoraccessseed, ichunkaccess, this); // dimension set importedFeatures.enabled.
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false); 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<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size()); List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) { for (StructureStart start : starts) {
BoundingBox footprint = start.getBoundingBox();
if (!engine.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
footprint.maxZ())) {
continue;
}
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord ownership =
NativeStructureOwnershipRecovery.resolve( NativeStructureOwnershipRecovery.resolve(
engine, world.getLevel(), structureId, structure, start); engine, world.getLevel(), structureId, structure, start);
@@ -1001,13 +1156,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
} }
private List<List<Structure>> structuresByStep(Registry<Structure> registry) { private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
int runtimeId = engine.getCacheID();
StructureStepCache cached = structureStepCache; StructureStepCache cached = structureStepCache;
if (cached != null && cached.registry() == registry) { if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
return cached.structures(); return cached.structures();
} }
synchronized (this) { synchronized (this) {
cached = structureStepCache; cached = structureStepCache;
if (cached != null && cached.registry() == registry) { if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
return cached.structures(); return cached.structures();
} }
int steps = GenerationStep.Decoration.values().length; int steps = GenerationStep.Decoration.values().length;
@@ -1022,7 +1178,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
grouped.set(step, List.copyOf(grouped.get(step))); grouped.set(step, List.copyOf(grouped.get(step)));
} }
List<List<Structure>> resolved = List.copyOf(grouped); List<List<Structure>> resolved = List.copyOf(grouped);
structureStepCache = new StructureStepCache(registry, resolved); structureStepCache = new StructureStepCache(runtimeId, registry, resolved);
return resolved; return resolved;
} }
} }
@@ -1054,7 +1210,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { 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)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY()); SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
BlockPos blockPos = sectionPos.origin(); BlockPos blockPos = sectionPos.origin();
@@ -1085,22 +1244,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public void spawnOriginalMobs(WorldGenRegion region) { public void spawnOriginalMobs(WorldGenRegion region) {
ChunkPos center = region.getCenter(); ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome; try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_spawn_original_mobs");
if (engine.getDimensionStackContext() == null) { GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); center.x(), center.z(), "bukkit_nms_spawn_original_mobs");
} else { GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
visibleBiome = customBiomeSource.getVisibleSurfaceBiome( GenerationSessionLease lease = requireGenerationLease("bukkit_nms_spawn_original_mobs");
center.getMinBlockX() + 8, IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
center.getMinBlockZ() + 8); if (!allowsRoutedDiscreteGeneration(region.getChunk(center.x(), center.z()), ChunkStatus.SPAWN)) {
if (visibleBiome == null) { return;
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
} }
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()); private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) {
NaturalSpawner.spawnMobsForChunkGeneration( if (chunk.getPersistedStatus().isOrAfter(stage)) {
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random); return false;
}
ChunkPos chunkPos = chunk.getPos();
return engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
} }
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables( private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
@@ -1133,7 +1310,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
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)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState()); boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
int height = engine.getDimensionStackContext() == null int height = engine.getDimensionStackContext() == null
@@ -1148,7 +1327,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override @Override
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) { public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
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)) { IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
boolean dimensionStack = engine.getDimensionStackContext() != null; boolean dimensionStack = engine.getDimensionStackContext() != null;
int block = dimensionStack 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) { private BukkitChunkGenerator.GenerationStagePermit requireGenerationStage(String operation) {
return platformGenerator == null return platformGenerator == null
? BukkitChunkGenerator.GenerationStagePermit.noop() ? 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 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( 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.Hunk;
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder; import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.util.common.data.IrisCustomData; import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.json.JSONObject; import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.mantle.runtime.Mantle; 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.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer; 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.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers; import net.bytebuddy.matcher.ElementMatchers;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
@@ -169,7 +173,6 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -181,11 +184,22 @@ public class NMSBinding implements INMSBinding {
private final AtomicBoolean injected = new AtomicBoolean(); private final AtomicBoolean injected = new AtomicBoolean();
private volatile ResettableClassFileTransformer levelStorageAccessTransformer; private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
private volatile ResettableClassFileTransformer serverLevelTransformer; private volatile ResettableClassFileTransformer serverLevelTransformer;
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
private boolean pluginClassLoaderCloseDeferred;
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>(); private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>(); private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>(); private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
private final AtomicCache<Method> byIdRef = 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) { private static Object getFor(Class<?> type, Object source) {
Object o = fieldFor(type, source); Object o = fieldFor(type, source);
@@ -1544,6 +1558,23 @@ public class NMSBinding implements INMSBinding {
} }
try { try {
IrisLogging.info("Injecting Bukkit"); 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() levelStorageAccessTransformer = new AgentBuilder.Default()
.disableClassFormatChanges() .disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
@@ -1579,18 +1610,22 @@ public class NMSBinding implements INMSBinding {
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e); IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer; ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer; ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
serverLevelTransformer = null; serverLevelTransformer = null;
levelStorageAccessTransformer = null; levelStorageAccessTransformer = null;
pluginClassLoaderTransformer = null;
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{ for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
partialServerLevel, partialServerLevel,
partialStorageAccess partialStorageAccess,
partialPluginClassLoader
}) { }) {
if (partial == null) { if (partial == null) {
continue; continue;
} }
try { try {
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION); 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; return false;
@@ -1637,16 +1672,52 @@ public class NMSBinding implements INMSBinding {
} }
@Override @Override
public boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) { public boolean isServerStopping() {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer(); return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
return ServerShutdownBoundary.await( }
@Override
public ServerShutdownBoundary createServerShutdownBoundary() {
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
return new ServerShutdownBoundary(
() -> server.hasFullyShutdown, () -> server.hasFullyShutdown,
server.getRunningThread(), server.getRunningThread()
timeout,
unit
); );
} }
@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 @Override
public KMap<Material, List<BlockProperty>> getBlockProperties() { public KMap<Material, List<BlockProperty>> getBlockProperties() {
KMap<Material, List<BlockProperty>> states = new KMap<>(); KMap<Material, List<BlockProperty>> states = new KMap<>();
@@ -1691,6 +1762,39 @@ public class NMSBinding implements INMSBinding {
return new FlatLevelSource(settings); return new FlatLevelSource(settings);
} }
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
private volatile boolean transformed;
private volatile Throwable failure;
@Override
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
boolean loaded, DynamicType dynamicType) {
transformed = true;
}
@Override
public void onError(String typeName, ClassLoader loader, JavaModule module,
boolean loaded, Throwable throwable) {
failure = throwable;
}
private void requireInstalled() {
if (!transformed || failure != null) {
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
}
}
}
private static class PluginClassLoaderCloseAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
Class<?> installer = Class.forName(
"art.arcane.iris.util.project.agent.Installer", true, ClassLoader.getSystemClassLoader());
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
.invoke(null, loader));
}
}
private static class ChunkAccessAdvice { private static class ChunkAccessAdvice {
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class) @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) { 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\")")); 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 was rejected during an engine transition"));
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime")); assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
assertTrue(source.contains("vanillaSpawnBiomes.get(biome.value())")); assertTrue(source.contains("runtimeBiomeState().vanillaSpawnBiomes().get(biome.value())"));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")")); assertTrue(source.contains("requireGenerationLease(\n \"bukkit_structure_biome\""));
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")")); 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("if (engine.isClosed())"));
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()")); assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
assertTrue(source.contains("catch (GenerationSessionException e)")); assertTrue(source.contains("catch (GenerationSessionException e)"));
@@ -140,7 +140,7 @@ public class CustomBiomeSourceStructureContractTest {
assertTrue(lookupEnd > lookupStart); assertTrue(lookupEnd > lookupStart);
String lookup = source.substring(lookupStart, lookupEnd); 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("prepareVisibleBiomeBatch()"));
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)")); assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()") 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, assertBefore(productionFill,
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(", "BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
"GenerationHistoryRuntimeRouter.RuntimeRoute route");
assertBefore(productionFill,
"GenerationHistoryRuntimeRouter.RuntimeRoute route",
"GenerationSessionLease lease"); "GenerationSessionLease lease");
assertBefore(productionFill, assertBefore(productionFill,
"requireNoiseGenerationStage(", "requireNoiseGenerationStage(",
"\"bukkit_nms_chunk_pipeline\")"); "\"bukkit_nms_chunk_pipeline\")");
assertBefore(productionFill, assertBefore(productionFill,
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")", "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("IrisContext.open(engine, lease.sessionId(), null)"));
assertTrue(productionFill.contains("openHistoryRuntimeScope(route)"));
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete("); assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()")); assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "lease.close();"); assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "closeNoisePipelineResources(");
assertBefore(completion, "lease.close();", "stage.close();"); assertBefore(completion, "closeNoisePipelineResources(", "completion.complete(filled)");
assertBefore(completion, "stage.close();", "completion.complete(filled)"); assertBefore(completion, "closeNoisePipelineResources(", "completion.cancel(false)");
assertBefore(completion, "stage.close();", "completion.cancel(false)"); assertBefore(completion, "closeNoisePipelineResources(", "completion.completeExceptionally(completionFailure)");
assertBefore(completion, "stage.close();", "completion.completeExceptionally(failure)");
assertTrue(completion.contains("else if (cancelled)")); assertTrue(completion.contains("else if (cancelled)"));
assertFalse(completion.contains("pipeline.isCancelled()")); assertFalse(completion.contains("pipeline.isCancelled()"));
assertFalse(completion.contains("finally")); assertFalse(completion.contains("finally"));
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)")); 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;")); assertTrue(productionFill.contains("return completion;"));
assertFalse(productionFill.contains("return pipeline;")); assertFalse(productionFill.contains("return pipeline;"));
assertFalse(productionFill.contains("pipeline.cancel(")); assertFalse(productionFill.contains("pipeline.cancel("));
@@ -422,6 +425,9 @@ public class IrisChunkGeneratorFailureContractTest {
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures"); String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes"); String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
String biomes = method(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface"); 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 noise = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise", "private static boolean isCancellationFailure");
String decoration = method(source, String decoration = method(source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)", "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(structures, "bukkit_nms_create_structures");
assertStageBeforeLease(references, "bukkit_nms_create_references"); assertStageBeforeLease(references, "bukkit_nms_create_references");
assertStageBeforeLease(biomes, "bukkit_nms_create_biomes"); 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, assertBefore(noise,
"requireNoiseGenerationStage(", "requireNoiseGenerationStage(",
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")"); "requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
@@ -437,7 +446,7 @@ public class IrisChunkGeneratorFailureContractTest {
assertBefore(decoration, assertBefore(decoration,
"requireGenerationLease(\"bukkit_nms_biome_decoration\")", "requireGenerationLease(\"bukkit_nms_biome_decoration\")",
"importedFeatures.prepare(generatoraccessseed)"); "importedFeatures.prepare(generatoraccessseed)");
assertEquals(4, occurrences(source, "requireGenerationStage(\"bukkit_nms_")); assertEquals(7, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
assertEquals(2, occurrences(source, "requireNoiseGenerationStage(")); assertEquals(2, occurrences(source, "requireNoiseGenerationStage("));
assertFalse(source.contains("GenerationSessionManager")); assertFalse(source.contains("GenerationSessionManager"));
} }
@@ -6,16 +6,48 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
public class NMSBindingShutdownBoundaryContractTest { 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 @Test
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception { public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n"); String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).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.hasFullyShutdown"));
assertTrue(boundary.contains("server.getRunningThread()")); 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) { 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.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.lang.reflect.Proxy;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -80,6 +81,30 @@ public class NativeStructureVolumeIndexTest {
assertEquals(coldResolutions, resolver.resolutions()); 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 @Test
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() { public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
@@ -188,12 +213,45 @@ public class NativeStructureVolumeIndexTest {
assertTrue(source.contains("isStructureChunk(")); 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) { private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z); SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece))); 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) { private static int findDisjointWindow(int chunkX, int chunkZ) {
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ); long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
for (int candidate = 64; candidate < 16_384; candidate += 17) { 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.volmlib.util.localization.BukkitLanguageSwitcher;
import art.arcane.iris.core.link.MultiverseCoreLink; import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.nms.INMS; 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.BukkitGuiHost;
import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.service.BoardSVC; import art.arcane.iris.core.service.BoardSVC;
@@ -139,6 +140,8 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileDescriptor;
import java.io.PrintStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
@@ -160,6 +163,7 @@ import java.util.regex.Pattern;
@SuppressWarnings("CanBeFinal") @SuppressWarnings("CanBeFinal")
public class Iris extends VolmitPlugin implements Listener, ReloadAware { public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L; 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 long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>(); 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 sharedRuntimeClosed = new AtomicBoolean(false);
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false); private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
private final AtomicBoolean terminalCleanupCompleted = 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 PlaceholderRegistration papiRegistration;
private volatile IrisPapiListener papiListener; private volatile IrisPapiListener papiListener;
private volatile IrisPapiState papiState; private volatile IrisPapiState papiState;
@@ -206,6 +212,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private BukkitDebugDump debugDump; private BukkitDebugDump debugDump;
private volatile SettingsHotloadWatch settingsHotloadWatch; private volatile SettingsHotloadWatch settingsHotloadWatch;
private volatile Thread serverLifecycleThread; private volatile Thread serverLifecycleThread;
private volatile ServerShutdownBoundary serverShutdownBoundary;
public static VolmitSender getSender() { public static VolmitSender getSender() {
if (sender == null) { if (sender == null) {
@@ -427,6 +434,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
if (e == null) { if (e == null) {
return; return;
} }
if (instance != null && instance.serverStopTeardownDeferred.get()) {
e.printStackTrace(SHUTDOWN_ERRORS);
return;
}
boolean debug = false; boolean debug = false;
if (instance != null) { if (instance != null) {
@@ -466,6 +477,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public static void reportError(String context, Throwable e) { public static void reportError(String context, Throwable e) {
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e; Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context; 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 { try {
if (instance != null) { if (instance != null) {
@@ -603,6 +619,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
sharedRuntimeClosed.set(false); sharedRuntimeClosed.set(false);
startupBoundaryRestart.set(false); startupBoundaryRestart.set(false);
terminalCleanupCompleted.set(false); terminalCleanupCompleted.set(false);
runtimeTeardownFailed.set(false);
generatorDrainCompleted = false;
deferredShutdownGenerators.clear(); deferredShutdownGenerators.clear();
MultiBurst.burst.reopen(); MultiBurst.burst.reopen();
MultiBurst.ioBurst.reopen(); MultiBurst.ioBurst.reopen();
@@ -742,6 +760,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
public void addShutdownHook() { public void addShutdownHook() {
removeShutdownHook(); removeShutdownHook();
serverLifecycleThread = Thread.currentThread(); serverLifecycleThread = Thread.currentThread();
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook"); shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
try { try {
Runtime.getRuntime().addShutdownHook(shutdownHook); Runtime.getRuntime().addShutdownHook(shutdownHook);
@@ -963,6 +982,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
drainWorldGenerators(reason, timeoutSeconds); drainWorldGenerators(reason, timeoutSeconds);
} }
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
return;
}
if (services != null && servicesDisabled.compareAndSet(false, true)) { if (services != null && servicesDisabled.compareAndSet(false, true)) {
// Only services whose onEnable actually completed; disabling a service that // Only services whose onEnable actually completed; disabling a service that
// never initialized runs teardown against uninitialized state. // never initialized runs teardown against uninitialized state.
@@ -970,6 +993,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try { try {
service.onDisable(); service.onDisable();
} catch (Throwable e) { } catch (Throwable e) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e); Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
} }
} }
@@ -979,8 +1003,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return; return;
} }
J.attempt(MultiBurst.burst::close); try {
J.attempt(MultiBurst.ioBurst::close); 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(); clearQueues();
IrisServices.clear(); IrisServices.clear();
} }
@@ -988,6 +1022,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
private void quiesceRuntimeForServerShutdown(String reason) { private void quiesceRuntimeForServerShutdown(String reason) {
serverStopTeardownDeferred.set(true); serverStopTeardownDeferred.set(true);
INMS.get().deferPluginClassLoaderClose();
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class); JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
if (jigsawStudioService != null) { if (jigsawStudioService != null) {
try { try {
@@ -1060,29 +1095,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
return; return;
} }
if (!awaitServerShutdownBoundary()) { 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; return;
} }
finishDeferredRuntimeTeardown("shutdown-hook", 30L); finishDeferredRuntimeTeardown("shutdown-hook", 30L);
} }
private boolean awaitServerShutdownBoundary() { private boolean awaitServerShutdownBoundary() {
if (!INMS.isBound()) { ServerShutdownBoundary boundary = serverShutdownBoundary;
if (boundary == null) {
return true; return true;
} }
try { try {
return INMS.get().awaitServerShutdownBoundary( return boundary.await(
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS, SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
TimeUnit.SECONDS TimeUnit.SECONDS
); );
} catch (Throwable e) { } 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; return false;
} }
} }
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) { private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
teardownRuntime(reason, 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(); finishTerminalCleanup();
} }
@@ -1094,12 +1135,36 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try { try {
runPostShutdown(); runPostShutdown();
} catch (Throwable e) { } catch (Throwable e) {
runtimeTeardownFailed.set(true);
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e); Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
} finally { } 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(); 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) { private void drainWorldGenerators(String reason, long timeoutSeconds) {
List<World> irisWorlds = new ArrayList<>(); List<World> irisWorlds = new ArrayList<>();
List<PlatformChunkGenerator> generators = new ArrayList<>(); List<PlatformChunkGenerator> generators = new ArrayList<>();
@@ -1115,6 +1180,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
} }
} }
if (generators.isEmpty()) { if (generators.isEmpty()) {
generatorDrainCompleted = true;
Iris.debug("No Iris worlds to freeze."); Iris.debug("No Iris worlds to freeze.");
return; return;
} }
@@ -1139,12 +1205,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
try { try {
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0])) CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
.get(timeoutSeconds, TimeUnit.SECONDS); .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) { } 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) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
Iris.warn("Iris generator drain interrupted; unload proceeding."); Iris.reportError("Iris generator drain was interrupted.", e);
} catch (ExecutionException e) { } catch (ExecutionException e) {
Iris.reportError(e.getCause() == null ? e : e.getCause()); Iris.reportError(e.getCause() == null ? e : e.getCause());
} }
@@ -765,13 +765,13 @@ public final class BukkitWorldReconciler {
@Override @Override
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) { public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
File snapshotRoot = snapshotRoot(worldKey);
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
String configuredWorldName = configuredWorldName(worldKey); String configuredWorldName = configuredWorldName(worldKey);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension); IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
if (irisDimension == null) { if (irisDimension == null) {
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\"."); throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
} }
File snapshotRoot = snapshotRoot(worldKey);
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
} }
private File snapshotRoot(NamespacedKey worldKey) { 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 " throw new IllegalStateException("Iris world storage does not match the current platform layout for "
+ worldKey + "."); + 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.Iris;
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging; 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.loader.IrisData;
import art.arcane.iris.core.pack.BrokenPackException; import art.arcane.iris.core.pack.BrokenPackException;
import art.arcane.iris.core.pack.PackDownloader; 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.spi.IrisPlatforms;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld; 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.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin; import art.arcane.iris.util.common.plugin.VolmitPlugin;
import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.bukkit.WorldIdentity;
@@ -255,15 +263,21 @@ public final class IrisWorldGeneratorResolver {
File levelRoot = IrisWorldStorage.levelRoot(); File levelRoot = IrisWorldStorage.levelRoot();
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot); NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName()); String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName());
File pack = IrisWorldStorage.frozenDimensionRoot( File dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
Bukkit.getWorldContainer(), Bukkit.getWorldContainer(),
levelRoot, levelRoot,
configuredWorldName, configuredWorldName,
worldKey worldKey
) )
.map(IrisWorldStorage::requireFrozenPackRoot)
.orElse(null); .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) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) { if (dimension == null) {
File packsRoot = IrisPlatforms.get().packsFolderNoCreate(); File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
@@ -445,7 +459,7 @@ public final class IrisWorldGeneratorResolver {
return; return;
} }
if (dimensionRoot != null if (dimensionRoot != null
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasFrozenPack(dimensionRoot))) { && (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasGenerationStorage(dimensionRoot))) {
return; return;
} }
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName()); NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
@@ -455,9 +469,10 @@ public final class IrisWorldGeneratorResolver {
+ " type=<pack>; Iris registers them with Multiverse itself."); + " type=<pack>; Iris registers them with Multiverse itself.");
} }
private static boolean hasFrozenPack(File dimensionRoot) { private static boolean hasGenerationStorage(File dimensionRoot) {
Path packRoot = dimensionRoot.toPath().toAbsolutePath().normalize().resolve("iris").resolve("pack"); GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot.toPath());
return Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS); return Files.isDirectory(paths.generationRoot(), LinkOption.NOFOLLOW_LINKS)
|| Files.isDirectory(paths.legacyPackRoot(), LinkOption.NOFOLLOW_LINKS);
} }
private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) { 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 " throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for "
+ worldKey + "."); + worldKey + ".");
} }
File snapshotRoot = IrisWorldStorage.requireFrozenPackRoot(dimensionRoot); long worldSeed = requireStoredWorldSeed(worldName, dimensionRoot);
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
File snapshotRoot = requireActivePack(history);
try { try {
requireSnapshotLoadable(snapshotRoot); requireSnapshotLoadable(snapshotRoot);
} catch (BrokenPackException exception) { } catch (BrokenPackException exception) {
@@ -487,18 +504,14 @@ public final class IrisWorldGeneratorResolver {
throw exception; throw exception;
} }
IrisDimension dimension = IrisData.get(snapshotRoot).getDimensionLoader().load(id, false); IrisDimension dimension = requireHistoricalDimension(history, snapshotRoot, id);
if (dimension == null) {
throw new IllegalStateException("Frozen Iris pack snapshot at " + snapshotRoot
+ " does not contain dimension " + id + ".");
}
Iris.debug("Assuming IrisDimension: " + dimension.getName()); Iris.debug("Assuming IrisDimension: " + dimension.getName());
IrisWorld world = IrisWorld.builder() IrisWorld world = IrisWorld.builder()
.platformIdentity(worldKey.toString()) .platformIdentity(worldKey.toString())
.name(worldName) .name(worldName)
.seed(1337) .seed(history.activeEpoch().worldSeed())
.worldFolder(dimensionRoot) .worldFolder(dimensionRoot)
.minHeight(dimension.getMinHeight()) .minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight()) .maxHeight(dimension.getMaxHeight())
@@ -506,7 +519,133 @@ public final class IrisWorldGeneratorResolver {
Iris.debug("Generator Config: " + world); 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( private record FreshValidation(
@@ -164,24 +164,28 @@ public final class PendingWorldReplacementManager implements Listener {
boolean configurationApplied = false; boolean configurationApplied = false;
try { try {
Files.createDirectory(paths.stage()); 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( long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
paths.target(), paths.target(),
paths.stage(), paths.stage(),
seedSelection 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) { if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId); 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); IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath()); String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
transaction = new Transaction( transaction = new Transaction(
@@ -806,7 +810,10 @@ public final class PendingWorldReplacementManager implements Listener {
validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind()); validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind());
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id()); ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
String fingerprint = WorldReplacementFilesystem.fingerprintPack( String fingerprint = WorldReplacementFilesystem.fingerprintPack(
paths.target().resolve("iris/pack")); IrisWorldStorage.requireActiveGenerationPackRoot(
paths.target().toFile(),
transaction.seed()
).toPath());
if (!transaction.packFingerprint().equals(fingerprint)) { if (!transaction.packFingerprint().equals(fingerprint)) {
throw new IOException("The replacement pack changed before runtime verification."); throw new IOException("The replacement pack changed before runtime verification.");
} }
@@ -176,7 +176,7 @@ public class CommandDeveloper implements DirectorExecutor {
return z ^ (z >>> 31); 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( public void updateWorld(
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true) @Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
World world, World world,
@@ -202,7 +202,7 @@ public class CommandDeveloper implements DirectorExecutor {
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH, LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
pack.getLoadKey() pack.getLoadKey()
)) { )) {
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder); Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
} catch (LifecycleOperationCoordinator.BusyException e) { } catch (LifecycleOperationCoordinator.BusyException e) {
sender().sendMessage(C.YELLOW + e.getMessage()); 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.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability; 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.HydrologyFeatureQuery;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision; 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.spi.PlatformStructureHooks;
import art.arcane.iris.util.common.director.DirectorExecutor; 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.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.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.director.specialhandlers.StructureHandler;
import art.arcane.iris.util.common.format.C; import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.plugin.VolmitSender; 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") @Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
public void 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, IrisRegion region,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true") @Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
boolean teleport boolean teleport
@@ -138,10 +140,6 @@ public class CommandFind implements DirectorExecutor {
return; return;
} }
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime(); IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
if (runtime == null) {
commandSender.sendMessage(C.YELLOW + "Hydrology is not active in this world.");
return;
}
HydrologyFeatureQuery query; HydrologyFeatureQuery query;
try { try {
query = HydrologyFeatureQuery.parse(type); query = HydrologyFeatureQuery.parse(type);
@@ -150,15 +148,19 @@ public class CommandFind implements DirectorExecutor {
return; return;
} }
Location origin = target.getLocation(); Location origin = target.getLocation();
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15); int requestedDistance = runtime == null
int maximumDistance = runtime.maximumFeatureSearchDistance( ? 8192
origin.getBlockX(), origin.getBlockZ(), requestedDistance); : 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 + "..."); commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
J.a(() -> { J.a(() -> {
try { try {
HydrologyFeatureRef feature = runtime.nearestFeature( GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
query.types(), activeEngine,
query.profileKey(), query,
origin.getBlockX(), origin.getBlockX(),
origin.getBlockZ(), origin.getBlockZ(),
maximumDistance, maximumDistance,
@@ -171,15 +173,14 @@ public class CommandFind implements DirectorExecutor {
+ maximumDistance + " blocks."); + maximumDistance + " blocks.");
return; return;
} }
int worldY = feature.y() + activeEngine.getDimension().getMinHeight();
String label = type + " hydrology feature"; String label = type + " hydrology feature";
if (!teleport) { if (!teleport) {
sendStructureMessage(target, commandSender, 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; return;
} }
Location destination = new Location( Location destination = new Location(
target.getWorld(), feature.x(), worldY, feature.z()); target.getWorld(), feature.x(), feature.y(), feature.z());
prepareStructureTeleport( prepareStructureTeleport(
target, target.getWorld(), commandSender, label, destination, true); target, target.getWorld(), commandSender, label, destination, true);
} catch (Throwable error) { } catch (Throwable error) {
@@ -233,6 +234,12 @@ public class CommandFind implements DirectorExecutor {
registered, decision, nativePlacement, locatableNativePlacement, registered, decision, nativePlacement, locatableNativePlacement,
locatableEditablePlacement, nativeGenerationEnabled, reachable); locatableEditablePlacement, nativeGenerationEnabled, reachable);
if (route != StructureLookupRoute.NATIVE && route != StructureLookupRoute.IRIS
&& GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)) {
locateIrisStructure(e, structureKey, commandSender);
return;
}
if (route == StructureLookupRoute.UNKNOWN) { if (route == StructureLookupRoute.UNKNOWN) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey))); commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
return; return;
@@ -672,7 +679,7 @@ public class CommandFind implements DirectorExecutor {
J.a(() -> { J.a(() -> {
try { try {
IrisStructureLocator.LocateResult result = 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) { if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
sendStructureMessage(target, commandSender, sendStructureMessage(target, commandSender,
C.YELLOW + "Unable to locate " + structure 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") @Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
public void 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, String object,
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true") @Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
boolean teleport 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); EngineBukkitOps.gotoObject(e, object, player(), teleport);
return; return;
} }
@@ -279,12 +279,14 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) { private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
dispatchSenders.set(sender); dispatchSenders.set(sender);
DirectorContext.touch(new VolmitSender(sender));
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) { 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))); return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
} catch (Throwable e) { } catch (Throwable e) {
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage()); Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
return DirectorExecutionResult.notHandled(); return DirectorExecutionResult.notHandled();
} finally { } finally {
DirectorContext.remove();
dispatchSenders.remove(); dispatchSenders.remove();
} }
} }
@@ -9,6 +9,40 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class IrisShutdownOrderingTest { 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 @Test
public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception { public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource"))); 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()")); 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 @Test
public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception { public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.startupSource"))); String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
@@ -43,6 +93,7 @@ public class IrisShutdownOrderingTest {
"finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)", "finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)",
"return;", "return;",
"awaitServerShutdownBoundary()", "awaitServerShutdownBoundary()",
"SHUTDOWN_ERRORS.println",
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)"); "finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
assertOrdered(finisher, assertOrdered(finisher,
"finisher.setDaemon(false)", "finisher.setDaemon(false)",
@@ -85,8 +136,8 @@ public class IrisShutdownOrderingTest {
assertOrdered(teardown, assertOrdered(teardown,
"drainWorldGenerators(reason, timeoutSeconds)", "drainWorldGenerators(reason, timeoutSeconds)",
"service.onDisable()", "service.onDisable()",
"MultiBurst.burst::close", "MultiBurst.burst.close()",
"MultiBurst.ioBurst::close"); "MultiBurst.ioBurst.close()");
assertTrue("Engine service must retain its generator close behind deferred service teardown", assertTrue("Engine service must retain its generator close behind deferred service teardown",
engineDisable.contains("startClose(")); engineDisable.contains("startClose("));
assertTrue("Studio service must retain its generator close behind deferred service teardown", assertTrue("Studio service must retain its generator close behind deferred service teardown",
@@ -195,7 +195,7 @@ public class IrisWorldGeneratorResolverTest {
} }
@Test @Test
public void configuredWorldResolutionUsesOnlyFrozenWorldLocalSnapshot() throws Exception { public void configuredWorldResolutionUsesOnlyActiveImmutableGenerationPack() throws Exception {
String source = Files.readString(Path.of( String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n"); "src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n");
int resolverStart = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator("); 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", "Frozen Iris world storage does not match the current platform layout",
currentPlatformRoot 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 validation = resolver.indexOf("requireSnapshotLoadable(snapshotRoot)");
int exactLoad = resolver.indexOf( int exactLoad = resolver.indexOf("requireHistoricalDimension(history, snapshotRoot, id)");
"IrisData.get(snapshotRoot).getDimensionLoader().load(id, false)"
);
int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())"); int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())");
int historicalSeed = resolver.indexOf(".seed(history.activeEpoch().worldSeed())");
int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)"); int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)");
assertTrue(dimensionRoot >= 0); assertTrue(dimensionRoot >= 0);
assertTrue(currentPlatformRoot > dimensionRoot); assertTrue(currentPlatformRoot > dimensionRoot);
assertTrue(layoutRefusal > currentPlatformRoot); assertTrue(layoutRefusal > currentPlatformRoot);
assertTrue(snapshotRoot > layoutRefusal); assertTrue(history > layoutRefusal);
assertTrue(snapshotRoot > history);
assertTrue(validation > snapshotRoot); assertTrue(validation > snapshotRoot);
assertTrue(exactLoad > validation); assertTrue(exactLoad > validation);
assertTrue(canonicalIdentity > exactLoad); assertTrue(canonicalIdentity > exactLoad);
assertTrue(resolvedStorage > canonicalIdentity); assertTrue(historicalSeed > canonicalIdentity);
assertTrue(resolvedStorage > historicalSeed);
assertFalse(resolver.contains("loadDimension(")); assertFalse(resolver.contains("loadDimension("));
assertFalse(resolver.contains("loadAnyDimension(")); assertFalse(resolver.contains("loadAnyDimension("));
assertFalse(resolver.contains("replaceIntoWorld(")); assertFalse(resolver.contains("replaceIntoWorld("));
assertFalse(resolver.contains("installIntoWorld(")); 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 @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("); String baseHeight = method(source, "public int getBaseHeight(");
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")")); assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)")); 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("); String generation = method(source, "public void generateNoise(");
assertTrue(generation.contains("throw new IllegalStateException")); 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) alias(libs.plugins.shadow)
} }
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
Properties rootProperties = new Properties() Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2')) 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:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(), "art.arcane:spi:${irisVersion}".toString(),
libs.paralithic, libs.paralithic,
libs.asm,
libs.lru, libs.lru,
libs.caffeine, libs.caffeine,
libs.dom4j, libs.dom4j,
+3
View File
@@ -26,6 +26,8 @@ plugins {
alias(libs.plugins.shadow) alias(libs.plugins.shadow)
} }
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
Properties rootProperties = new Properties() Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2')) 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:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(), "art.arcane:spi:${irisVersion}".toString(),
libs.paralithic, libs.paralithic,
libs.asm,
libs.lru, libs.lru,
libs.caffeine, libs.caffeine,
libs.dom4j, libs.dom4j,
@@ -1,9 +1,11 @@
package art.arcane.iris.nativegen; package art.arcane.iris.nativegen;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore; 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.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet; 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 net.minecraft.world.level.levelgen.structure.StructureStart;
import java.util.ArrayList; import java.util.ArrayList;
import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -48,14 +51,17 @@ public final class NativeStructureReferenceRepair {
originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) { originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) {
for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS; for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS;
originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) { originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) {
ChunkAccess originChunk = level.getChunk( try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS); openOriginRuntimeScope(engine, originChunkX, originChunkZ)) {
for (Map.Entry<Structure, StructureStart> entry : originChunk.getAllStarts().entrySet()) { ChunkAccess originChunk = level.getChunk(
ScannedStart start = scanStart( originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS);
engine, serverLevel, structureManager, targetChunk, registry, for (Map.Entry<Structure, StructureStart> entry : originChunk.getAllStarts().entrySet()) {
originChunk, entry.getKey(), entry.getValue()); ScannedStart start = scanStart(
if (start != null && isTargetRelevant(engine, targetChunk, start)) { engine, serverLevel, structureManager, targetChunk, registry,
starts.add(start); 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( private static boolean isTargetRelevant(
Engine engine, ChunkAccess targetChunk, ScannedStart scanned) { Engine engine, ChunkAccess targetChunk, ScannedStart scanned) {
StructureStart start = scanned.start(); StructureStart start = scanned.start();
@@ -1,11 +1,13 @@
package art.arcane.iris.nativegen; package art.arcane.iris.nativegen;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner; import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
import art.arcane.iris.engine.framework.NativeStructureStartPlan; import art.arcane.iris.engine.framework.NativeStructureStartPlan;
import art.arcane.iris.engine.framework.NativeStructureVolume; 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.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList; 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.StructureStart;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@@ -38,6 +41,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntBinaryOperator; import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
import java.util.function.Supplier; import java.util.function.Supplier;
/** /**
@@ -60,13 +64,14 @@ public final class NativeStructureVolumeIndex {
private final Context context; private final Context context;
private final OriginResolver origins; private final OriginResolver origins;
private final Map<Long, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS); private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
private final Map<Long, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS); private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> originBuilds = private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds = private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
private final ReentrantLock[] originWindowStripes = createOriginWindowStripes(); private final ReentrantLock[] originWindowStripes = createOriginWindowStripes();
private final IntConsumer retirementListener = this::evictRuntime;
private NativeStructureVolumeIndex(Context context) { private NativeStructureVolumeIndex(Context context) {
this.context = Objects.requireNonNull(context, "Native structure volume context must not be null"); 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) { public static void install(Engine engine, Context context) {
Objects.requireNonNull(engine, "Native structure volume index requires an engine"); Engine requiredEngine = Objects.requireNonNull(engine, "Native structure volume index requires an engine");
INDEXES.put(engine, new NativeStructureVolumeIndex(context)); 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) { public static void uninstall(Engine engine) {
if (engine != null) { 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) { 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 fromChunkX = Math.min(minX, maxX) >> 4;
int toChunkX = Math.max(minX, maxX) >> 4; int toChunkX = Math.max(minX, maxX) >> 4;
int fromChunkZ = Math.min(minZ, maxZ) >> 4; int fromChunkZ = Math.min(minZ, maxZ) >> 4;
@@ -128,7 +146,7 @@ public final class NativeStructureVolumeIndex {
KList<NativeStructureVolume> matches = null; KList<NativeStructureVolume> matches = null;
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) { for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) { 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)) { if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
continue; continue;
} }
@@ -144,8 +162,13 @@ public final class NativeStructureVolumeIndex {
return matches == null ? NativeStructureVolume.NONE : matches; return matches == null ? NativeStructureVolume.NONE : matches;
} }
private KList<NativeStructureVolume> chunkVolumes(Engine engine, int chunkX, int chunkZ) { private KList<NativeStructureVolume> chunkVolumes(
return cached(queryCache, queryBuilds, chunkKey(chunkX, chunkZ), Engine engine,
int runtimeId,
int chunkX,
int chunkZ
) {
return cached(queryCache, queryBuilds, new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
() -> buildChunkVolumes(engine, chunkX, chunkZ)); () -> buildChunkVolumes(engine, chunkX, chunkZ));
} }
@@ -196,8 +219,16 @@ public final class NativeStructureVolumeIndex {
} }
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) { KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
return cached(originCache, originBuilds, chunkKey(chunkX, chunkZ), try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
() -> origins.volumesAt(engine, chunkX, chunkZ)); 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) { private KList<NativeStructureVolume> buildOriginVolumes(Engine engine, int chunkX, int chunkZ) {
@@ -362,9 +393,23 @@ public final class NativeStructureVolumeIndex {
return target; return target;
} }
private KList<NativeStructureVolume> cached(Map<Long, KList<NativeStructureVolume>> cache, void evictRuntime(int runtimeId) {
ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> builds, synchronized (originCache) {
long key, Supplier<KList<NativeStructureVolume>> loader) { 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) { synchronized (cache) {
KList<NativeStructureVolume> hit = cache.get(key); KList<NativeStructureVolume> hit = cache.get(key);
if (hit != null) { if (hit != null) {
@@ -392,7 +437,9 @@ public final class NativeStructureVolumeIndex {
try { try {
KList<NativeStructureVolume> built = loader.get(); KList<NativeStructureVolume> built = loader.get();
synchronized (cache) { synchronized (cache) {
cache.put(key, built); if (builds.get(key) == future) {
cache.put(key, built);
}
} }
future.complete(built); future.complete(built);
return 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) { return new LinkedHashMap<>(16, 0.75F, true) {
@Override @Override
protected boolean removeEldestEntry(Map.Entry<Long, KList<NativeStructureVolume>> eldest) { protected boolean removeEldestEntry(
Map.Entry<RuntimeChunkKey, KList<NativeStructureVolume>> eldest
) {
return size() > capacity; 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 * 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. * 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.DimensionStackContext;
import art.arcane.iris.engine.DimensionStackLayout; import art.arcane.iris.engine.DimensionStackLayout;
import art.arcane.iris.engine.DimensionTerrainContext; 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.Engine;
import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
@@ -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.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet; import net.minecraft.world.level.levelgen.structure.StructureSet;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
@@ -72,7 +75,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
private final BiomeSource serializedSource; private final BiomeSource serializedSource;
private final Set<String> warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet(); 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 // 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. // surviving a repoint with the previous pack's content.
private final AtomicLong packGeneration = new AtomicLong(); private final AtomicLong packGeneration = new AtomicLong();
@@ -105,9 +107,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
possibleStructureBiomeKeys = null; possibleStructureBiomeKeys = null;
biomeKeySets = null; biomeKeySets = null;
possibleBiomesCache = 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)); StructureStateBiomeSource source = new StructureStateBiomeSource(this, Set.copyOf(possible));
structureStateSources.add(source);
return source; return source;
} }
@@ -308,15 +312,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (engine == null) { if (engine == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler); return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
} }
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome"); GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
if (lease == null) { engine, quartX << 2, quartZ << 2, "modded_structure_biome");
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition"); try (historyScope) {
} GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { if (lease == null) {
if (!isReady(engine)) { throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime"); }
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) QuartPos.toBlock(quartZ)
); );
long key = packNoiseKey(quartX, quartY, quartZ); long key = packNoiseKey(quartX, quartY, quartZ);
int runtimeIdentity = engine.getCacheID();
BiomeHolderTable cache = structureBiomeCache; BiomeHolderTable cache = structureBiomeCache;
if (cacheable) { if (cacheable) {
Holder<Biome> cached = cache.get(key); Holder<Biome> cached = cache.get(runtimeIdentity, key);
if (cached != null) { if (cached != null) {
return cached; return cached;
} }
} }
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler); Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
if (cacheable) { if (cacheable) {
cache.put(key, resolved); cache.put(runtimeIdentity, key, resolved);
} }
return resolved; return resolved;
} }
@@ -413,16 +422,20 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (engine == null) { if (engine == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler); return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
} }
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome"); GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
if (lease == null) { engine, quartX << 2, quartZ << 2, "modded_visible_biome");
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition"); try (historyScope) {
} GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { if (lease == null) {
if (!isReady(engine)) { throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime"); }
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) QuartPos.toBlock(quartZ)
); );
long key = packNoiseKey(quartX, quartY, quartZ); long key = packNoiseKey(quartX, quartY, quartZ);
int runtimeIdentity = engine.getCacheID();
BiomeHolderTable cache = visibleBiomeCache; BiomeHolderTable cache = visibleBiomeCache;
if (cacheable) { if (cacheable) {
Holder<Biome> cached = cache.get(key); Holder<Biome> cached = cache.get(runtimeIdentity, key);
if (cached != null) { if (cached != null) {
return cached; return cached;
} }
} }
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler); Holder<Biome> resolved = resolveVisibleBiome(
engine, quartX, quartY, quartZ, sampler, null);
if (cacheable) { if (cacheable) {
cache.put(key, resolved); cache.put(runtimeIdentity, key, resolved);
} }
return resolved; return resolved;
} }
@@ -458,45 +473,50 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (engine == null) { if (engine == null) {
return null; return null;
} }
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome"); GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
if (lease == null) { engine, blockX, blockZ, "modded_surface_spawn_biome");
return null; try (historyScope) {
} GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { if (lease == null) {
if (!isReady(engine)) {
return null; return null;
} }
Registry<Biome> registry = biomeRegistry(); try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (registry == null) { if (!isReady(engine)) {
return 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;
} }
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) { if (lease == null) {
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition"); throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
} }
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { boolean surfaceQuery;
if (!isReady(engine)) { try (lease) {
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime"); 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( if (!surfaceQuery) {
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
return super.getBiomesWithin(x, y, z, radius, sampler); return super.getBiomesWithin(x, y, z, radius, sampler);
} }
int minQuartX = QuartPos.fromBlock(x - radius); int minQuartX = QuartPos.fromBlock(x - radius);
@@ -552,7 +578,11 @@ final class IrisModdedBiomeSource extends BiomeSource {
Set<Holder<Biome>> biomes = new HashSet<>(columns); Set<Holder<Biome>> biomes = new HashSet<>(columns);
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) { for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) { 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; return biomes;
@@ -562,13 +592,14 @@ final class IrisModdedBiomeSource extends BiomeSource {
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ, private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
Climate.Sampler sampler) { Climate.Sampler sampler) {
long key = packColumnKey(quartX, quartZ); long key = packColumnKey(quartX, quartZ);
int runtimeIdentity = engine.getCacheID();
BiomeHolderTable cache = surfaceStructureBiomeCache; BiomeHolderTable cache = surfaceStructureBiomeCache;
Holder<Biome> cached = cache.get(key); Holder<Biome> cached = cache.get(runtimeIdentity, key);
if (cached != null) { if (cached != null) {
return cached; return cached;
} }
Holder<Biome> resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler); Holder<Biome> resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
cache.put(key, resolved); cache.put(runtimeIdentity, key, resolved);
return resolved; return resolved;
} }
@@ -618,11 +649,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
return resolved; 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( private Holder<Biome> resolveVisibleBiome(
Engine engine, Engine engine,
int quartX, int quartX,
@@ -637,20 +663,21 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (resolution == null || registry == null) { if (resolution == null || registry == null) {
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler); return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
} }
return resolveVisibleBiome( String historicalBiomeKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
engine, registry, resolution, quartX, quartY, quartZ, sampler); resolution.blockX(),
} resolution.blockY(),
resolution.blockZ()).orElse(null);
private Holder<Biome> resolveVisibleBiome( if (historicalBiomeKey != null) {
Engine engine, Holder<Biome> historical = resolveHolder(registry, historicalBiomeKey);
Registry<Biome> registry, if (historical == null) {
BiomeResolution resolution, throw new IllegalStateException("Historical Iris biome '" + historicalBiomeKey
int quartX, + "' is not registered at block " + resolution.blockX() + ","
int quartY, + resolution.blockY() + "," + resolution.blockZ());
int quartZ, }
Climate.Sampler sampler return historical;
) { }
String biomeKey; String biomeKey;
String unresolvedBiomeKey;
if (resolution.irisBiome().isCustom()) { if (resolution.irisBiome().isCustom()) {
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome( IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ()); resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
@@ -658,18 +685,27 @@ final class IrisModdedBiomeSource extends BiomeSource {
return fallbackBiome(registry, "custom derivative of '" return fallbackBiome(registry, "custom derivative of '"
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler); + resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
} }
biomeKey = customBiomeRef( String logicalBiomeKey = customBiomeRef(
resolution.packName(), resolution.dimension(), customBiome.getId()); 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()) { } else if (resolution.underground()) {
biomeKey = resolution.irisBiome().getGroundBiomeKey( biomeKey = resolution.irisBiome().getGroundBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ()); resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
unresolvedBiomeKey = biomeKey;
} else { } else {
biomeKey = resolution.irisBiome().getSkyBiomeKey( biomeKey = resolution.irisBiome().getSkyBiomeKey(
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ()); resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
unresolvedBiomeKey = biomeKey;
} }
Holder<Biome> resolved = resolveHolder(registry, biomeKey); Holder<Biome> resolved = resolveHolder(registry, biomeKey);
return resolved == null return resolved == null
? fallbackBiome(registry, biomeKey, quartX, quartY, quartZ, sampler) ? fallbackBiome(registry, unresolvedBiomeKey, quartX, quartY, quartZ, sampler)
: resolved; : 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; IrisModdedChunkGenerator current = generator;
if (current == null) { if (current == null) {
throw new IllegalStateException("Iris structure biome source is not bound to its generator"); throw new IllegalStateException("Iris structure biome source is not bound to its generator");
} }
Engine engine = current.awaitStructureEngine(); Engine engine = current.awaitStructureEngine();
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome"); GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
if (lease == null) { engine, quartX << 2, quartZ << 2, "modded_structure_state_biome");
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition"); try (historyScope) {
} GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { if (lease == null) {
if (!isReady(engine)) { throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
} }
Registry<Biome> registry = biomeRegistry(); try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (registry == null) { if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome lookup has no biome registry"); 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(); 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) { private GenerationSessionLease tryAcquireGenerationLease(Engine engine, String operation) {
if (engine == null || engine.isClosed()) { if (engine == null || engine.isClosed()) {
return null; return null;
@@ -950,8 +997,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
if (!isReady(engine)) { if (!isReady(engine)) {
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime"); throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
} }
Set<String> structureRequired = collectStructureBiomeKeys( LinkedHashSet<String> structureRequired = new LinkedHashSet<>(collectStructureBiomeKeys(
engine.getDimension(), engine.getData()); engine.getDimension(), engine.getData()));
structureRequired.addAll(IrisModdedChunkGenerator.retainedBiomeKeys(engine));
LinkedHashSet<String> visibleRequired = new LinkedHashSet<>(); LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>(); LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
boolean visibleFallbackPossible = false; boolean visibleFallbackPossible = false;
@@ -959,11 +1007,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
IrisBiome irisBiome = ownedBiome.biome(); IrisBiome irisBiome = ownedBiome.biome();
if (irisBiome.isCustom()) { if (irisBiome.isCustom()) {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
String customKey = customBiomeRef( String customKey = ownedBiome.data().customBiomeResourceKey(
ownedBiome.data().getDataFolder().getName(), ownedBiome.dimension(), customBiome);
ownedBiome.dimension(),
customBiome.getId()
);
visibleRequired.add(customKey); visibleRequired.add(customKey);
visibleEmittable.add(customKey); visibleEmittable.add(customKey);
} }
@@ -1035,6 +1080,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
irisBiome, irisBiome,
underground, underground,
dimension, dimension,
data,
data.getDataFolder().getName(), data.getDataFolder().getName(),
blockX, blockX,
blockY, blockY,
@@ -1181,6 +1227,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
IrisBiome irisBiome, IrisBiome irisBiome,
boolean underground, boolean underground,
IrisDimension dimension, IrisDimension dimension,
IrisData data,
String packName, String packName,
int blockX, int blockX,
int blockY, int blockY,
@@ -1224,35 +1271,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
private volatile AtomicReferenceArray<Entry> table; private volatile AtomicReferenceArray<Entry> table;
Holder<Biome> get(long key) { Holder<Biome> get(int runtimeIdentity, long key) {
AtomicReferenceArray<Entry> slots = table; AtomicReferenceArray<Entry> slots = table;
if (slots == null) { if (slots == null) {
return null; return null;
} }
int home = home(key); int home = home(runtimeIdentity, key);
for (int probe = 0; probe < PROBE_LIMIT; probe++) { for (int probe = 0; probe < PROBE_LIMIT; probe++) {
Entry entry = slots.get((home + probe) & MASK); Entry entry = slots.get((home + probe) & MASK);
if (entry == null) { if (entry == null) {
return null; return null;
} }
if (entry.key() == key) { if (entry.runtimeIdentity() == runtimeIdentity && entry.key() == key) {
return entry.value(); return entry.value();
} }
} }
return null; return null;
} }
void put(long key, Holder<Biome> value) { void put(int runtimeIdentity, long key, Holder<Biome> value) {
AtomicReferenceArray<Entry> slots = table; AtomicReferenceArray<Entry> slots = table;
if (slots == null) { if (slots == null) {
slots = install(); slots = install();
} }
int home = home(key); int home = home(runtimeIdentity, key);
Entry entry = new Entry(key, value); Entry entry = new Entry(runtimeIdentity, key, value);
for (int probe = 0; probe < PROBE_LIMIT; probe++) { for (int probe = 0; probe < PROBE_LIMIT; probe++) {
int slot = (home + probe) & MASK; int slot = (home + probe) & MASK;
Entry existing = slots.get(slot); Entry existing = slots.get(slot);
if (existing == null || existing.key() == key) { if (existing == null
|| (existing.runtimeIdentity() == runtimeIdentity && existing.key() == key)) {
slots.set(slot, entry); slots.set(slot, entry);
return; return;
} }
@@ -1260,6 +1308,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
slots.set(home, entry); 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() { private synchronized AtomicReferenceArray<Entry> install() {
AtomicReferenceArray<Entry> existing = table; AtomicReferenceArray<Entry> existing = table;
if (existing != null) { if (existing != null) {
@@ -1270,29 +1331,25 @@ final class IrisModdedBiomeSource extends BiomeSource {
return created; return created;
} }
private static int home(long key) { private static int home(int runtimeIdentity, long key) {
long mixed = key * 0x9E3779B97F4A7C15L; long mixed = (key ^ ((long) runtimeIdentity * 0xC2B2AE3D27D4EB4FL))
* 0x9E3779B97F4A7C15L;
return (int) ((mixed ^ (mixed >>> 32)) & MASK); 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 static final class StructureStateBiomeSource extends BiomeSource {
private final IrisModdedBiomeSource delegate; private final IrisModdedBiomeSource delegate;
private final Set<Holder<Biome>> possibleBiomes; private final Set<Holder<Biome>> possibleBiomes;
private volatile BiomeHolderTable resolvedBiomes = new BiomeHolderTable();
private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set<Holder<Biome>> possibleBiomes) { private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set<Holder<Biome>> possibleBiomes) {
this.delegate = delegate; this.delegate = delegate;
this.possibleBiomes = possibleBiomes; this.possibleBiomes = possibleBiomes;
} }
private void clearCache() {
resolvedBiomes = new BiomeHolderTable();
}
@Override @Override
protected MapCodec<? extends BiomeSource> codec() { protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Structure state biome sources are not serializable"); throw new UnsupportedOperationException("Structure state biome sources are not serializable");
@@ -1305,15 +1362,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
@Override @Override
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
long key = packNoiseKey(x, y, z); return delegate.resolveRequiredStructureBiome(x, y, z, sampler);
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;
} }
@Override @Override
@@ -20,11 +20,17 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine; 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.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease; 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.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.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl; import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.nativegen.NativeStructureStartInjector; 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.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.chunk.LevelChunkSection; 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.Heightmap;
import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomState; 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 net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.IntBinaryOperator; import java.util.function.IntBinaryOperator;
import java.util.function.IntConsumer;
public final class IrisModdedChunkGenerator extends ChunkGenerator { public final class IrisModdedChunkGenerator extends ChunkGenerator {
// Vanilla-shaped fallback for an unbound generator (matches IrisDimension defaults). getMinY, // 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 ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
private final ModdedImportedFeatureStage importedFeatures; private final ModdedImportedFeatureStage importedFeatures;
private final AtomicBoolean announced = new AtomicBoolean(false); private final AtomicBoolean announced = new AtomicBoolean(false);
private final IntConsumer generationRuntimeRetirementListener = this::retireGenerationRuntimeCaches;
private volatile boolean unloading; private volatile boolean unloading;
private volatile Engine engine; private volatile Engine engine;
private volatile String activePack; private volatile String activePack;
private volatile String activeDimensionKey; 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 seedOverride = Long.MIN_VALUE;
private volatile long lastChunkGenAt = 0L; private volatile long lastChunkGenAt = 0L;
private volatile Set<String> configuredStructureBiomeKeys; private volatile Set<String> configuredStructureBiomeKeys;
private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack; private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack;
private volatile ModdedDimensionMetadata.DimensionMetadata heightMetadata; private volatile ModdedDimensionMetadata.DimensionMetadata heightMetadata;
private volatile ServerLevel boundLevel; private volatile ServerLevel boundLevel;
private volatile IrisEngine retirementListenerEngine;
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) { public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource)); 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) { 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(); ServerLevel level = boundLevel();
if (level != null) { if (level != null) {
this.generationMode = generationMode;
repointAndBind(level, pack, packDimensionKey, seed); repointAndBind(level, pack, packDimensionKey, seed);
return; return;
} }
applyUnboundConfiguration(pack, packDimensionKey, seed); applyUnboundConfiguration(pack, packDimensionKey, seed, packRoot, generationMode);
} }
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) { synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
@@ -185,7 +230,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
requireGlobalStructureGeneration( requireGlobalStructureGeneration(
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey); level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
Engine activeEngine = engineIfBound(); Engine activeEngine = engineIfBound();
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed); Engine replacement = ModdedWorldEngines.prepareReplacement(
level,
pack,
packDimensionKey,
seed,
generationMode
);
try { try {
if (activeEngine != null) { if (activeEngine != null) {
requireStructureBiomeUniverseCompatible( requireStructureBiomeUniverseCompatible(
@@ -213,13 +264,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.activePack = pack; this.activePack = pack;
this.activeDimensionKey = packDimensionKey; this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed; this.seedOverride = seed;
this.engine = replacement;
bindGenerationRuntimeRetirementListener(replacement);
this.immutablePackRoot = replacement.getData().getDataFolder().toPath().toAbsolutePath().normalize();
this.configuredStructureBiomeKeys = null; this.configuredStructureBiomeKeys = null;
this.configuredPack = null; this.configuredPack = null;
this.heightMetadata = engineHeights(replacement); this.heightMetadata = engineHeights(replacement);
this.engineBinding.reset(); this.engineBinding.reset();
this.announced.set(false); this.announced.set(false);
resetRuntimeCaches(); resetRuntimeCaches();
this.engine = replacement;
this.engineBinding.complete(replacement); this.engineBinding.complete(replacement);
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off. // 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 // 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() { private void clearEngineBinding() {
unbindGenerationRuntimeRetirementListener();
this.engine = null; this.engine = null;
this.boundLevel = null; this.boundLevel = null;
this.configuredStructureBiomeKeys = null; this.configuredStructureBiomeKeys = null;
this.configuredPack = null; this.configuredPack = null;
this.immutablePackRoot = null;
this.engineBinding.reset(); this.engineBinding.reset();
this.announced.set(false); this.announced.set(false);
this.structureBiomeSource.clearCaches(); this.structureBiomeSource.clearCaches();
@@ -254,10 +309,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
this.spawnTables.resetVanillaSpawnBiomes(); 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.activePack = pack;
this.activeDimensionKey = packDimensionKey; this.activeDimensionKey = packDimensionKey;
this.seedOverride = seed; this.seedOverride = seed;
this.immutablePackRoot = packRoot == null ? null : packRoot.toAbsolutePath().normalize();
this.generationMode = Objects.requireNonNull(generationMode, "generationMode");
this.engine = null; this.engine = null;
this.boundLevel = null; this.boundLevel = null;
this.configuredStructureBiomeKeys = null; this.configuredStructureBiomeKeys = null;
@@ -272,7 +336,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
} }
public synchronized void resetToDefault() { public synchronized void resetToDefault() {
repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE); repoint(
defaultPack,
defaultDimensionKey,
Long.MIN_VALUE,
null,
ModdedGenerationMode.PERSISTENT_RESTORE
);
} }
public String activePack() { public String activePack() {
@@ -312,7 +382,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
BlockPos pos, int radius, BlockPos pos, int radius,
boolean findUnexplored) { boolean findUnexplored) {
Engine current = engine(); 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)) { IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures( HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures(
level, holders, current); level, holders, current);
@@ -481,14 +556,22 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return existing; return existing;
} }
try { try {
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride); Engine created = ModdedWorldEngines.get(
level,
activePack,
activeDimensionKey,
seedOverride,
generationMode
);
requireCompletedShutdown(created); requireCompletedShutdown(created);
if (created.isClosed() || created.getComplex() == null) { if (created.isClosed() || created.getComplex() == null) {
throw new IllegalStateException("Iris generator '" + dimensionKey throw new IllegalStateException("Iris generator '" + dimensionKey
+ "' created an engine without a ready biome complex"); + "' created an engine without a ready biome complex");
} }
engine = created; engine = created;
bindGenerationRuntimeRetirementListener(created);
boundLevel = level; boundLevel = level;
immutablePackRoot = created.getData().getDataFolder().toPath().toAbsolutePath().normalize();
heightMetadata = engineHeights(created); heightMetadata = engineHeights(created);
configuredStructureBiomeKeys = null; configuredStructureBiomeKeys = null;
structureBiomeSource.clearCaches(); structureBiomeSource.clearCaches();
@@ -649,10 +732,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
} }
Engine current = engine; Engine current = engine;
if (current != null && !current.isClosed() && !current.isClosing()) { 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"); try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) { IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys( Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(current);
current.getAllBiomes(), current.getDimension().getLoadKey());
configuredStructureBiomeKeys = resolved; configuredStructureBiomeKeys = resolved;
return resolved; return resolved;
} catch (GenerationSessionException e) { } 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) { private static ModdedDimensionMetadata.DimensionMetadata engineHeights(Engine engine) {
IrisDimension dimension = engine.getDimension(); IrisDimension dimension = engine.getDimension();
int minY = engine.getMinHeight(); int minY = engine.getMinHeight();
@@ -713,8 +821,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
if (configuredPack != null) { if (configuredPack != null) {
return configuredPack; return configuredPack;
} }
PackValidationRegistry.requireLoadable(activePack); Path packRoot = immutablePackRoot;
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey); 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); IrisData data = IrisData.get(packDirectory);
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey); IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
if (dimension == null) { if (dimension == null) {
@@ -800,36 +915,54 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override @Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt( public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) { Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = biome.value().getMobSettings().getMobs(category); Engine current = engine();
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt( try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
biome, structureManager, category, pos); current, pos.getX(), pos.getZ(), "modded_mob_spawn_table");
if (resolvedSpawns != explicitSpawns) { GenerationSessionLease lease = requireGenerationLease(current, "modded_mob_spawn_table");
return resolvedSpawns; 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); Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry); spawnTables.initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value()); Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
if (vanillaSpawnBiome == null) { if (vanillaSpawnBiome == null) {
return explicitSpawns; return explicitSpawns;
} }
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category); WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns =
if (explicitSpawns.isEmpty()) { vanillaSpawnBiome.value().getMobSettings().getMobs(category);
return vanillaSpawns; if (explicitSpawns.isEmpty()) {
} return vanillaSpawns;
if (vanillaSpawns.isEmpty()) { }
return explicitSpawns; if (vanillaSpawns.isEmpty()) {
} return explicitSpawns;
}
return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns); return spawnTables.mergedSpawnTable(
current,
biome.value(),
category,
vanillaSpawns,
explicitSpawns
);
}
} }
@Override @Override
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender, public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
StructureManager structureManager, ChunkAccess chunk) { StructureManager structureManager, ChunkAccess chunk) {
Engine current = engine(); 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)) { IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler()); chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
return CompletableFuture.completedFuture(chunk); return CompletableFuture.completedFuture(chunk);
@@ -844,19 +977,35 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
ModdedIrisLog.debug("Iris generating chunk {},{}", pos.x(), pos.z()); ModdedIrisLog.debug("Iris generating chunk {},{}", pos.x(), pos.z());
PlatformBlockState air = IrisPlatforms.get().registries().air(); PlatformBlockState air = IrisPlatforms.get().registries().air();
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
generationEngine, pos.x(), pos.z(), "modded_chunk_pipeline");
if (ModdedGenPool.parallelChunkSystem()) { try {
return CompletableFuture.completedFuture( if (ModdedGenPool.parallelChunkSystem()) {
generateTerrain(chunk, generationEngine, pos, air)); 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, private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
PlatformBlockState air) { PlatformBlockState air,
try (GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline"); 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)) { IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) {
if (announced.compareAndSet(false, true)) { if (announced.compareAndSet(false, true)) {
ModdedIrisLog.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})", 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,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES)); Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z()); ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
if (route != null) {
route.claimGeneratedSemantics();
}
return chunk; return chunk;
} catch (GenerationSessionException e) { } catch (GenerationSessionException e) {
if (generationEngine.isClosing() || e.isExpectedTeardown()) { if (generationEngine.isClosing() || e.isExpectedTeardown()) {
@@ -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) { private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
int baseX = pos.getMinBlockX(); int baseX = pos.getMinBlockX();
int baseZ = pos.getMinBlockZ(); int baseZ = pos.getMinBlockZ();
@@ -961,18 +1174,47 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override @Override
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) { 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 @Override
public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) { 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 @Override
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) { public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
Engine current = engine(); Engine current = engine(level.getLevel());
importedFeatures.prepare(current); ChunkPos chunkPos = chunk.getPos();
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration"); 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)) { 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); nativeStructures.placeVanillaStructures(level, chunk, structureManager);
// Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk // Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk
// step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the // step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the
@@ -984,8 +1226,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override @Override
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) { public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
Engine current = engine(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)) { 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()); Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey); super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject( Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
@@ -1008,7 +1263,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override @Override
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) { public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
Engine current = engine(level.getLevel()); 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)) { IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
NativeStructureReferenceRepair.createReferences( NativeStructureReferenceRepair.createReferences(
current, level, structureManager, chunk); current, level, structureManager, chunk);
@@ -1021,25 +1280,42 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
@Override @Override
public void spawnOriginalMobs(WorldGenRegion region) { public void spawnOriginalMobs(WorldGenRegion region) {
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry);
ChunkPos center = region.getCenter(); ChunkPos center = region.getCenter();
Holder<Biome> visibleBiome; Engine current = engine(region.getLevel());
if (engine == null || engine.getDimensionStackContext() == null) { try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); current, center.x(), center.z(), "modded_spawn_original_mobs");
} else { GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
visibleBiome = structureBiomeSource.getVisibleSurfaceBiome( GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_original_mobs");
center.getMinBlockX() + 8, IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
center.getMinBlockZ() + 8); ChunkAccess centerChunk = region.getChunk(center.x(), center.z());
if (visibleBiome == null) { if (!allowsRoutedDiscreteGeneration(
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY())); 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 @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) { public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
Engine current = requireDataQueryEngine("base height"); Engine current = requireDataQueryEngine("base height");
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState()); 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)) { IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
int height = current.getDimensionStackContext() == null int height = current.getDimensionStackContext() == null
? current.getHeight(x, z, ignoreFluid) ? current.getHeight(x, z, ignoreFluid)
@@ -1091,7 +1369,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
int minY = heightAccessor.getMinY(); int minY = heightAccessor.getMinY();
BlockState[] states = new BlockState[heightAccessor.getHeight()]; BlockState[] states = new BlockState[heightAccessor.getHeight()];
Engine current = requireDataQueryEngine("base column"); 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)) { IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
BlockState airState = Blocks.AIR.defaultBlockState(); BlockState airState = Blocks.AIR.defaultBlockState();
boolean dimensionStack = current.getDimensionStackContext() != null; 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) { GenerationSessionLease requireGenerationLease(Engine current, String operation) {
try { try {
return current.acquireGenerationLease(operation); 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 * 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 * instance root is the JVM working directory - not configDir().getParent(), which points somewhere else
@@ -18,6 +18,9 @@
package art.arcane.iris.modded; 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.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -98,11 +102,11 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
return key; return key;
} }
Engine engine = context.getEngine(); Engine engine = context.getEngine();
String prefix = engine.getDimension().getCustomBiomeKeyPrefix(); BiomeOwner owner = findCustomBiomeOwner(engine, key);
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) { if (owner != null) {
return key; return owner.physicalKey();
} }
return ModdedWorldgenIds.biomeRef(engine, key.substring(prefix.length())); return engine.getData().physicalBiomeResourceKey(engine.getDimension(), key);
} }
@Override @Override
@@ -139,31 +143,81 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
} }
private int idForDerivative(Registry<Biome> registry, String key) { private int idForDerivative(Registry<Biome> registry, String key) {
IrisBiome owner = findCustomBiomeOwner(key); BiomeOwner owner = findCustomBiomeOwner(key);
if (owner == null) { if (owner == null) {
return -1; return -1;
} }
String derivativeKey = owner.getVanillaDerivativeKey(); String derivativeKey = owner.biome().getVanillaDerivativeKey();
if (derivativeKey == null) { if (derivativeKey == null) {
return -1; return -1;
} }
return idForKey(registry, derivativeKey); return idForKey(registry, derivativeKey);
} }
private IrisBiome findCustomBiomeOwner(String key) { private BiomeOwner findCustomBiomeOwner(String key) {
for (Engine engine : ModdedWorldEngines.activeEngines()) { for (Engine engine : ModdedWorldEngines.activeEngines()) {
if (engine == null || engine.isClosed()) { if (engine == null || engine.isClosed()) {
continue; continue;
} }
IrisDimension dimension = engine.getDimension(); BiomeOwner owner = findCustomBiomeOwner(engine, key);
for (IrisBiome biome : dimension.getAllBiomes(engine)) { if (owner != null) {
if (!biome.isCustom()) { return owner;
continue; }
} }
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) { return null;
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))) { }
return biome;
} 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; this.registry = registry;
} }
} }
private record BiomeOwner(IrisDimension dimension, IrisData data, IrisBiome biome, String physicalKey) {
}
} }
@@ -19,7 +19,9 @@
package art.arcane.iris.modded; package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.GenerationActivation;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder; 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.ServerLevelData;
import net.minecraft.world.level.storage.WorldData; import net.minecraft.world.level.storage.WorldData;
import java.io.File; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -120,14 +125,76 @@ public final class ModdedDimensionManager {
return generator.commandEngine(); return generator.commandEngine();
} }
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) { public static Handle createTransientStudio(
ModdedStartup.requirePackForWorldCreation(pack); 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(); ModdedServerAccess serverAccess = requireAccess();
synchronized (LOCK) { synchronized (LOCK) {
ResourceKey<Level> key = levelKey(dimensionId); 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); Handle existing = HANDLES.get(dimensionId);
if (existing != null && serverAccess.hasLevel(server, key)) { 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()); Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
HANDLES.put(dimensionId, refreshed); HANDLES.put(dimensionId, refreshed);
return 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"); 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); 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); Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
HANDLES.put(dimensionId, handle); HANDLES.put(dimensionId, handle);
return handle; return handle;
} }
try { 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); HANDLES.put(dimensionId, handle);
ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed); ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
return handle; return handle;
@@ -157,28 +232,109 @@ public final class ModdedDimensionManager {
} }
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) { public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
ModdedDimensionRegistryStore.PersistentDimension previous = synchronized (LOCK) {
ModdedDimensionRegistryStore.get(server, dimensionId); ModdedDimensionRegistryStore.PersistentDimension previous =
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed)); ModdedDimensionRegistryStore.get(server, dimensionId);
try { if (previous != null) {
return create(server, dimensionId, pack, packDimensionKey, seed); throw new IllegalStateException("Iris dimension '" + dimensionId
} catch (Throwable e) { + "' already exists. Use the Iris world update command to stage a pack update.");
}
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(
dimensionId,
pack,
packDimensionKey,
seed
));
try { try {
if (previous == null) { return create(
server,
dimensionId,
pack,
packDimensionKey,
seed,
ModdedGenerationMode.PERSISTENT_CREATE
);
} catch (Throwable e) {
try {
ModdedDimensionRegistryStore.remove(server, dimensionId); ModdedDimensionRegistryStore.remove(server, dimensionId);
} else { } catch (Throwable rollbackFailure) {
ModdedDimensionRegistryStore.put(server, previous); e.addSuppressed(rollbackFailure);
} }
} catch (Throwable rollbackFailure) { if (e instanceof RuntimeException runtimeException) {
e.addSuppressed(rollbackFailure); 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) { ModdedDimensionRegistryStore.PersistentDimension updated =
throw fatalError; 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); 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); Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
IrisDimension dimension = loadPackDimension(pack, packDimensionKey); String typeRef = runtimePack.dimensionTypeKey();
String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey);
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef)); ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
ModdedRuntimeRegistry.ensureCustomBiomes(registryAccess, dimension, pack);
ModdedRuntimeRegistry.ensureDimensionType(registry, typeKey, typeRef);
return ModdedForcedDatapack.requireRegisteredDimensionType( return ModdedForcedDatapack.requireRegisteredDimensionType(
typeRef, registry.get(typeKey), pack, packDimensionKey); typeRef, registry.get(typeKey), pack, packDimensionKey);
} }
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) { private static Handle inject(
try { MinecraftServer server,
File packFolder = ModdedWorldEngines.packFolder(pack); ModdedServerAccess serverAccess,
if (!packFolder.isDirectory()) { String dimensionId,
throw new IllegalStateException("Iris pack directory is missing: " + packFolder.getAbsolutePath()); ResourceKey<Level> key,
} String pack,
IrisDimension dimension = IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey); String packDimensionKey,
if (dimension == null) { long seed,
throw new IllegalStateException("Iris pack '" + pack RuntimePack runtimePack
+ "' 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) {
RegistryAccess registryAccess = server.registryAccess(); 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); Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
FixedBiomeSource biomeSource = new FixedBiomeSource(plains); FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey; String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef); 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); LevelStem stem = new LevelStem(dimensionType, generator);
WorldData worldData = server.getWorldData(); WorldData worldData = server.getWorldData();
@@ -681,6 +836,77 @@ public final class ModdedDimensionManager {
return bound; 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 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; package art.arcane.iris.modded;
import art.arcane.iris.core.loader.IrisData; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
@@ -28,6 +31,7 @@ import net.minecraft.resources.Identifier;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
final class ModdedDimensionMetadata { final class ModdedDimensionMetadata {
private ModdedDimensionMetadata() { private ModdedDimensionMetadata() {
@@ -45,7 +49,10 @@ final class ModdedDimensionMetadata {
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) { static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
LinkedHashSet<String> keys = new LinkedHashSet<>( 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)) { for (IrisRegion region : dimension.getAllRegions(() -> data)) {
if (region == null) { if (region == null) {
continue; continue;
@@ -60,7 +67,27 @@ final class ModdedDimensionMetadata {
return Set.copyOf(keys); 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<>(); LinkedHashSet<String> keys = new LinkedHashSet<>();
for (IrisBiome irisBiome : biomes) { for (IrisBiome irisBiome : biomes) {
if (irisBiome == null) { if (irisBiome == null) {
@@ -74,12 +101,19 @@ final class ModdedDimensionMetadata {
continue; continue;
} }
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
keys.add(IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId())); keys.add(customBiomeKey.apply(customBiome).toLowerCase(Locale.ROOT));
} }
} }
return Set.copyOf(keys); 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) { static int clampSpawnHeight(int minY, int height) {
int minimum = minY + 1; int minimum = minY + 1;
int maximum = minY + height - 2; int maximum = minY + height - 2;
@@ -53,6 +53,10 @@ public final class ModdedDimensionRegistryStore {
return contents(file).dimensions(); 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 * 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. * 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) { 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) { public record PersistentDimension(String id, String pack, String dimension, long seed) {
@@ -19,6 +19,7 @@
package art.arcane.iris.modded; package art.arcane.iris.modded;
import art.arcane.iris.engine.IrisEngineMantle; import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.history.GenerationHistoryPaths;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
@@ -38,7 +39,8 @@ public final class ModdedDimensionStorage {
"region", "region",
"entities", "entities",
"poi", "poi",
IrisEngineMantle.STORAGE_FOLDER_NAME IrisEngineMantle.STORAGE_FOLDER_NAME,
GenerationHistoryPaths.IRIS_DIRECTORY_NAME
); );
private ModdedDimensionStorage() { private ModdedDimensionStorage() {
@@ -18,7 +18,7 @@
package art.arcane.iris.modded; 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.loader.IrisData;
import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages; 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.PackDirectoryResolver;
import art.arcane.iris.core.pack.PackValidationResult; import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator; 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.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionType; 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.KList;
import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.collection.KSet;
import net.minecraft.network.chat.Component; 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.PackLocationInfo;
import net.minecraft.server.packs.PackSelectionConfig; import net.minecraft.server.packs.PackSelectionConfig;
import net.minecraft.server.packs.PackType; 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.Pack;
import net.minecraft.server.packs.repository.PackSource; import net.minecraft.server.packs.repository.PackSource;
import net.minecraft.server.packs.repository.RepositorySource; 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.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer; 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_ID = "iris_worldgen";
private static final String PACK_FOLDER = "iris"; private static final String PACK_FOLDER = "iris";
private static final String HASH_FILE_NAME = "packs.hash"; 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. private static final String HASH_SALT = "iris-forced-datapack-v3-content-addressed-biomes";
// 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 long PACKS_HASH_TTL_NANOS = 2_000_000_000L; private static final long PACKS_HASH_TTL_NANOS = 2_000_000_000L;
private static final Object LOCK = new Object(); private static final Object LOCK = new Object();
private static final AtomicBoolean LOADED = new AtomicBoolean(false); 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 PublishedState published;
private static volatile HashMemo packsHashMemo; private static volatile HashMemo packsHashMemo;
@@ -119,11 +132,9 @@ public final class ModdedForcedDatapack {
reason = "no published pack"; reason = "no published pack";
} else if (!hash.isEmpty() && !state.packsHash().equals(hash)) { } else if (!hash.isEmpty() && !state.packsHash().equals(hash)) {
reason = "stale cache (hash changed)"; reason = "stale cache (hash changed)";
} else if (hash.isEmpty()) {
reason = "current registry inputs could not be verified";
} else { } 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 { try {
return requireReadablePack(state.directory()); return requireReadablePack(state.directory());
} catch (RuntimeException unreadable) { } catch (RuntimeException unreadable) {
@@ -163,17 +174,7 @@ public final class ModdedForcedDatapack {
} }
private static Pack buildPack() { private static Pack buildPack() {
try { return requireReadablePack(regenerate());
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;
}
} }
private static Pack requireReadablePack(Path directory) { 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 // 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. // otherwise mismatch what was just published and send the next serve straight back into buildPack.
packsHashMemo = new HashMemo(packsHash, System.nanoTime()); packsHashMemo = new HashMemo(packsHash, System.nanoTime());
STALE_SERVE_LOGGED.set(false);
return packDirectory(); return packDirectory();
} catch (IOException | RuntimeException | Error failure) { } catch (IOException | RuntimeException | Error failure) {
try { try {
@@ -335,10 +335,6 @@ public final class ModdedForcedDatapack {
return hash; 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 { private static String packsHash() throws IOException {
MessageDigest digest; MessageDigest digest;
try { try {
@@ -348,39 +344,13 @@ public final class ModdedForcedDatapack {
} }
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName() digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
+ '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8)); + '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
Path root = packsRoot(); String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
if (Files.isDirectory(root)) { compilationPackRoots(),
List<String> entries = new ArrayList<>(); List.of(),
Files.walkFileTree(root, new SimpleFileVisitor<Path>() { DataVersion.getLatest().get(),
@Override false
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) { );
return !directory.equals(root) digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
&& 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');
}
}
return HexFormat.of().formatHex(digest.digest()); return HexFormat.of().formatHex(digest.digest());
} }
@@ -390,10 +360,11 @@ public final class ModdedForcedDatapack {
int packCount = 0; int packCount = 0;
KList<String> presetIds = new KList<>(); KList<String> presetIds = new KList<>();
File root = packsRoot().toFile(); Path worldRoot = worldRootOrNull();
List<File> packs = PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(root); Set<PackSelection> legacySelections = unadoptedSelections(worldRoot);
for (File pack : packs) { for (File pack : compilationPackRoots(worldRoot)) {
if (stagePack(pack, fixer, stagingDirectory, seenBiomes, presetIds)) { PackStageContext context = packStageContext(pack.toPath(), legacySelections);
if (stagePack(context, fixer, stagingDirectory, seenBiomes, presetIds)) {
packCount++; 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, Map<String, KSet<String>> seenBiomes,
KList<String> presetIds) throws IOException { KList<String> presetIds) throws IOException {
File sourcePack = context.packRoot().toFile();
PackValidationResult validation; PackValidationResult validation;
try { try {
validation = PackValidator.validateForDatapackBootstrap(sourcePack); validation = PackValidator.validateForDatapackBootstrap(sourcePack);
} catch (Throwable validationFailure) { } catch (Throwable validationFailure) {
if (context.required()) {
throw requiredPackFailure(context, "validation", validationFailure);
}
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because validation failed", ModdedIrisLog.error("Iris excluded pack '{}' from Create World because validation failed",
sourcePack.getName(), validationFailure); sourcePack.getName(), validationFailure);
rethrowIfUnrecoverable(validationFailure); rethrowIfUnrecoverable(validationFailure);
return false; return false;
} }
if (!validation.isLoadable()) { 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: {}", ModdedIrisLog.error("Iris excluded pack '{}' from Create World: {} blocking validation error(s); first error: {}",
sourcePack.getName(), validation.getBlockingErrors().size(), sourcePack.getName(), validation.getBlockingErrors().size(),
validation.getBlockingErrors().getFirst()); validation.getBlockingErrors().getFirst());
@@ -441,8 +421,11 @@ public final class ModdedForcedDatapack {
packFolders.add(packStagingDirectory.toFile()); packFolders.add(packStagingDirectory.toFile());
boolean installed; boolean installed;
try { try {
installed = installPack(sourcePack, fixer, packFolders, packBiomes, packPresetIds); installed = installPack(context, fixer, packFolders, packBiomes, packPresetIds);
} catch (Throwable installationFailure) { } catch (Throwable installationFailure) {
if (context.required()) {
throw requiredPackFailure(context, "datapack serialization", installationFailure);
}
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because datapack serialization failed", ModdedIrisLog.error("Iris excluded pack '{}' from Create World because datapack serialization failed",
sourcePack.getName(), installationFailure); sourcePack.getName(), installationFailure);
rethrowIfUnrecoverable(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<>(); List<Path> entries = new ArrayList<>();
try (Stream<Path> walk = Files.walk(sourceDirectory)) { try (Stream<Path> walk = Files.walk(sourceDirectory)) {
walk.sorted(Comparator.comparingInt(Path::getNameCount)).forEach(entries::add); walk.sorted(Comparator.comparingInt(Path::getNameCount)).forEach(entries::add);
@@ -490,9 +473,14 @@ public final class ModdedForcedDatapack {
Path relative = sourceDirectory.relativize(source); Path relative = sourceDirectory.relativize(source);
Path destination = destinationDirectory.resolve(relative); Path destination = destinationDirectory.resolve(relative);
if (Files.isDirectory(source)) { if (Files.isDirectory(source)) {
if (Files.exists(destination) && !Files.isDirectory(destination)) {
throw new IOException("Conflicting Iris datapack output at " + destination + ".");
}
Files.createDirectories(destination); Files.createDirectories(destination);
} else if (!Files.exists(destination)) { } else if (!Files.exists(destination)) {
Files.copy(source, 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, Map<String, KSet<String>> seenBiomes,
KList<String> presetIds) throws IOException { KList<String> presetIds) throws IOException {
String packName = packFolder.getName(); File packFolder = context.packRoot().toFile();
String packName = context.packName();
File dimensionsDirectory = new File(packFolder, "dimensions"); File dimensionsDirectory = new File(packFolder, "dimensions");
if (!dimensionsDirectory.isDirectory()) { if (!dimensionsDirectory.isDirectory()) {
return false; return false;
} }
IrisData data = IrisData.get(packFolder); IrisData data = IrisData.openDatapackCompiler(packFolder);
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys(); try {
if (dimensionKeys == null || dimensionKeys.length == 0) { if (context.retainedContract() != null) {
return false; data.bindGenerationRegistryContract(context.retainedContract());
}
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");
} }
String biomePathPrefix = ModdedWorldgenIds.biomePathPrefix(packName, dimensionKey); String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
dimension.installBiomes(fixer, () -> data, folders, "irisworldgen", biomePathPrefix, if (dimensionKeys == null || dimensionKeys.length == 0) {
biomesForNamespace(seenBiomes, biomePathPrefix)); return false;
dimension.installBiomes(fixer, () -> data, folders, }
biomesForNamespace(seenBiomes, dimension.getLoadKey())); int installedDimensions = 0;
writeDimensionType(folders, fixer, dimension, packName, dimensionKey); List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey); for (String dimensionKey : sortedDimensionKeys) {
writeWorldPreset(folders, packName, dimensionKey, presetRef); if (context.dimensionKey() != null && !context.dimensionKey().equals(dimensionKey)) {
presetIds.add(presetRef); 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) { 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, static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension,
String pack, String packDimensionKey) throws IOException { 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(); IrisDimensionType type = dimension.getDimensionType();
String json = type.toJson(fixer); String json = Objects.requireNonNull(sourceResolver, "sourceResolver").generatedSource(
String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey); GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
typeRef,
type.toJson(fixer),
fixer
);
String typePath = typeRef.substring(typeRef.indexOf(':') + 1); String typePath = typeRef.substring(typeRef.indexOf(':') + 1);
String typeNamespace = typeRef.substring(0, typeRef.indexOf(':'));
for (File datapackRoot : folders) { 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"); .resolve("dimension_type").resolve(typePath + ".json");
Files.createDirectories(output.getParent()); Files.createDirectories(output.getParent());
Files.writeString(output, json, StandardCharsets.UTF_8); Files.writeString(output, json, StandardCharsets.UTF_8);
if (!emitLegacyAlias) {
continue;
}
// Load-bearing, not dead: worlds created before the scoped pack path reference // Load-bearing, not dead: worlds created before the scoped pack path reference
// irisworldgen:<dimensionTypeKey> in their level.dat, and ModdedWorldEngines accepts that legacy // irisworldgen:<dimensionTypeKey> in their level.dat, and ModdedWorldEngines accepts that legacy
// key when validating the runtime dimension contract. Removing this emission unloads those worlds. // 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() { private static Path packsRoot() {
return IrisPlatforms.get().packsFolderNoCreate().toPath(); 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) { 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 { final class ModdedImportedFeatureStage {
private static final String CYCLE_MARKER = "Feature order cycle found"; 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 IrisModdedBiomeSource biomeSource;
private final ReentrantLock buildLock = new ReentrantLock(); 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 IrisModdedChunkGenerator generator;
private volatile FeatureTable featureTable;
private volatile long inertGeneration = NO_GENERATION;
ModdedImportedFeatureStage(IrisModdedBiomeSource biomeSource) { ModdedImportedFeatureStage(IrisModdedBiomeSource biomeSource) {
this.biomeSource = biomeSource; this.biomeSource = biomeSource;
@@ -103,12 +101,12 @@ final class ModdedImportedFeatureStage {
* pack can never serve another. * pack can never serve another.
*/ */
void invalidate() { void invalidate() {
featureTable = null; featureTables.clear();
inertGeneration = NO_GENERATION; inertGenerations.clear();
} }
boolean active() { 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. * <p>With {@code importedFeatures} disabled there is no table and this is vanilla's default getter.
*/ */
BiomeGenerationSettings generationSettings(Holder<Biome> biome) { 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) { if (table == null) {
return biome.value().getGenerationSettings(); return biome.value().getGenerationSettings();
} }
@@ -151,8 +150,9 @@ final class ModdedImportedFeatureStage {
if (engine == null || engine.isClosed() || engine.isClosing()) { if (engine == null || engine.isClosed() || engine.isClosing()) {
return; return;
} }
int runtimeIdentity = engine.getCacheID();
long generation = biomeSource.packGeneration(); long generation = biomeSource.packGeneration();
if (settled(generation)) { if (settled(runtimeIdentity, generation)) {
return; return;
} }
if (waitForBuild) { if (waitForBuild) {
@@ -161,35 +161,35 @@ final class ModdedImportedFeatureStage {
return; return;
} }
try { try {
if (settled(generation)) { if (settled(runtimeIdentity, generation)) {
return; return;
} }
build(engine, generation); build(engine, runtimeIdentity, generation);
} finally { } finally {
buildLock.unlock(); buildLock.unlock();
} }
} }
private boolean settled(long generation) { private boolean settled(int runtimeIdentity, long generation) {
FeatureTable current = featureTable; FeatureTable current = featureTables.get(runtimeIdentity);
if (current != null && current.generation() == generation) { if (current != null && current.generation() == generation) {
return true; 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; IrisImportedFeatureControl control;
try { try {
control = NativeFeatureGenerationPolicy.control(engine); control = NativeFeatureGenerationPolicy.control(engine);
} catch (RuntimeException error) { } catch (RuntimeException error) {
ModdedIrisLog.error("Iris could not read importedFeatures for this dimension; features off: {}", ModdedIrisLog.error("Iris could not read importedFeatures for this dimension; features off: {}",
error.toString()); error.toString());
markInert(generation); markInert(runtimeIdentity, generation);
return; return;
} }
if (!control.shouldGenerateFeatures()) { if (!control.shouldGenerateFeatures()) {
markInert(generation); markInert(runtimeIdentity, generation);
return; return;
} }
FeatureTable built; FeatureTable built;
@@ -199,15 +199,15 @@ final class ModdedImportedFeatureStage {
} catch (Throwable error) { } catch (Throwable error) {
ModdedIrisLog.error("Iris importedFeatures is off for {}: feature table construction failed: {}", ModdedIrisLog.error("Iris importedFeatures is off for {}: feature table construction failed: {}",
dimensionKey(engine), error.toString()); dimensionKey(engine), error.toString());
markInert(generation); markInert(runtimeIdentity, generation);
return; return;
} }
if (built == null) { if (built == null) {
markInert(generation); markInert(runtimeIdentity, generation);
return; return;
} }
featureTable = built; featureTables.put(runtimeIdentity, built);
inertGeneration = NO_GENERATION; inertGenerations.remove(runtimeIdentity);
// Arm the worldcheck log watch here, before any chunk decorates: arming from the first pass instead // 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. // missed every far-chunk write the first chunk made. No-op unless -Diris.worldcheck is set.
WorldCheckFeaturePlacement.arm(); WorldCheckFeaturePlacement.arm();
@@ -216,9 +216,9 @@ final class ModdedImportedFeatureStage {
built.derivatives().size()); built.derivatives().size());
} }
private void markInert(long generation) { private void markInert(int runtimeIdentity, long generation) {
featureTable = null; featureTables.remove(runtimeIdentity);
inertGeneration = generation; inertGenerations.put(runtimeIdentity, generation);
} }
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) { private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
@@ -312,7 +312,8 @@ final class ModdedImportedFeatureStage {
* control is disabled or the table degraded. * control is disabled or the table degraded.
*/ */
void run(WorldGenLevel level, ChunkAccess chunk, Engine engine) { void run(WorldGenLevel level, ChunkAccess chunk, Engine engine) {
FeatureTable table = featureTable; int runtimeIdentity = engine.getCacheID();
FeatureTable table = featureTables.get(runtimeIdentity);
if (table == null) { if (table == null) {
WorldCheckFeaturePlacement.recordFeaturesOff(); WorldCheckFeaturePlacement.recordFeaturesOff();
return; return;
@@ -320,7 +321,8 @@ final class ModdedImportedFeatureStage {
if (table.generation() != biomeSource.packGeneration()) { if (table.generation() != biomeSource.packGeneration()) {
// A repoint landed between the prepare above and this chunk. Refuse stale content outright; the // A repoint landed between the prepare above and this chunk. Refuse stale content outright; the
// next chunk's prepare rebuilds against the new pack. // next chunk's prepare rebuilds against the new pack.
invalidate(); featureTables.remove(runtimeIdentity, table);
inertGenerations.remove(runtimeIdentity);
return; return;
} }
IrisModdedChunkGenerator owner = generator; 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, private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes, Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed, 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.IrisStructureLocator;
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy; import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord; 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.NativeStructureStartPlan;
import art.arcane.iris.engine.object.IrisMaterialPalette; import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisNativeStructureDecision; import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -233,17 +234,30 @@ final class ModdedNativeStructureStage {
if (!start.isValid() || previousStarts.get(structure) == start) { if (!start.isValid() || previousStarts.get(structure) == start) {
continue; continue;
} }
if (configuredStarts.containsKey(structure)) {
recordWorldCheckStructureShift(
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
continue;
}
Identifier id = registry.getKey(structure); Identifier id = registry.getKey(structure);
String structureId = id == null ? null : id.toString(); String structureId = id == null ? null : id.toString();
if (structureId == null) { if (structureId == null) {
throw NativeStructureGenerationException.failure( throw NativeStructureGenerationException.failure(
"resolution", null, chunkPos.x(), chunkPos.z()); "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()); boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
IrisNativeStructureDecision decision; IrisNativeStructureDecision decision;
try { try {
@@ -326,6 +340,14 @@ final class ModdedNativeStructureStage {
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure); List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size()); List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
for (StructureStart start : starts) { for (StructureStart start : starts) {
BoundingBox footprint = start.getBoundingBox();
if (!current.getComplex().allowsNewGenerationFootprint(
footprint.minX(),
footprint.minZ(),
footprint.maxX(),
footprint.maxZ())) {
continue;
}
NativeStructureOwnershipRecord ownership = NativeStructureOwnershipRecord ownership =
NativeStructureOwnershipRecovery.resolve( NativeStructureOwnershipRecovery.resolve(
current, world.getLevel(), structureId, structure, start); current, world.getLevel(), structureId, structure, start);
@@ -18,6 +18,7 @@
package art.arcane.iris.modded; package art.arcane.iris.modded;
import art.arcane.iris.BuildConstants;
import art.arcane.iris.modded.api.ModdedCustomContentRegistry; import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
import art.arcane.iris.modded.api.ModdedDataType; import art.arcane.iris.modded.api.ModdedDataType;
import art.arcane.iris.spi.IrisLogging; 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.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType; import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformItem; import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.volmlib.util.data.UnresolvedKeyLog; import art.arcane.volmlib.util.data.UnresolvedKeyLog;
@@ -59,6 +61,19 @@ public final class ModdedRegistries implements PlatformRegistries {
this.server = server; 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 @Override
public PlatformBlockState block(String key) { public PlatformBlockState block(String key) {
return ModdedBlockResolution.get(key); return ModdedBlockResolution.get(key);
@@ -71,7 +71,7 @@ public final class ModdedRuntimeRegistry {
if (!seen.add(biomeId)) { if (!seen.add(biomeId)) {
continue; 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)); ResourceKey<Biome> biomeKey = ResourceKey.create(Registries.BIOME, Identifier.parse(biomeRef));
if (registry.get(biomeKey).isEmpty()) { if (registry.get(biomeKey).isEmpty()) {
missing.add(biomeRef); missing.add(biomeRef);
@@ -36,27 +36,31 @@ import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.MobSpawnSettings; import net.minecraft.world.level.biome.MobSpawnSettings;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
final class ModdedSpawnTableMerger { final class ModdedSpawnTableMerger {
private final IrisModdedChunkGenerator generator; 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) { ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
this.generator = generator; this.generator = generator;
} }
void initializeVanillaSpawnBiomes(Registry<Biome> registry) { void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
SpawnTableState active = state; Engine current = generator.engineOrNull();
if (active.initialized) { if (current == null) {
return; return;
} }
synchronized (active) { int runtimeIdentity = current.getCacheID();
if (active != state || active.initialized) { if (initializedRuntimeIdentities.contains(runtimeIdentity)) {
return; return;
} }
Engine current = generator.engineOrNull(); synchronized (generator) {
if (current == null) { if (initializedRuntimeIdentities.contains(runtimeIdentity)) {
return; return;
} }
@@ -68,7 +72,7 @@ final class ModdedSpawnTableMerger {
hostDimension, hostDimension,
current.getData(), current.getData(),
hostDimension.getReachableBiomes(current), hostDimension.getReachableBiomes(current),
active runtimeIdentity
); );
DimensionStackContext stackContext = current.getDimensionStackContext(); DimensionStackContext stackContext = current.getDimensionStackContext();
if (stackContext != null) { if (stackContext != null) {
@@ -82,11 +86,11 @@ final class ModdedSpawnTableMerger {
dimension, dimension,
terrainContext.getData(), terrainContext.getData(),
dimension.getReachableBiomes(terrainContext), dimension.getReachableBiomes(terrainContext),
active runtimeIdentity
); );
} }
} }
active.initialized = true; initializedRuntimeIdentities.add(runtimeIdentity);
} }
} }
} }
@@ -96,7 +100,7 @@ final class ModdedSpawnTableMerger {
IrisDimension dimension, IrisDimension dimension,
IrisData data, IrisData data,
Iterable<IrisBiome> biomes, Iterable<IrisBiome> biomes,
SpawnTableState state int runtimeIdentity
) { ) {
for (IrisBiome irisBiome : biomes) { for (IrisBiome irisBiome : biomes) {
if (irisBiome == null || !irisBiome.isCustom()) { if (irisBiome == null || !irisBiome.isCustom()) {
@@ -110,31 +114,43 @@ final class ModdedSpawnTableMerger {
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) { for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
Holder<Biome> customHolder = resolveBiomeHolder( Holder<Biome> customHolder = resolveBiomeHolder(
registry, registry,
ModdedWorldgenIds.biomeRef( data.customBiomeResourceKey(dimension, customBiome)
data.getDataFolder().getName(),
dimension.getLoadKey(),
customBiome.getId()
)
); );
if (customHolder == null) { if (customHolder == null) {
continue; continue;
} }
state.vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder); vanillaSpawnBiomes.putIfAbsent(
new SpawnBiomeKey(runtimeIdentity, customHolder.value()),
vanillaHolder
);
} }
} }
} }
Holder<Biome> vanillaSpawnBiome(Biome biome) { 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( WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
Biome biome, MobCategory category, Engine engine,
Biome biome,
MobCategory category,
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns, WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) { WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
SpawnTableKey key = new SpawnTableKey(biome, category); SpawnTableKey key = new SpawnTableKey(engine.getCacheID(), biome, category);
return state.mergedSpawnTables.computeIfAbsent( return mergedSpawnTables.computeIfAbsent(
key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns)); 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) { private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
@@ -150,16 +166,16 @@ final class ModdedSpawnTableMerger {
} }
void resetVanillaSpawnBiomes() { 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 record SpawnTableKey(int runtimeIdentity, Biome biome, MobCategory category) {
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables =
new ConcurrentHashMap<>();
private volatile boolean initialized;
} }
} }
@@ -223,7 +223,13 @@ public final class ModdedStartup {
index++; index++;
long dimensionStartedAt = System.currentTimeMillis(); long dimensionStartedAt = System.currentTimeMillis();
try { try {
ModdedDimensionManager.create(server, dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed()); ModdedDimensionManager.restorePersistent(
server,
dimension.id(),
dimension.pack(),
dimension.dimension(),
dimension.seed()
);
injected++; injected++;
ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms", ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms",
index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(), index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(),
@@ -18,21 +18,30 @@
package art.arcane.iris.modded; 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.loader.IrisData;
import art.arcane.iris.core.pack.PackValidationRegistry; import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineTarget; 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.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract; import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.modded.command.ModdedGuiHost; import art.arcane.iris.modded.command.ModdedGuiHost;
import art.arcane.iris.spi.IrisPlatforms;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource; import net.minecraft.world.level.storage.LevelResource;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@@ -45,13 +54,25 @@ public final class ModdedWorldEngines {
private 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); Engine existing = ENGINES.get(level);
if (existing != null) { if (existing != null) {
return existing; return existing;
} }
return ENGINES.computeIfAbsent(level, return ENGINES.computeIfAbsent(level,
(ServerLevel l) -> createAndApplyWorldBoundary(l, pack, dimensionKey, seedOverride)); (ServerLevel l) -> createAndApplyWorldBoundary(
l,
pack,
dimensionKey,
seedOverride,
generationMode
));
} }
public static Collection<Engine> activeEngines() { public static Collection<Engine> activeEngines() {
@@ -81,8 +102,14 @@ public final class ModdedWorldEngines {
ModdedIrisLog.info("Iris engine evicted for {}", level.dimension().identifier()); ModdedIrisLog.info("Iris engine evicted for {}", level.dimension().identifier());
} }
static Engine prepareReplacement(ServerLevel level, String pack, String dimensionKey, long seedOverride) { static Engine prepareReplacement(
return create(level, pack, dimensionKey, seedOverride); ServerLevel level,
String pack,
String dimensionKey,
long seedOverride,
ModdedGenerationMode generationMode
) {
return create(level, pack, dimensionKey, seedOverride, generationMode);
} }
static void installReplacement(ServerLevel level, Engine replacement) { static void installReplacement(ServerLevel level, Engine replacement) {
@@ -103,9 +130,14 @@ public final class ModdedWorldEngines {
ModdedGuiHost.unbind(engine); ModdedGuiHost.unbind(engine);
} }
private static Engine createAndApplyWorldBoundary(ServerLevel level, String pack, private static Engine createAndApplyWorldBoundary(
String dimensionKey, long seedOverride) { ServerLevel level,
Engine engine = create(level, pack, dimensionKey, seedOverride); String pack,
String dimensionKey,
long seedOverride,
ModdedGenerationMode generationMode
) {
Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode);
try { try {
engine.getPlatformHooks().applyWorldBoundary(engine); engine.getPlatformHooks().applyWorldBoundary(engine);
return 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(); 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; long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
validateDimensionContract(pack, dimensionKey, dimension, level); ModdedGenerationMode requiredMode = Objects.requireNonNull(generationMode, "generationMode");
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile(); if (requiredMode == ModdedGenerationMode.TRANSIENT_STUDIO) {
IrisWorld world = IrisWorld.builder() 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()) .platformIdentity(level.dimension().identifier().toString())
.name(level.dimension().identifier().toString().replace(':', '_')) .name(level.dimension().identifier().toString().replace(':', '_'))
.seed(seed) .seed(seed)
@@ -150,40 +338,52 @@ public final class ModdedWorldEngines {
.maxHeight(dimension.getMaxHeight()) .maxHeight(dimension.getMaxHeight())
.platformWorld(new ModdedPlatformWorld(level)) .platformWorld(new ModdedPlatformWorld(level))
.build(); .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, private static void requireReady(
IrisDimension dimension, ServerLevel level) { 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(); DimensionType actualType = level.dimensionType();
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey() String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
.map(key -> key.identifier().toString()) .map(key -> key.identifier().toString())
.orElse("<unregistered>"); .orElse("<unregistered>");
String legacyTypeKey = "irisworldgen:" + dimension.getDimensionTypeKey();
String expectedTypeKey = legacyTypeKey.equals(actualTypeKey)
? legacyTypeKey
: ModdedWorldgenIds.dimensionTypeRef(pack, dimensionKey);
IrisDimensionRuntimeContract expected = new IrisDimensionRuntimeContract( IrisDimensionRuntimeContract expected = new IrisDimensionRuntimeContract(
expectedTypeKey, recorded.dimensionTypeKey(),
dimension.getMinHeight(), recorded.minHeight(),
dimension.getMaxHeight() - dimension.getMinHeight(), recorded.height(),
dimension.getLogicalHeight()); recorded.logicalHeight());
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract( IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
actualTypeKey, actualTypeKey,
actualType.minY(), actualType.minY(),
@@ -3,7 +3,9 @@ package art.arcane.iris.modded;
import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.Engine;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
public final class ModdedWorldgenIds { public final class ModdedWorldgenIds {
private static final String NAMESPACE = "irisworldgen"; private static final String NAMESPACE = "irisworldgen";
@@ -29,8 +31,33 @@ public final class ModdedWorldgenIds {
} }
public static String biomeRef(Engine engine, String biome) { public static String biomeRef(Engine engine, String biome) {
return biomeRef(engine.getData().getDataFolder().getName(), return engine.getData().customBiomeResourceKey(engine.getDimension(), biome);
engine.getDimension().getLoadKey(), 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) { public static String displayName(String presetPath) {
@@ -100,4 +127,7 @@ public final class ModdedWorldgenIds {
} }
return Character.toUpperCase(normalized.charAt(0)) + normalized.substring(1); 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.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability; import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision; import art.arcane.iris.engine.object.IrisNativeStructureDecision;
import art.arcane.iris.engine.object.NativeStructureGenerationStatus; import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
@@ -109,7 +110,7 @@ final class ModdedCommandSuggestions {
} }
static Set<String> reachableBiomeKeys(Engine engine) { static Set<String> reachableBiomeKeys(Engine engine) {
return reachableBiomeKeys(engine.getAllBiomes()); return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
} }
static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) { static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) {
@@ -123,7 +124,7 @@ final class ModdedCommandSuggestions {
} }
static boolean isReachableBiome(Engine engine, String biomeKey) { 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) { static boolean isReachableBiome(Iterable<IrisBiome> biomes, String biomeKey) {
@@ -135,7 +136,8 @@ final class ModdedCommandSuggestions {
try { try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) { 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) { } catch (Throwable e) {
warnTabFailure("region keys", context.getSource(), e); warnTabFailure("region keys", context.getSource(), e);
@@ -148,7 +150,7 @@ final class ModdedCommandSuggestions {
try { try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) { if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder); return SharedSuggestionProvider.suggest(GenerationFindCatalog.objectKeys(engine), builder);
} }
} catch (Throwable e) { } catch (Throwable e) {
warnTabFailure("object keys", context.getSource(), e); warnTabFailure("object keys", context.getSource(), e);
@@ -191,6 +193,7 @@ final class ModdedCommandSuggestions {
Collection<String> unregisteredIrisKeys = eligibleUnregisteredEditableKeys( Collection<String> unregisteredIrisKeys = eligibleUnregisteredEditableKeys(
irisKeys, registeredKeys, irisKeys, registeredKeys,
(String candidate) -> IrisStructureLocator.hasNativePlacement(engine, candidate)); (String candidate) -> IrisStructureLocator.hasNativePlacement(engine, candidate));
nativeKeys.addAll(GenerationFindCatalog.retainedStructureKeys(engine));
return SharedSuggestionProvider.suggest( return SharedSuggestionProvider.suggest(
combineStructureKeys(unregisteredIrisKeys, nativeKeys), builder); combineStructureKeys(unregisteredIrisKeys, nativeKeys), builder);
} catch (Throwable e) { } 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.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.framework.StructureReachability; import art.arcane.iris.engine.framework.StructureReachability;
import art.arcane.iris.engine.framework.WrongEngineBroException; 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.HydrologyFeatureQuery;
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime; import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisNativeStructureDecision; 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)); IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8));
return 0; return 0;
} }
IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim()); IrisBiome biome = GenerationFindCatalog.biome(engine, key);
if (biome == null) { if (biome == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key))); IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
return 0; 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()); locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
return 1; 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)); IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9));
return 0; return 0;
} }
IrisRegion region = engine.getData().getRegionLoader().load(key.trim()); IrisRegion region = GenerationFindCatalog.region(engine, key);
if (region == null) { if (region == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key))); IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key)));
return 0; 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()); locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
return 1; return 1;
} }
@@ -132,7 +125,7 @@ final class ModdedLocateCommands {
return 0; return 0;
} }
String key = keyRaw.trim(); 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))); 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; return 0;
} }
@@ -158,10 +151,6 @@ final class ModdedLocateCommands {
return 0; return 0;
} }
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime(); IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
if (runtime == null) {
IrisModdedCommands.fail(source, "Hydrology is not active in this dimension.");
return 0;
}
HydrologyFeatureQuery query; HydrologyFeatureQuery query;
try { try {
query = HydrologyFeatureQuery.parse(type); query = HydrologyFeatureQuery.parse(type);
@@ -171,15 +160,19 @@ final class ModdedLocateCommands {
} }
int originX = player.blockPosition().getX(); int originX = player.blockPosition().getX();
int originZ = player.blockPosition().getZ(); int originZ = player.blockPosition().getZ();
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15); int requestedDistance = runtime == null
int maximumDistance = runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance); ? 8192
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
int maximumDistance = runtime == null
? requestedDistance
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
MinecraftServer server = source.getServer(); MinecraftServer server = source.getServer();
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "..."); IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
MultiBurst.burst.completeValueAsync(() -> runtime.nearestFeature( MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
query.types(), query.profileKey(), originX, originZ, maximumDistance, engine, query, originX, originZ, maximumDistance,
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source, (int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null)) "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) { if (error != null) {
ModdedIrisLog.error("Hydrology locate failed for {}", type, error); ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
IrisModdedCommands.fail(source, IrisModdedCommands.fail(source,
@@ -192,13 +185,12 @@ final class ModdedLocateCommands {
+ maximumDistance + " blocks."); + maximumDistance + " blocks.");
return; return;
} }
int worldY = feature.y() + engine.getDimension().getMinHeight() + 2;
teleportToStructure( teleportToStructure(
source, source,
level, level,
player, player,
feature.x(), feature.x(),
worldY, feature.y() + 2,
feature.z(), feature.z(),
type + " hydrology feature" type + " hydrology feature"
); );
@@ -225,8 +217,9 @@ final class ModdedLocateCommands {
} }
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key); Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
if (resolved.isEmpty()) { if (resolved.isEmpty()) {
if (!IrisStructureLocator.hasNativePlacement(engine, key) if ((!IrisStructureLocator.hasNativePlacement(engine, key)
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key)) { && IrisStructureLocator.hasLocatableEditablePlacement(engine, key))
|| GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)) {
locateIrisStructure(source, level, engine, player, key); locateIrisStructure(source, level, engine, player, key);
return 1; return 1;
} }
@@ -247,6 +240,10 @@ final class ModdedLocateCommands {
if (!ModdedCommandSuggestions.isEligibleRegisteredStructure( if (!ModdedCommandSuggestions.isEligibleRegisteredStructure(
decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement, decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement,
reachable, nativeGenerationEnabled)) { reachable, nativeGenerationEnabled)) {
if (GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)) {
locateIrisStructure(source, level, engine, player, key);
return 1;
}
IrisModdedCommands.fail(source, registeredStructureUnavailableMessage( IrisModdedCommands.fail(source, registeredStructureUnavailableMessage(
target.key(), target.availability(), decision, target.key(), target.availability(), decision,
nativePlacement, locatableNativePlacement, nativePlacement, locatableNativePlacement,
@@ -312,7 +309,7 @@ final class ModdedLocateCommands {
Thread thread = new Thread(() -> { Thread thread = new Thread(() -> {
try { try {
IrisStructureLocator.LocateResult result = 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) { 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)))); 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; return;
@@ -491,7 +491,7 @@ public final class ModdedStudioCommands {
ModdedDimensionManager.Handle handle = null; ModdedDimensionManager.Handle handle = null;
try { try {
replaceExistingStudio(server, owner, dimensionId); replaceExistingStudio(server, owner, dimensionId);
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed); handle = ModdedDimensionManager.createTransientStudio(server, dimensionId, pack, pack, seed);
STUDIOS.put(owner, dimensionId); STUDIOS.put(owner, dimensionId);
if (owner.equals(CONSOLE_OWNER)) { if (owner.equals(CONSOLE_OWNER)) {
completeConsoleOpen(source, dimensionId, pack, seed, handle); completeConsoleOpen(source, dimensionId, pack, seed, handle);
@@ -78,6 +78,7 @@ public final class ModdedWorldCommands {
root.then(enableTree("enable")); root.then(enableTree("enable"));
root.then(enableTree("create")); root.then(enableTree("create"));
root.then(updateTree());
root.then(replaceOverworldTree()); root.then(replaceOverworldTree());
root.then(mainWorldTree()); root.then(mainWorldTree());
@@ -110,6 +111,20 @@ public final class ModdedWorldCommands {
.executes(ModdedCommandTree.localized((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), false)))); .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) { private static LiteralArgumentBuilder<CommandSourceStack> deleteTree(String name) {
return Commands.literal(name) return Commands.literal(name)
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS) .then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
@@ -197,6 +212,47 @@ public final class ModdedWorldCommands {
return 1; 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) { private static int replaceOverworld(CommandSourceStack source, String packRaw, String seedRaw) {
MinecraftServer server = source.getServer(); MinecraftServer server = source.getServer();
Long seed = parseSeed(source, seedRaw); Long seed = parseSeed(source, seedRaw);
@@ -18,6 +18,7 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -267,10 +268,11 @@ public class IrisModdedStructureParityTest {
.setInferredType(InferredType.SEA); .setInferredType(InferredType.SEA);
Set<String> keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys( 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", 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:desert"));
assertFalse(keys.contains("minecraft:plains")); assertFalse(keys.contains("minecraft:plains"));
@@ -52,6 +52,19 @@ public class ModdedForcedDatapackTest {
assertEquals("iris:overworld", ModdedWorldgenIds.generatorIdentity("overworld")); assertEquals("iris:overworld", ModdedWorldgenIds.generatorIdentity("overworld"));
assertEquals("iris:other/overworld", assertEquals("iris:other/overworld",
ModdedWorldgenIds.generatorIdentity("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 @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 { private List<String> directoryEntries(Path root) throws IOException {
try (Stream<Path> entries = Files.list(root)) { try (Stream<Path> entries = Files.list(root)) {
return entries.map((Path path) -> path.getFileName().toString()) 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("catch (GenerationSessionException e)"));
assertTrue(source.contains("e.isExpectedTeardown()")); assertTrue(source.contains("e.isExpectedTeardown()"));
assertTrue(source.contains("Iris structure biome key lookup was rejected during an engine transition")); 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 @Test
@@ -203,7 +205,7 @@ public class ModdedGenerationLeaseContractTest {
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java"); String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
String start = method(jobSource, "public static boolean start("); String start = method(jobSource, "public static boolean start(");
int taskConstruction = start.indexOf("PregenTask task = PregenTask.builder()"); 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(taskConstruction >= 0);
assertTrue(profileMutation > taskConstruction); assertTrue(profileMutation > taskConstruction);
@@ -14,15 +14,16 @@ public class ModdedLifecycleFailureContractTest {
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources"; private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
@Test @Test
public void existingDimensionRepointBindsBeforeHandlePublication() throws IOException { public void existingDimensionStagesWithoutReplacingTheLiveEngine() throws IOException {
String managerSource = source("ModdedDimensionManager.java"); 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 existingStart = requiredIndex(create, "if (existing != null");
int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1); int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1);
int injectionStart = requiredIndex(create, "try {", presentStart); int injectionStart = requiredIndex(create, "try {", presentStart);
assertRepointBeforePublication(create.substring(existingStart, presentStart)); assertFalse(create.substring(existingStart, presentStart).contains("repointAndBind("));
assertRepointBeforePublication(create.substring(presentStart, injectionStart)); assertFalse(create.substring(presentStart, injectionStart).contains("repointAndBind("));
assertBefore(create, "ModdedGenerationHistoryStorage.createOrStage(", "if (existing != null");
String generatorSource = source("IrisModdedChunkGenerator.java"); String generatorSource = source("IrisModdedChunkGenerator.java");
String repointAndBind = method(generatorSource, "void repointAndBind("); String repointAndBind = method(generatorSource, "void repointAndBind(");
@@ -40,6 +41,31 @@ public class ModdedLifecycleFailureContractTest {
assertFalse(installReplacement.contains("catch (")); 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 @Test
public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException { public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException {
String source = source("ModdedDimensionManager.java"); String source = source("ModdedDimensionManager.java");
@@ -110,18 +136,13 @@ public class ModdedLifecycleFailureContractTest {
} }
@Test @Test
public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException { public void persistentRestoreOpensHistoryBeforeConsultingTheGlobalPack() throws IOException {
String source = source("ModdedDimensionManager.java"); String source = source("ModdedGenerationHistoryStorage.java");
String loading = method(source, "private static IrisDimension loadPackDimension("); String restore = method(source, "static GenerationHistory restoreOrAdopt(");
assertFalse(loading.contains("return null;")); assertBefore(restore, "GenerationHistory.openIfPresent(", "sourceSupplier\").get();");
assertFalse(loading.contains("e.toString()")); assertTrue(restore.contains("if (existing.isPresent())"));
int catchIndex = loading.indexOf("catch ("); assertBefore(restore, "requireRegistryDefinitions(existing.get()", "return existing.get();");
if (catchIndex >= 0) {
String failure = blockAt(loading, requiredIndex(loading, "{", catchIndex));
assertTrue(failure.contains("throw "));
assertTrue(failure.contains(", e);") || failure.contains("throw e;"));
}
} }
@Test @Test
@@ -249,12 +270,6 @@ public class ModdedLifecycleFailureContractTest {
assertBefore(failure, "rollback.restore(failure);", "throw "); 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) { private static void assertBefore(String source, String first, String second) {
int firstIndex = requiredIndex(source, first); int firstIndex = requiredIndex(source, first);
int secondIndex = requiredIndex(source, second); 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(")); assertTrue(open.contains("TRANSITIONS.submit("));
assertFalse(open.contains("orTimeout(")); assertFalse(open.contains("orTimeout("));
assertFalse(open.contains("deadlineNanos")); assertFalse(open.contains("deadlineNanos"));
assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.create("); assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.createTransientStudio(");
assertBefore(execute, "ModdedDimensionManager.create(", "ModdedDimensionManager.teleportAsync("); assertBefore(execute, "ModdedDimensionManager.createTransientStudio(", "ModdedDimensionManager.teleportAsync(");
assertFalse(execute.contains("deadlineNanos")); assertFalse(execute.contains("deadlineNanos"));
assertFalse(source.contains("player.teleportTo(studio")); 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.NativeStructureOwnershipRecord;
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore; import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.NativeStructureStartPlan; 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.IrisDimension;
import art.arcane.iris.engine.object.IrisNativeStructure; import art.arcane.iris.engine.object.IrisNativeStructure;
import art.arcane.iris.engine.object.IrisNativeStructureDecision; import art.arcane.iris.engine.object.IrisNativeStructureDecision;
@@ -429,6 +430,14 @@ public class NativeStructureReferenceRepairTest {
return data; return data;
} }
@Override
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
int blockX,
int blockZ
) {
return null;
}
@Override @Override
public boolean isClosing() { public boolean isClosing() {
return false; return false;
+3
View File
@@ -26,6 +26,8 @@ plugins {
alias(libs.plugins.shadow) alias(libs.plugins.shadow)
} }
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
Properties rootProperties = new Properties() Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2')) 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:core:${irisVersion}".toString(),
"art.arcane:spi:${irisVersion}".toString(), "art.arcane:spi:${irisVersion}".toString(),
libs.paralithic, libs.paralithic,
libs.asm,
libs.lru, libs.lru,
libs.caffeine, libs.caffeine,
libs.dom4j, libs.dom4j,
+4
View File
@@ -426,6 +426,8 @@ tasks.named('neoforgeJar').configure {
} }
List<String> commonModdedArtifactEntries = [ 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/engine/IrisEngine.class',
'art/arcane/iris/spi/IrisPlatform.class', 'art/arcane/iris/spi/IrisPlatform.class',
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class', 'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
@@ -530,6 +532,8 @@ tasks.register('verifyModdedArtifacts') {
// the two plugin descriptors and the slimjar manifests. Nothing in the reference graph points at // 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. // them, so a shadowJar exclude that drops one is silent until a server hits that path.
List<String> requiredBukkitArtifactEntries = [ List<String> requiredBukkitArtifactEntries = [
'META-INF/iris/generation-kernels/catalog.tsv',
'META-INF/iris/generation-kernels/abi-1.seal',
'plugin.yml', 'plugin.yml',
'paper-plugin.yml', 'paper-plugin.yml',
'slimjar.dat', 'slimjar.dat',
+1
View File
@@ -21,6 +21,7 @@ repositories {
dependencies { dependencies {
implementation(gradleApi()) implementation(gradleApi())
implementation('org.ow2.asm:asm:9.10.1') implementation('org.ow2.asm:asm:9.10.1')
implementation('org.apache.commons:commons-compress:1.28.0')
implementation('com.github.VolmitSoftware:NMSTools:c88961416f') implementation('com.github.VolmitSoftware:NMSTools:c88961416f')
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21') implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
testImplementation('junit:junit:4.13.2') 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 org.gradle.api.GradleException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption; 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.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public final class JarCompactor { public final class JarCompactor {
private static final int BUFFER_BYTES = 64 * 1024; 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 { private static void rewrite(Path source, Path destination) throws IOException {
byte[] buffer = new byte[BUFFER_BYTES]; byte[] buffer = new byte[BUFFER_BYTES];
try (InputStream rawInput = new BufferedInputStream(Files.newInputStream(source)); try (ZipFile input = new ZipFile(source.toFile());
ZipInputStream input = new ZipInputStream(rawInput); ZipArchiveOutputStream output = new ZipArchiveOutputStream(destination)) {
OutputStream rawOutput = new BufferedOutputStream(Files.newOutputStream(destination)); if (!output.isSeekable()) {
ZipOutputStream output = new ZipOutputStream(rawOutput)) { throw new IOException("Jar compaction requires seekable output.");
}
output.setLevel(9); output.setLevel(9);
ZipEntry entry; output.setUseZip64(Zip64Mode.Never);
while ((entry = input.getNextEntry()) != null) { 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()) { if (entry.isDirectory()) {
continue; continue;
} }
ZipEntry compacted = copyMetadata(entry); if (!names.add(entry.getName())) {
output.putNextEntry(compacted); throw new IOException("Duplicate jar entry: " + entry.getName());
int read;
while ((read = input.read(buffer)) >= 0) {
if (read > 0) {
output.write(buffer, 0, read);
}
} }
output.closeEntry(); output.putArchiveEntry(copyMetadata(entry));
copyEntry(input, output, entry, buffer);
output.closeArchiveEntry();
} }
} }
} }
private static ZipEntry copyMetadata(ZipEntry source) { private static void copyEntry(ZipFile input, ZipArchiveOutputStream output, ZipEntry entry, byte[] buffer)
ZipEntry target = new ZipEntry(source.getName()); 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); target.setMethod(ZipEntry.DEFLATED);
if (source.getTime() >= 0L) { if (source.getTime() >= 0L) {
target.setTime(source.getTime()); target.setTime(source.getTime());
@@ -78,7 +101,8 @@ public final class JarCompactor {
target.setComment(source.getComment()); target.setComment(source.getComment());
} }
if (source.getExtra() != null) { if (source.getExtra() != null) {
target.setExtra(source.getExtra()); target.setExtraFields(ExtraFieldUtils.parse(source.getExtra(), true,
ZipArchiveEntry.ExtraFieldParsingMode.BEST_EFFORT));
} }
return target; 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.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets; 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.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.jar.JarOutputStream; 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.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class JarCompactorTest { public class JarCompactorTest {
@Rule @Rule
@@ -36,4 +53,128 @@ public class JarCompactorTest {
jar.getJarEntry("example/value.txt")).readAllBytes()); 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' id 'java'
} }
repositories {
mavenCentral()
}
dependencies {
testImplementation('junit:junit:4.13.2')
}
tasks.named('jar', Jar).configure { tasks.named('jar', Jar).configure {
manifest.attributes( manifest.attributes(
'Agent-Class': 'art.arcane.iris.util.project.agent.Installer', 'Agent-Class': 'art.arcane.iris.util.project.agent.Installer',
@@ -1,9 +1,41 @@
package art.arcane.iris.util.project.agent; package art.arcane.iris.util.project.agent;
import java.lang.instrument.Instrumentation; 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 { public class Installer {
private static volatile Instrumentation instrumentation; 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() { public static Instrumentation getInstrumentation() {
Instrumentation instrumentation = Installer.instrumentation; Instrumentation instrumentation = Installer.instrumentation;
@@ -26,4 +58,4 @@ public class Installer {
return; return;
Installer.instrumentation = instrumentation; 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('junit:junit:4.13.2')
testImplementation('org.mockito:mockito-core:5.23.0') testImplementation('org.mockito:mockito-core:5.23.0')
testImplementation(libs.paper.api) testImplementation(libs.paper.api)
testImplementation(libs.log4j.core)
testRuntimeOnly(libs.paper.api) testRuntimeOnly(libs.paper.api)
testImplementation(libs.multiverseCore) testImplementation(libs.multiverseCore)
} }
@@ -520,3 +521,5 @@ tasks.register('bukkitPurityRatchet') {
tasks.named('check').configure { tasks.named('check').configure {
dependsOn('bukkitPurityRatchet') 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.nms.datapack.IDataFixer;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher; import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.pack.PackDirectoryResolver; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom; 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.IrisDimension;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
@@ -40,13 +46,14 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
public final class IrisDatapackCompiler { public final class IrisDatapackCompiler {
private static final int INPUT_FINGERPRINT_SCHEMA = 2; private static final int INPUT_FINGERPRINT_SCHEMA = 3;
private static final int INPUT_BUFFER_BYTES = 64 * 1024; private static final int INPUT_BUFFER_BYTES = 64 * 1024;
private static final int WORLD_PACK_SCAN_DEPTH = 8; private static final int WORLD_PACK_SCAN_DEPTH = 8;
private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+"); private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
@@ -90,6 +97,7 @@ public final class IrisDatapackCompiler {
) throws IOException { ) throws IOException {
LinkedHashMap<Path, File> roots = new LinkedHashMap<>(); LinkedHashMap<Path, File> roots = new LinkedHashMap<>();
collectInstalledPackRoots(dataDirectory.resolve("packs"), roots, validateWholePack); collectInstalledPackRoots(dataDirectory.resolve("packs"), roots, validateWholePack);
collectCanonicalWorldPackRoot(serverRoot, roots, validateWholePack);
collectWorldPackRoots(serverRoot.resolve("dimensions"), roots, validateWholePack); collectWorldPackRoots(serverRoot.resolve("dimensions"), roots, validateWholePack);
return new ArrayList<>(roots.values()); return new ArrayList<>(roots.values());
} }
@@ -179,14 +187,18 @@ public final class IrisDatapackCompiler {
Objects.requireNonNull(packRoots, "packRoots"); Objects.requireNonNull(packRoots, "packRoots");
Objects.requireNonNull(fixer, "fixer"); Objects.requireNonNull(fixer, "fixer");
LinkedHashMap<String, String> requirements = new LinkedHashMap<>(); LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
Set<String> installedBiomeKeys = new LinkedHashSet<>(); Map<String, Set<String>> biomeTags = new TreeMap<>();
for (File packRoot : packRoots) { for (File packRoot : packRoots) {
PackDirectoryResolver.requireSafePackTree(packRoot); PackDirectoryResolver.requireSafePackTree(packRoot);
if (!hasDimensions(packRoot.toPath())) { if (!hasDimensions(packRoot.toPath())) {
continue; continue;
} }
BiomeAliasContext aliasContext = biomeAliasContext(packRoot.toPath());
IrisData data = IrisData.openDatapackCompiler(packRoot); IrisData data = IrisData.openDatapackCompiler(packRoot);
try { try {
if (aliasContext.retainedContract() != null) {
data.bindGenerationRegistryContract(aliasContext.retainedContract());
}
ResourceLoader<IrisDimension> loader = data.getDimensionLoader(); ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
String[] possibleKeys = loader.getPossibleKeys(); String[] possibleKeys = loader.getPossibleKeys();
if (possibleKeys == null || possibleKeys.length == 0) { if (possibleKeys == null || possibleKeys.length == 0) {
@@ -199,14 +211,16 @@ public final class IrisDatapackCompiler {
} }
collectRegistryRequirements( collectRegistryRequirements(
requirements, requirements,
installedBiomeKeys, biomeTags,
dimension, dimension,
fixer); fixer,
aliasContext);
} }
} finally { } finally {
data.close(); data.close();
} }
} }
addBiomeTagRequirements(requirements, biomeTags);
return Map.copyOf(requirements); return Map.copyOf(requirements);
} }
@@ -216,23 +230,35 @@ public final class IrisDatapackCompiler {
) throws IOException { ) throws IOException {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension"); IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer"); IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer");
IrisData data = Objects.requireNonNull(
requiredDimension.getLoader(),
"Iris dimension loader"
);
LinkedHashMap<String, String> requirements = new LinkedHashMap<>(); LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
Map<String, Set<String>> biomeTags = new TreeMap<>();
BiomeAliasContext aliasContext = BiomeAliasContext.contentAddressedOnly();
collectRegistryRequirements( collectRegistryRequirements(
requirements, requirements,
new LinkedHashSet<>(), biomeTags,
requiredDimension, requiredDimension,
requiredFixer); requiredFixer,
aliasContext);
collectDimensionStackRegistryRequirements( collectDimensionStackRegistryRequirements(
requirements, requirements,
biomeTags,
requiredDimension, requiredDimension,
requiredFixer); requiredFixer,
aliasContext);
addBiomeTagRequirements(requirements, biomeTags);
return Map.copyOf(requirements); return Map.copyOf(requirements);
} }
private static void collectDimensionStackRegistryRequirements( private static void collectDimensionStackRegistryRequirements(
Map<String, String> requirements, Map<String, String> requirements,
Map<String, Set<String>> biomeTags,
IrisDimension hostDimension, IrisDimension hostDimension,
IDataFixer fixer IDataFixer fixer,
BiomeAliasContext aliasContext
) throws IOException { ) throws IOException {
if (!hostDimension.hasDimensionStack()) { if (!hostDimension.hasDimensionStack()) {
return; return;
@@ -255,21 +281,31 @@ public final class IrisDatapackCompiler {
} }
collectRegistryRequirements( collectRegistryRequirements(
requirements, requirements,
new LinkedHashSet<>(), biomeTags,
sourceDimension, sourceDimension,
fixer); fixer,
aliasContext);
} }
} }
private static void collectRegistryRequirements( private static void collectRegistryRequirements(
Map<String, String> requirements, Map<String, String> requirements,
Set<String> installedBiomeKeys, Map<String, Set<String>> biomeTags,
IrisDimension dimension, IrisDimension dimension,
IDataFixer fixer IDataFixer fixer,
) { BiomeAliasContext aliasContext
requirements.put( ) throws IOException {
"dimension_type/iris:" + dimension.getDimensionTypeKey(), String dimensionTypeResourceKey = "iris:" + dimension.getDimensionTypeKey();
fingerprintContent(dimension.getDimensionType().toJson(fixer))); 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(); ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) { for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
@@ -281,13 +317,25 @@ public final class IrisDatapackCompiler {
if (customBiome == null) { if (customBiome == null) {
continue; continue;
} }
String biomeKey = dimension.getCustomBiomeKey(customBiome.getId()); String json = customBiome.generateJson(fixer, contentGate);
if (!installedBiomeKeys.add(biomeKey)) { String currentPhysicalKey = GenerationRegistryContractFactory.customBiomeResourceKey(
continue; 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<>(); TreeSet<String> tags = new TreeSet<>();
for (String tag : customBiome.getEffectiveTags(derivativeKey)) { for (String tag : customBiome.getEffectiveTags(derivativeKey)) {
String normalizedTag = normalizeRegistryKey(tag); String normalizedTag = normalizeRegistryKey(tag);
@@ -295,13 +343,48 @@ public final class IrisDatapackCompiler {
tags.add(normalizedTag); tags.add(normalizedTag);
} }
} }
requirements.put( for (String resourceKey : resourceKeys) {
"worldgen/biome_tags/" + biomeKey, String emittedJson = aliasContext.generatedSource(
fingerprintContent(String.join("\n", tags))); 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) { private static String normalizeRegistryKey(String key) {
if (key == null || key.isBlank()) { if (key == null || key.isBlank()) {
return null; return null;
@@ -359,8 +442,12 @@ public final class IrisDatapackCompiler {
if (!hasDimensions(packRoot.toPath())) { if (!hasDimensions(packRoot.toPath())) {
continue; continue;
} }
BiomeAliasContext aliasContext = biomeAliasContext(packRoot.toPath());
IrisData data = IrisData.openDatapackCompiler(packRoot); IrisData data = IrisData.openDatapackCompiler(packRoot);
try { try {
if (aliasContext.retainedContract() != null) {
data.bindGenerationRegistryContract(aliasContext.retainedContract());
}
ResourceLoader<IrisDimension> loader = data.getDimensionLoader(); ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
String[] possibleKeys = loader.getPossibleKeys(); String[] possibleKeys = loader.getPossibleKeys();
if (possibleKeys == null || possibleKeys.length == 0) { if (possibleKeys == null || possibleKeys.length == 0) {
@@ -379,11 +466,22 @@ public final class IrisDatapackCompiler {
.add(new DimensionCandidate( .add(new DimensionCandidate(
dimension, dimension,
packRoot.toPath().toAbsolutePath().normalize(), 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<>()); KSet<String> seenBiomes = biomes.computeIfAbsent(dimension.getLoadKey(), ignored -> new KSet<>());
dimension.installBiomes(fixer, dimension::getLoader, datapackRoots, seenBiomes); dimension.installBiomes(
dimension.installDimensionType(fixer, datapackRoots); fixer,
dimension::getLoader,
datapackRoots,
seenBiomes,
aliasContext
);
dimension.installDimensionType(fixer, datapackRoots, aliasContext);
installedDimensions++; installedDimensions++;
dimensionCount++; 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( private static void collectNamespaceWorldPackRoots(
Path namespace, Path namespace,
List<Path> candidates List<Path> candidates
@@ -510,18 +625,12 @@ public final class IrisDatapackCompiler {
public FileVisitResult preVisitDirectory( public FileVisitResult preVisitDirectory(
Path directory, Path directory,
BasicFileAttributes attributes BasicFileAttributes attributes
) { ) throws IOException {
if (!directory.equals(namespace) if (!directory.equals(namespace)
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) { && PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
return FileVisitResult.SKIP_SUBTREE; return FileVisitResult.SKIP_SUBTREE;
} }
Path irisRoot = directory.resolve("iris"); if (collectWorldRootPackRoots(directory, candidates)) {
Path candidate = irisRoot.resolve("pack");
if (!Files.isSymbolicLink(irisRoot)
&& !Files.isSymbolicLink(candidate)
&& Files.isDirectory(candidate, LinkOption.NOFOLLOW_LINKS)
&& hasDimensions(candidate)) {
candidates.add(candidate);
return FileVisitResult.SKIP_SUBTREE; return FileVisitResult.SKIP_SUBTREE;
} }
return FileVisitResult.CONTINUE; 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 { private static List<Path> visibleDirectories(Path root) throws IOException {
try (Stream<Path> entries = Files.list(root)) { try (Stream<Path> entries = Files.list(root)) {
return entries return entries
@@ -562,6 +700,43 @@ public final class IrisDatapackCompiler {
roots.putIfAbsent(identity, normalized.toFile()); 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) { private static boolean hasDimensions(Path root) {
Path dimensions = root.resolve("dimensions"); Path dimensions = root.resolve("dimensions");
if (Files.isSymbolicLink(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 { public static final class DimensionHeight {
private final IDataFixer fixer; private final IDataFixer fixer;
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3]; private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
@@ -349,6 +349,11 @@ public class IrisSettings {
public static class IrisSettingsGenerator { public static class IrisSettingsGenerator {
public String defaultWorldType = "overworld"; public String defaultWorldType = "overworld";
public boolean preventLeafDecay = true; public boolean preventLeafDecay = true;
public int generationTransitionWidthBlocks = 256;
public int getGenerationTransitionWidthBlocks() {
return Math.max(16, Math.min(generationTransitionWidthBlocks, 8_192));
}
} }
@Data @Data
@@ -1,5 +1,6 @@
package art.arcane.iris.core; package art.arcane.iris.core;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
@@ -404,10 +405,6 @@ public final class IrisWorldStorage {
return Optional.ofNullable(NamespacedKey.fromString(namespace + ":" + key)); return Optional.ofNullable(NamespacedKey.fromString(namespace + ":" + key));
} }
public static File packRoot(NamespacedKey key) {
return new File(dimensionRoot(key), "iris/pack");
}
public static File requireFrozenDimensionRoot( public static File requireFrozenDimensionRoot(
File worldContainer, File worldContainer,
File levelRoot, File levelRoot,
@@ -485,26 +482,28 @@ public final class IrisWorldStorage {
return configuredRoot; return configuredRoot;
} }
public static File requireFrozenPackRoot(File dimensionRoot) { public static File requireActiveGenerationPackRoot(File dimensionRoot) {
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot") Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
.toPath() .toPath()
.toAbsolutePath() .toAbsolutePath()
.normalize(); .normalize();
Path irisRoot = root.resolve("iris"); try {
Path packRoot = irisRoot.resolve("pack"); return GenerationHistory.open(root).activePackRoot().toFile();
for (Path path : new Path[]{root, irisRoot, packRoot}) { } catch (IOException failure) {
if (Files.isSymbolicLink(path)) { throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
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);
}
} }
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) { private static boolean isExistingSafeDimensionRoot(Path storageRoot, Path dimensionRoot) {
@@ -367,7 +367,7 @@ public class IrisWorlds {
configuredWorldName, configuredWorldName,
worldKey worldKey
); );
return dimensionRoot.map(IrisWorldStorage::requireFrozenPackRoot); return dimensionRoot.map(IrisWorldStorage::requireActiveGenerationPackRoot);
} }
private static void warnUnusableStorage(String worldIdentity, IllegalStateException failure) { 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", IrisLogging.error("Iris world %s has unusable world storage and is excluded: %s",
worldIdentity, failure.getMessage()); 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); worldIdentity);
} }
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) { for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
if (i.isCustom()) { if (i.isCustom()) {
for (IrisBiomeCustom j : i.getCustomDerivitives()) { 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.IrisWorldStorage;
import art.arcane.iris.core.WorldSlotKey; import art.arcane.iris.core.WorldSlotKey;
import art.arcane.iris.engine.IrisEngineMantle; import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.history.GenerationHistoryPaths;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.InvalidConfigurationException;
@@ -229,8 +230,20 @@ public final class BukkitWorldConfiguration {
} }
Path resolvedRoot = dimensionRoot.toPath().toAbsolutePath().normalize(); Path resolvedRoot = dimensionRoot.toPath().toAbsolutePath().normalize();
try { try {
IrisWorldStorage.requireFrozenPackRoot(dimensionRoot); IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot);
} catch (IllegalStateException unusablePack) { } 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. // Only a folder that owns something can be corrupted by a vanilla generator writing into it.
return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot, return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot,
hasWorldData(resolvedRoot) hasWorldData(resolvedRoot)
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.ExactWorldSlotPathPolicy; import art.arcane.iris.core.ExactWorldSlotPathPolicy;
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter; import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.core.pack.PackDirectoryResolver; import art.arcane.iris.core.pack.PackDirectoryResolver;
import java.io.IOException; import java.io.IOException;
@@ -94,7 +95,7 @@ public final class WorldReplacementFilesystem {
State state = inspect(requiredPaths); State state = inspect(requiredPaths);
if (state.stagePresent()) { if (state.stagePresent()) {
requireSafeTree(requiredPaths.stage(), "replacement stage"); requireSafeTree(requiredPaths.stage(), "replacement stage");
requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint); requireFingerprint(activePackRoot(requiredPaths.stage()), expectedFingerprint);
if (originalTargetPresent) { if (originalTargetPresent) {
Path retainedWorld = state.targetPresent() Path retainedWorld = state.targetPresent()
? requiredPaths.target() ? requiredPaths.target()
@@ -118,7 +119,7 @@ public final class WorldReplacementFilesystem {
throw new IOException("Replacement backup state does not match the original target state."); throw new IOException("Replacement backup state does not match the original target state.");
} }
requireSafeTree(requiredPaths.stage(), "replacement stage"); requireSafeTree(requiredPaths.stage(), "replacement stage");
requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint); requireFingerprint(activePackRoot(requiredPaths.stage()), expectedFingerprint);
move(requiredPaths.stage(), requiredPaths.target()); move(requiredPaths.stage(), requiredPaths.target());
state = inspect(requiredPaths); state = inspect(requiredPaths);
} }
@@ -130,11 +131,11 @@ public final class WorldReplacementFilesystem {
throw new IOException("Replacement publication lost or unexpectedly created its backup."); throw new IOException("Replacement publication lost or unexpectedly created its backup.");
} }
requireSafeTree(requiredPaths.target(), "replacement target"); requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint); requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
if (originalTargetPresent) { if (originalTargetPresent) {
preservePaperWorldMetadata(requiredPaths.backup(), requiredPaths.target()); preservePaperWorldMetadata(requiredPaths.backup(), requiredPaths.target());
requireSafeTree(requiredPaths.target(), "replacement 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."); throw new IOException("Committed replacement storage does not contain one exact target directory.");
} }
requireSafeTree(requiredPaths.target(), "replacement target"); requireSafeTree(requiredPaths.target(), "replacement target");
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint); requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
} }
public static void validatePublishedTarget( 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."); throw new IOException("Published replacement storage does not match its retained-world contract.");
} }
requireSafeTree(requiredPaths.target(), "replacement target"); 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 { public static String fingerprintPack(Path packRoot) throws IOException {
@@ -279,6 +280,10 @@ public final class WorldReplacementFilesystem {
return HexFormat.of().formatHex(digest.digest()); 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) { private static FingerprintEntry fingerprintEntry(Path root, Path path) {
Path relative = root.relativize(path); Path relative = root.relativize(path);
return new FingerprintEntry(path, relative, relative.toString()); 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.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator; import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog; 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.StructureRecoveryResult;
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter; import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.engine.object.IrisBiome; 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.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity; 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 art.arcane.iris.util.project.context.IrisContext;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -87,7 +93,9 @@ import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@@ -137,6 +145,17 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private final transient List<Engine> engines = new ArrayList<>(); 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 transient volatile Engine soleEngine;
private IrisData(File dataFolder) { private IrisData(File dataFolder) {
@@ -170,6 +189,90 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return Optional.ofNullable(dataLoaders.get(dataFolder)); 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) { public static boolean invalidateLoadedStructureResources(File dataFolder) {
Path requested = dataFolderIdentity(Objects.requireNonNull( Path requested = dataFolderIdentity(Objects.requireNonNull(
dataFolder, dataFolder,
@@ -273,7 +376,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
try { try {
if (nearest != null) { if (nearest != null) {
T t = nearest.load(type, key, false); T t = nearest.load(type, key, false);
if (t != null) { if (t != null || nearest.generationRegistryContract != null) {
return t; return t;
} }
} }
@@ -516,6 +619,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
StructureGraphCatalog.invalidate(this); StructureGraphCatalog.invalidate(this);
IrisObjectScale.invalidate(this); IrisObjectScale.invalidate(this);
contentGate = null; contentGate = null;
generationPackFingerprint = null;
synchronized (dimensionCustomBiomeResourceKeys) {
dimensionCustomBiomeResourceKeys.clear();
}
compatReport.clear(); compatReport.clear();
closed = false; closed = false;
possibleSnippets = new KMap<>(); possibleSnippets = new KMap<>();
@@ -831,4 +938,34 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
} }
IrisLogging.debug("Loaded Prefetch Cache to reduce generation disk use."); 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)." + " (dimension blockFallbacks, block backup)."
); );
public static final LinesKey COMMAND_DEVELOPER_UPDATE_WORLD_WARNING = LinesKey.of( public static final LinesKey COMMAND_DEVELOPER_UPDATE_WORLD_WARNING = LinesKey.of(
"iris.bukkit.runtime.commanddeveloper.update_world_warning", "iris.bukkit.runtime.commanddeveloper.stage_world_generation_update_warning",
C.RED + "You should always make a backup before using this", C.RED + "Back up the complete world before staging this update.",
C.YELLOW + "Issues caused by this can be, but are not limited to:", C.YELLOW + "Existing chunks will not be regenerated or rewritten.",
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)", C.YELLOW + "New chunks use the staged pack after restart and blend across the frozen boundary.",
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks", C.YELLOW + "The world seed, height, environment, and dimension type cannot change.",
C.YELLOW + " - Structures not spawning again when regenerating", C.YELLOW + "Every backup must retain the complete iris/generation directory.",
C.YELLOW + " - Caves not lining up", C.RED + "To stage the update and request a clean restart:",
C.YELLOW + " - Terrain layers not lining up",
C.RED + "Now that you are aware of the risks, and have made a back-up:",
C.RED + "/iris developer update-world {world} {pack} confirm=true" 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( 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", "iris.director.commanddeveloper.param.center_chunk_z",
"Center chunk Z" "Center chunk Z"
); );
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE = TextKey.of( public static final TextKey COMMAND_DEVELOPER_DIRECTOR_STAGE_WORLD_GENERATION_UPDATE = TextKey.of(
"iris.director.commanddeveloper.director.update_pack_world_unsafe", "iris.director.commanddeveloper.director.stage_world_generation_update",
"Update the pack of a world (UNSAFE!)" "Stage a world generation update"
); );
public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_UPDATE = TextKey.of( public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_UPDATE = TextKey.of(
"iris.director.commanddeveloper.param.world_update", "iris.director.commanddeveloper.param.world_update",
@@ -889,7 +889,7 @@ public final class DirectorCommandMessages {
COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER, COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER,
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X, COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X,
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z, 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_WORLD_UPDATE,
COMMAND_DEVELOPER_PARAM_PACK_INSTALL_INTO_WORLD, COMMAND_DEVELOPER_PARAM_PACK_INSTALL_INTO_WORLD,
COMMAND_DEVELOPER_PARAM_MAKE_SURE_MAKE_BACKUP_READ_WARNINGS_FIRST, 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.framework.NativeStructureVolume;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata; import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
@@ -57,9 +58,14 @@ import java.awt.Color;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public interface INMSBinding { 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(Material material);
boolean hasTile(Location l); boolean hasTile(Location l);
@@ -273,8 +279,18 @@ public interface INMSBinding {
default void uninjectBukkit() { default void uninjectBukkit() {
} }
default boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) { default void deferPluginClassLoaderClose() {
return true; }
default void releasePluginClassLoaderClose() {
}
default boolean isServerStopping() {
return false;
}
default ServerShutdownBoundary createServerShutdownBoundary() {
return new ServerShutdownBoundary(() -> true, Thread.currentThread());
} }
KMap<Material, List<BlockProperty>> getBlockProperties(); KMap<Material, List<BlockProperty>> getBlockProperties();
@@ -7,33 +7,32 @@ import java.util.function.BooleanSupplier;
public final class ServerShutdownBoundary { public final class ServerShutdownBoundary {
private static final long MAX_JOIN_SLICE_MILLIS = 1000L; 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( public boolean await(long timeout, TimeUnit unit) {
BooleanSupplier boundaryReached,
Thread serverThread,
long timeout,
TimeUnit unit
) {
BooleanSupplier reached = Objects.requireNonNull(boundaryReached, "Server shutdown boundary");
Thread activeServerThread = Objects.requireNonNull(serverThread, "Server thread");
TimeUnit activeUnit = Objects.requireNonNull(unit, "Server shutdown timeout unit"); TimeUnit activeUnit = Objects.requireNonNull(unit, "Server shutdown timeout unit");
if (reached.getAsBoolean()) { if (boundaryReached.getAsBoolean()) {
return true; return true;
} }
if (activeServerThread == Thread.currentThread()) { if (serverThread == Thread.currentThread()) {
return false; return false;
} }
long timeoutNanos = Math.max(0L, activeUnit.toNanos(timeout)); long timeoutNanos = Math.max(0L, activeUnit.toNanos(timeout));
long started = System.nanoTime(); long started = System.nanoTime();
boolean interrupted = false; boolean interrupted = false;
while (!reached.getAsBoolean()) { while (!boundaryReached.getAsBoolean()) {
long remaining = timeoutNanos - (System.nanoTime() - started); long remaining = timeoutNanos - (System.nanoTime() - started);
if (remaining <= 0L || !activeServerThread.isAlive()) { if (remaining <= 0L || !serverThread.isAlive()) {
restoreInterrupt(interrupted); restoreInterrupt(interrupted);
return reached.getAsBoolean(); return boundaryReached.getAsBoolean();
} }
long joinMillis = Math.max( long joinMillis = Math.max(
@@ -41,7 +40,7 @@ public final class ServerShutdownBoundary {
Math.min(MAX_JOIN_SLICE_MILLIS, TimeUnit.NANOSECONDS.toMillis(remaining)) Math.min(MAX_JOIN_SLICE_MILLIS, TimeUnit.NANOSECONDS.toMillis(remaining))
); );
try { try {
activeServerThread.join(joinMillis); serverThread.join(joinMillis);
} catch (InterruptedException e) { } catch (InterruptedException e) {
interrupted = true; 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.Filter;
import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger; import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.message.Message;
import java.util.List; 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. // 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 static final List<String> FILTERS = List.of(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID);
private boolean installed = false; private LoggerConfig installedLogger;
public void onEnable() { public void onEnable() {
if (installed) { if (installedLogger != null) {
return; return;
} }
((Logger) LogManager.getRootLogger()).addFilter(this); LoggerConfig logger = ((Logger) LogManager.getRootLogger()).get();
installed = true; logger.addFilter(this);
installedLogger = logger;
} }
public void initialize() { public void initialize() {
@@ -41,11 +43,12 @@ public class LogFilterSVC implements IrisService, Filter {
} }
public void onDisable() { public void onDisable() {
if (!installed) { LoggerConfig logger = installedLogger;
if (logger == null) {
return; return;
} }
((Logger) LogManager.getRootLogger()).get().removeFilter(this); logger.removeFilter(this);
installed = false; installedLogger = null;
} }
public boolean isStarted() { public boolean isStarted() {
@@ -69,10 +69,27 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
public void dereference() { public void dereference() {
IrisData.dereference(); IrisData.dereference();
threads.removeIf((i) -> !i.isAlive()); threads.removeIf((i) -> !i.isAlive());
services.removeIf(ExecutorService::isShutdown); services.removeIf(ExecutorService::isTerminated);
updateCaches(); 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 @Override
public void onEnable() { public void onEnable() {
/* /*
@@ -19,6 +19,7 @@
package art.arcane.iris.core.service; package art.arcane.iris.core.service;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.IrisStartupValidation; 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.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.data.cache.AtomicCache; 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.object.IrisDimension;
import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitPlatform; import art.arcane.iris.platform.bukkit.BukkitPlatform;
@@ -166,18 +175,197 @@ public class StudioSVC implements IrisService {
queueStudioWorldDeletionOnStartup(worldNamesToDelete); queueStudioWorldDeletionOnStartup(worldNamesToDelete);
} }
public IrisDimension installIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) { public IrisDimension installIntoWorld(
return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), false); VolmitSender sender,
IrisDimension dimension,
File dimensionRoot,
long worldSeed
) {
return publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, false);
} }
public IrisDimension replaceIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) { public IrisDimension replaceIntoWorld(
return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), true); 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) { public IrisDimension replaceIntoPackDirectory(VolmitSender sender, IrisDimension dimension, File folder) {
return installIntoDirectory(sender, dimension, folder, true); 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( private IrisDimension installIntoDirectory(
VolmitSender sender, VolmitSender sender,
IrisDimension dimension, IrisDimension dimension,
@@ -1534,6 +1722,13 @@ public class StudioSVC implements IrisService {
RESTART RESTART
} }
private record GenerationCandidate(
String packFingerprint,
GenerationEpoch.DimensionContract dimensionContract,
GenerationRegistryContract registryContract
) {
}
public IrisProject getActiveProject() { public IrisProject getActiveProject() {
return activeProject; return activeProject;
} }
@@ -263,8 +263,10 @@ public class IrisCreator {
IrisDimension installedDimension = resolvedDimension; IrisDimension installedDimension = resolvedDimension;
if (!studio() || benchmark) { if (!studio() || benchmark) {
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack"); reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
installedDimension = IrisServices.get(StudioSVC.class) StudioSVC studioService = IrisServices.get(StudioSVC.class);
.installIntoWorld(sender, resolvedDimension, dimensionRoot); installedDimension = benchmark
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
if (installedDimension == null) { if (installedDimension == null) {
throw new IrisException("Failed to install dimension pack for " + dimension()); 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.IrisWorldStorage;
import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pregenerator.PregenPerformanceProfile; import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.pregenerator.PregeneratorMethod;
@@ -57,7 +58,6 @@ import org.jetbrains.annotations.ApiStatus;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -74,7 +74,6 @@ import art.arcane.volmlib.util.localization.MessageArgument;
public class IrisToolbelt { public class IrisToolbelt {
@ApiStatus.Internal @ApiStatus.Internal
public static Map<String, Boolean> toolbeltConfiguration = new HashMap<>(); public static Map<String, Boolean> toolbeltConfiguration = new HashMap<>();
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
/** /**
* Finds an installed dimension or returns null. * Finds an installed dimension or returns null.
@@ -582,26 +581,7 @@ public class IrisToolbelt {
} }
public static boolean isServerStopping() { public static boolean isServerStopping() {
Method method = BUKKIT_IS_STOPPING_METHOD; return INMS.isBound() && INMS.get().isServerStopping();
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;
}
} }
public static void beginWorldMaintenance(World world, String reason) { 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.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld; 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.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.platform.bukkit.BukkitEnvironment; import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
@@ -31,6 +32,7 @@ import org.bukkit.WorldCreator;
import org.bukkit.generator.ChunkGenerator; import org.bukkit.generator.ChunkGenerator;
import java.io.File; import java.io.File;
import java.io.IOException;
public class IrisWorldCreator { public class IrisWorldCreator {
private String name; private String name;
@@ -100,15 +102,42 @@ public class IrisWorldCreator {
.seed(seed) .seed(seed)
.worldFolder(worldFolder) .worldFolder(worldFolder)
.build(); .build();
ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio GenerationHistory generationHistory = persistent
? dim.getLoader().getDataFolder() : ? requireGenerationHistory(w.worldFolder(), seed)
new File(w.worldFolder(), "iris/pack"), dimensionName); : 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) return creator.environment(environment)
.generateStructures(true) .generateStructures(true)
.generator(g).seed(seed); .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() { private World.Environment findEnvironment() {
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension; IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment()); return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment());
@@ -121,10 +121,10 @@ public final class DimensionTerrainContext implements DataProvider {
engine.getDimension(), engine.getDimension(),
engine.getData(), engine.getData(),
engine.getHeight(), engine.getHeight(),
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(), natural ? complex.getUnblendedNaturalHeightStream() : complex.getHeightStream(),
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(), natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
fluidHeightStream, fluidHeightStream,
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(), natural ? complex.getUnblendedNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
complex.getRegionStream(), complex.getRegionStream(),
complex.getRockStream(), complex.getRockStream(),
fluidBlockSampler, fluidBlockSampler,
@@ -20,7 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.protocol.IrisProtocolServer; 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.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState; import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.EngineTarget;
@@ -53,32 +53,40 @@ final class EngineHotloader {
void hotloadComplex() { void hotloadComplex() {
synchronized (engine.lifecycleLock) { synchronized (engine.lifecycleLock) {
engine.requireRunning("rebuild the biome complex"); engine.requireRunning("rebuild the biome complex");
requireMutableGenerationRuntime("rebuild the biome complex");
engine.awaitNativeStructureBootstrap("complex hotload"); engine.awaitNativeStructureBootstrap("complex hotload");
engine.lifecycleState = LifecycleState.HOTLOADING; engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previous = engine.runtime; EngineRuntime previous = engine.runtime;
GenerationRuntime previousGeneration = previous.generation();
IrisComplex nextComplex = null; IrisComplex nextComplex = null;
try { try {
engine.sealForTransition("complex hotload", false); engine.sealForTransition("complex hotload", false);
engine.prepareRuntimeHotload(); 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); engine.runtimeAssembly.set(assembly);
EngineRuntime next; EngineRuntime next;
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { 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; nextComplex = assembly.complex;
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext(); assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
assembly.upperContext = engine.runtimeBuilder.buildUpperContext(); assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes(); BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
next = previous.withComplex( GenerationRuntime nextGeneration = previousGeneration.withComplex(
assembly.cacheId, assembly.cacheId,
assembly.complex, assembly.complex,
assembly.upperContext, assembly.upperContext,
assembly.dimensionStackContext, assembly.dimensionStackContext,
biomeMaxes); biomeMaxes);
next = previous.withGeneration(nextGeneration);
} finally { } finally {
engine.runtimeAssembly.remove(); engine.runtimeAssembly.remove();
} }
Throwable retirementFailure = runCleanup(null, previous.complex()::close); Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close);
if (retirementFailure != null) { if (retirementFailure != null) {
retirementFailure = runCleanup(retirementFailure, nextComplex::close); retirementFailure = runCleanup(retirementFailure, nextComplex::close);
nextComplex = null; nextComplex = null;
@@ -91,7 +99,7 @@ final class EngineHotloader {
engine.getClosing().set(false); engine.getClosing().set(false);
engine.backgroundTasks.openBackgroundTaskAdmission(); engine.backgroundTasks.openBackgroundTaskAdmission();
} catch (Throwable e) { } catch (Throwable e) {
if (nextComplex != null && nextComplex != previous.complex()) { if (nextComplex != null && nextComplex != previousGeneration.complex()) {
Throwable cleanupFailure = runCleanup(null, nextComplex::close); Throwable cleanupFailure = runCleanup(null, nextComplex::close);
if (cleanupFailure != null) { if (cleanupFailure != null) {
e.addSuppressed(cleanupFailure); e.addSuppressed(cleanupFailure);
@@ -108,6 +116,7 @@ final class EngineHotloader {
void hotloadSilently() { void hotloadSilently() {
synchronized (engine.lifecycleLock) { synchronized (engine.lifecycleLock) {
engine.requireRunning("hotload"); engine.requireRunning("hotload");
requireMutableGenerationRuntime("hotload");
engine.awaitNativeStructureBootstrap("hotload"); engine.awaitNativeStructureBootstrap("hotload");
engine.lifecycleState = LifecycleState.HOTLOADING; engine.lifecycleState = LifecycleState.HOTLOADING;
EngineRuntime previousRuntime = engine.runtime; 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 * 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 * {@code IllegalStateException} out of the codec (a long pack key or exception message overruns
@@ -19,48 +19,26 @@
package art.arcane.iris.engine; package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.EngineEffects; 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 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. * Immutable snapshot of everything an {@link IrisEngine} publishes as a single unit.
* Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}. * Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}.
*/ */
record EngineRuntime( record EngineRuntime(
int cacheId, GenerationRuntime generation,
EngineTarget target,
IrisComplex complex,
UpperDimensionContext upperContext,
DimensionStackContext dimensionStackContext,
EngineEffects effects, EngineEffects effects,
EngineMode mode, EngineWorldManager worldManager
EngineWorldManager worldManager,
CompletableFuture<Long> hash32,
BiomeMaxes biomeMaxes
) { ) {
EngineRuntime withComplex( EngineRuntime {
int nextCacheId, Objects.requireNonNull(generation, "generation runtime");
IrisComplex nextComplex, Objects.requireNonNull(effects, "engine effects");
UpperDimensionContext nextUpperContext, Objects.requireNonNull(worldManager, "engine world manager");
DimensionStackContext nextDimensionStackContext,
BiomeMaxes nextBiomeMaxes
) {
return new EngineRuntime(
nextCacheId,
target,
nextComplex,
nextUpperContext,
nextDimensionStackContext,
effects,
mode,
worldManager,
hash32,
nextBiomeMaxes);
} }
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.IrisData;
import art.arcane.iris.core.loader.ResourceLoader; 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.IrisEngine.LifecycleState;
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
import art.arcane.iris.engine.framework.EngineEffects; import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineEffectsProvider; import art.arcane.iris.engine.framework.EngineEffectsProvider;
import art.arcane.iris.engine.framework.EngineMode; import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineTarget; import art.arcane.iris.engine.framework.EngineTarget;
import art.arcane.iris.engine.framework.EngineWorldManager; import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.EngineWorldManagerProvider; 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.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomePaletteLayer; import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
import art.arcane.iris.engine.object.IrisDecorator; 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.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.project.context.IrisContext;
@@ -45,6 +43,9 @@ import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.M;
import java.io.File; 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.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -64,22 +65,56 @@ final class EngineRuntimeBuilder {
} }
EngineRuntime buildRuntime() { EngineRuntime buildRuntime() {
return buildRuntime(engine.getTarget()); return buildRuntime(
engine.getTarget(),
engine.getInitialMantleStorageDirectory(),
engine.getInitialKernelVersion(),
engine.getInitialTransitionPlan(),
null);
} }
EngineRuntime buildRuntime(EngineTarget runtimeTarget) { 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); engine.runtimeAssembly.set(assembly);
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
IrisLogging.debug("Setup Engine " + assembly.cacheId); IrisLogging.debug("Setup Engine " + assembly.cacheId);
long started = M.ms(); 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"); IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
started = M.ms(); started = M.ms();
assembly.dimensionStackContext = buildDimensionStackContext(); assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms"); IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
started = M.ms(); started = M.ms();
assembly.upperContext = buildUpperContext(); assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms"); IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
started = M.ms(); started = M.ms();
assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine); assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine);
@@ -89,21 +124,16 @@ final class EngineRuntimeBuilder {
IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms"); IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms");
assembly.hash32 = new CompletableFuture<>(); assembly.hash32 = new CompletableFuture<>();
started = M.ms(); started = M.ms();
engine.getMantle().hotload(); assembly.mantle.hotload();
IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms"); IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms");
started = M.ms(); started = M.ms();
assembly.mode = createMode(); assembly.mode = assembly.runtimeKernel.createMode(engine);
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData()); assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode);
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) -> assembly.runtimeKernel.registerDimensionStack(
staticObjects.apply(engine, x, z, blocks)); engine,
if (assembly.dimensionStackContext != null) { assembly.mode,
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine); assembly.dimensionStackContext
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));
}
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms"); IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
started = M.ms(); started = M.ms();
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine); assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
@@ -112,7 +142,7 @@ final class EngineRuntimeBuilder {
} }
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms"); IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
BiomeMaxes biomeMaxes = computeBiomeMaxes(); BiomeMaxes biomeMaxes = computeBiomeMaxes();
return assembly.freeze(biomeMaxes); return assembly.freezeRuntime(biomeMaxes);
} catch (Throwable e) { } catch (Throwable e) {
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e); Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
if (cleanupFailure != 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) { 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) { if (retirementFailure != null) {
retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure); retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure);
engine.lifecycleState = LifecycleState.FAILED; engine.lifecycleState = LifecycleState.FAILED;
@@ -136,7 +218,7 @@ final class EngineRuntimeBuilder {
} }
engine.runtime = next; engine.runtime = next;
engine.publishedTarget = next.target(); engine.publishedTarget = next.generation().target();
engine.getGenerationSessions().activateNextSession(); engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING; engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false); engine.getClosing().set(false);
@@ -163,33 +245,11 @@ final class EngineRuntimeBuilder {
throw new IllegalStateException("Failed to start the Iris world manager.", e); throw new IllegalStateException("Failed to start the Iris world manager.", e);
} }
scheduleRuntimeTasks(next); scheduleRuntimeTasks(next);
IrisLogging.debug("Engine Setup Complete " + next.cacheId()); IrisLogging.debug("Engine Setup Complete " + next.generation().cacheId());
} }
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) { private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
try { schedulePackHash(engineRuntime.generation());
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);
}
try { try {
if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) { if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) {
throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh."); throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh.");
@@ -199,71 +259,34 @@ final class EngineRuntimeBuilder {
} }
} }
UpperDimensionContext buildUpperContext() { private void schedulePackHash(GenerationRuntime generationRuntime) {
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;
try { try {
IrisDimensionMode configuredMode = engine.getDimension().getMode(); if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
if (configuredMode == null) { try {
configuredMode = new IrisDimensionMode(); generationRuntime.hash32().complete(computePackHash(generationRuntime.data()));
engine.getDimension().setMode(configuredMode); } 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) { } catch (Throwable e) {
configuredFailure = e; generationRuntime.hash32().completeExceptionally(e);
IrisLogging.reportError(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 { private long computePackHash(IrisData data) {
EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(engine); File[] roots = data.getLoaders()
if (fallback == null) { .values()
throw new IllegalStateException("OVERWORLD mode factory returned null"); .stream()
} .map(ResourceLoader::getFolderName)
return fallback; .map(name -> new File(data.getDataFolder(), name))
} catch (Throwable fallbackFailure) { .filter(File::exists)
fallbackFailure.addSuppressed(configuredFailure); .filter(File::isDirectory)
throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure); .toArray(File[]::new);
} return IO.hashRecursiveMeta(roots);
} }
BiomeMaxes computeBiomeMaxes() { BiomeMaxes computeBiomeMaxes() {
@@ -317,11 +340,27 @@ final class EngineRuntimeBuilder {
return current; 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 { static final class RuntimeAssembly {
private static final AtomicInteger RUNTIME_IDS = new AtomicInteger(); private static final AtomicInteger RUNTIME_IDS = new AtomicInteger();
final int cacheId; final int cacheId;
final EngineTarget target; final EngineTarget target;
final Path mantleStorageDirectory;
final GenerationKernelRegistry.Version kernelVersion;
final GenerationKernelRegistry.RuntimeKernel runtimeKernel;
final SeedManager seedManager;
final TransitionGenerationPlan transitionPlan;
static int nextRuntimeId() { static int nextRuntimeId() {
return RUNTIME_IDS.incrementAndGet(); return RUNTIME_IDS.incrementAndGet();
@@ -331,29 +370,76 @@ final class EngineRuntimeBuilder {
DimensionStackContext dimensionStackContext; DimensionStackContext dimensionStackContext;
EngineEffects effects; EngineEffects effects;
EngineMode mode; EngineMode mode;
EngineMantle mantle;
EngineWorldManager worldManager; EngineWorldManager worldManager;
CompletableFuture<Long> hash32; CompletableFuture<Long> hash32;
boolean ownsMantle;
RuntimeAssembly(int cacheId, EngineTarget target) { RuntimeAssembly(int cacheId, EngineTarget target) {
this.cacheId = cacheId; this(
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(
cacheId, cacheId,
target, 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, complex,
upperContext, upperContext,
dimensionStackContext, dimensionStackContext,
effects,
mode, mode,
worldManager, mantle,
mantleStorageDirectory,
hash32, hash32,
biomeMaxes); 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.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.engine.IrisEngine.LifecycleState; import art.arcane.iris.engine.IrisEngine.LifecycleState;
import art.arcane.iris.engine.framework.GenerationSessionException; 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.NativeStructureOwnershipStore;
import art.arcane.iris.engine.framework.PreservationRegistry; 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.IrisLogging;
import art.arcane.iris.spi.IrisServices; import art.arcane.iris.spi.IrisServices;
@@ -38,7 +40,6 @@ final class EngineShutdownSequence {
private final IrisEngine engine; private final IrisEngine engine;
private boolean runtimeReleased; private boolean runtimeReleased;
private boolean targetReleased; private boolean targetReleased;
private boolean mantleReleased;
private boolean engineDataReleased; private boolean engineDataReleased;
private boolean preservationReleased; private boolean preservationReleased;
@@ -47,6 +48,17 @@ final class EngineShutdownSequence {
} }
void close() { 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; Throwable failure;
synchronized (engine.lifecycleLock) { synchronized (engine.lifecycleLock) {
if (engine.closed) { if (engine.closed) {
@@ -122,14 +134,10 @@ final class EngineShutdownSequence {
if (runtimeReleased) { if (runtimeReleased) {
failure = releaseTarget(failure); failure = releaseTarget(failure);
} }
if (targetReleased) {
failure = releaseMantle(failure);
}
if (prefetchFailure == null if (prefetchFailure == null
&& engineDataFailure == null && engineDataFailure == null
&& runtimeReleased && runtimeReleased
&& targetReleased && targetReleased) {
&& mantleReleased) {
failure = releaseEngineDataForShutdown(failure); failure = releaseEngineDataForShutdown(failure);
} }
if (engineDataReleased) { if (engineDataReleased) {
@@ -140,7 +148,6 @@ final class EngineShutdownSequence {
if (failure == null if (failure == null
&& runtimeReleased && runtimeReleased
&& targetReleased && targetReleased
&& mantleReleased
&& engineDataReleased && engineDataReleased
&& preservationReleased) { && preservationReleased) {
engine.closed = true; engine.closed = true;
@@ -185,10 +192,10 @@ final class EngineShutdownSequence {
() -> NativeStructureOwnershipStore.close(engine)); () -> NativeStructureOwnershipStore.close(engine));
cleanupFailure = appendFailure(cleanupFailure, ownershipFailure); cleanupFailure = appendFailure(cleanupFailure, ownershipFailure);
if (ownershipFailure == null) { if (ownershipFailure == null) {
cleanupFailure = closeDetachedGenerationRuntimes(cleanupFailure);
cleanupFailure = closeRuntime(engine.runtime, cleanupFailure); cleanupFailure = closeRuntime(engine.runtime, cleanupFailure);
engine.runtime = null; engine.runtime = null;
cleanupFailure = runCleanup(cleanupFailure, engine.getTarget()::close); cleanupFailure = runCleanup(cleanupFailure, engine.publishedTarget::close);
cleanupFailure = runCleanup(cleanupFailure, engine.getMantle()::close);
cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData); cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData);
engine.closed = true; engine.closed = true;
cleanupFailure = runCleanup(cleanupFailure, () -> { cleanupFailure = runCleanup(cleanupFailure, () -> {
@@ -232,18 +239,69 @@ final class EngineShutdownSequence {
assembly.hash32.cancel(true); 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; return failure;
} }
Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) { Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) {
return closeRuntime(engineRuntime, null, failure);
}
Throwable closeRuntime(
EngineRuntime engineRuntime,
EngineMantle retainedMantle,
Throwable failure
) {
if (engineRuntime == null) { if (engineRuntime == null) {
return failure; return failure;
} }
failure = runCleanup(failure, engineRuntime.worldManager()::close); failure = runCleanup(failure, engineRuntime.worldManager()::close);
failure = runCleanup(failure, engineRuntime.effects()::close); failure = runCleanup(failure, engineRuntime.effects()::close);
failure = runCleanup(failure, engineRuntime.mode()::close); return closeGenerationRuntime(engineRuntime.generation(), retainedMantle, failure);
failure = runCleanup(failure, engineRuntime.complex()::close); }
failure = runCleanup(failure, () -> engineRuntime.hash32().cancel(true));
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; return failure;
} }
@@ -251,6 +309,10 @@ final class EngineShutdownSequence {
if (runtimeReleased) { if (runtimeReleased) {
return failure; return failure;
} }
Throwable detachedFailure = closeDetachedGenerationRuntimes(null);
if (detachedFailure != null) {
return appendFailure(failure, detachedFailure);
}
Throwable runtimeFailure = closeRuntime(engine.runtime, null); Throwable runtimeFailure = closeRuntime(engine.runtime, null);
if (runtimeFailure != null) { if (runtimeFailure != null) {
return appendFailure(failure, runtimeFailure); return appendFailure(failure, runtimeFailure);
@@ -260,11 +322,23 @@ final class EngineShutdownSequence {
return failure; 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) { private Throwable releaseTarget(Throwable failure) {
if (targetReleased) { if (targetReleased) {
return failure; return failure;
} }
Throwable targetFailure = runCleanup(null, engine.getTarget()::close); Throwable targetFailure = runCleanup(null, engine.publishedTarget::close);
if (targetFailure != null) { if (targetFailure != null) {
return appendFailure(failure, targetFailure); return appendFailure(failure, targetFailure);
} }
@@ -272,18 +346,6 @@ final class EngineShutdownSequence {
return failure; 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) { private Throwable releaseEngineDataForShutdown(Throwable failure) {
if (engineDataReleased) { if (engineDataReleased) {
return failure; 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);
}
}
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff. Show more