mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Merged generation history
- Immutable generation epochs now coexist with dimension stacks - Historical terrain, biomes, structures, and locate behavior remain stable across updates - Versioned generation kernels preserve the combined runtime contract
This commit is contained in:
commit
56b5f4dd0b
263 files changed
+39624
-1849
No files matched your search
+169
-80
@@ -8,9 +8,11 @@ import art.arcane.iris.engine.DimensionStackContext;
|
||||
import art.arcane.iris.engine.DimensionStackLayout;
|
||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
@@ -40,6 +42,7 @@ import org.bukkit.craftbukkit.CraftWorld;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -65,14 +68,11 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private final Registry<Biome> biomeRegistry;
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
||||
private volatile IrisDimension cacheDimension;
|
||||
private volatile int cacheRuntimeId;
|
||||
private final ConcurrentHashMap<RuntimeNoiseKey, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<RuntimeNoiseKey, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<RuntimeColumnKey, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<RuntimeColumnKey, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, RuntimeBiomeState> runtimeBiomeStates = new ConcurrentHashMap<>();
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
@@ -80,10 +80,10 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
||||
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
||||
this.cacheDimension = engine.getDimension();
|
||||
this.cacheRuntimeId = engine.getCacheID();
|
||||
if (engine instanceof IrisEngine irisEngine) {
|
||||
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
|
||||
}
|
||||
runtimeBiomeState();
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(
|
||||
@@ -133,7 +133,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
if (irisBiome.isCustom()) {
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(
|
||||
customRegistry, ownedBiome.dimension(), customBiome.getId());
|
||||
customRegistry, engine, ownedBiome.dimension(), customBiome.getId());
|
||||
if (customHolder == null) {
|
||||
throw new IllegalStateException("Iris custom visible biome '"
|
||||
+ customBiomeKey(ownedBiome.dimension(), customBiome.getId())
|
||||
@@ -257,7 +257,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
runtimeBiomeState();
|
||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||
@@ -284,7 +284,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
String key = customBiomeKey(ownedBiome.dimension(), j.getId());
|
||||
Holder<Biome> holder = resolveCustomBiomeHolder(
|
||||
customRegistry, ownedBiome.dimension(), j.getId());
|
||||
customRegistry, engine, ownedBiome.dimension(), j.getId());
|
||||
if (holder == null) {
|
||||
if (fallback != null) {
|
||||
m.put(key, fallback);
|
||||
@@ -318,7 +318,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(
|
||||
customRegistry, ownedBiome.dimension(), customBiome.getId());
|
||||
customRegistry, engine, ownedBiome.dimension(), customBiome.getId());
|
||||
if (customHolder != null) {
|
||||
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
}
|
||||
@@ -340,8 +340,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
if (!isRuntimeAvailable()) {
|
||||
return null;
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
return vanillaSpawnBiomes.get(biome.value());
|
||||
return runtimeBiomeState().vanillaSpawnBiomes().get(biome.value());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +353,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
if (!isRuntimeAvailable()) {
|
||||
return null;
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
runtimeBiomeState();
|
||||
int quartX = QuartPos.fromBlock(blockX);
|
||||
int quartZ = QuartPos.fromBlock(blockZ);
|
||||
int sampleX = QuartPos.toBlock(quartX);
|
||||
@@ -390,15 +389,17 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||
openHistoryCoordinateScope(blockX, blockZ, "bukkit_structure_biome");
|
||||
GenerationSessionLease lease = requireGenerationLease(
|
||||
"bukkit_structure_biome",
|
||||
"Iris structure biome lookup was rejected during an engine transition");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
|
||||
}
|
||||
}
|
||||
@@ -409,7 +410,8 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
boolean cacheable = isBiomeCacheable(x, z);
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
|
||||
engine.getCacheID(), packNoiseKey(x, y, z));
|
||||
if (cacheable) {
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
@@ -455,7 +457,6 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
return findNaturalSurfaceBiomeHorizontal(
|
||||
x, y, z, searchRadius, quartStep, allowed, random);
|
||||
}
|
||||
@@ -533,7 +534,6 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||
boolean monumentQuery = radius == 29
|
||||
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||
@@ -556,20 +556,27 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
||||
long columnKey = packColumnKey(x, z);
|
||||
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
|
||||
if (surfaceHolder != null) {
|
||||
return surfaceHolder;
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||
openHistoryCoordinateScope(blockX, blockZ, "bukkit_surface_structure_biome")) {
|
||||
RuntimeColumnKey columnKey = new RuntimeColumnKey(
|
||||
engine.getCacheID(), packColumnKey(x, z));
|
||||
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
|
||||
if (surfaceHolder != null) {
|
||||
return surfaceHolder;
|
||||
}
|
||||
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(
|
||||
columnKey, resolvedSurfaceHolder);
|
||||
if (existingSurfaceHolder != null) {
|
||||
return existingSurfaceHolder;
|
||||
}
|
||||
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
surfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedSurfaceHolder;
|
||||
}
|
||||
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
|
||||
if (existingSurfaceHolder != null) {
|
||||
return existingSurfaceHolder;
|
||||
}
|
||||
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
surfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedSurfaceHolder;
|
||||
}
|
||||
|
||||
private Pair<BlockPos, Holder<Biome>> findNaturalSurfaceBiomeHorizontal(
|
||||
@@ -609,21 +616,27 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) {
|
||||
long columnKey = packColumnKey(x, z);
|
||||
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||
openHistoryCoordinateScope(blockX, blockZ, "bukkit_natural_structure_biome")) {
|
||||
RuntimeColumnKey columnKey = new RuntimeColumnKey(
|
||||
engine.getCacheID(), packColumnKey(x, z));
|
||||
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
|
||||
columnKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
naturalSurfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedHolder;
|
||||
}
|
||||
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
|
||||
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
|
||||
columnKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
naturalSurfaceStructureBiomeCache.clear();
|
||||
}
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
||||
@@ -671,11 +684,12 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
x << 2, z << 2, "bukkit_visible_biome");
|
||||
GenerationSessionLease lease = requireGenerationLease(
|
||||
"bukkit_visible_biome",
|
||||
"Iris visible biome lookup was rejected during an engine transition");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
prepareVisibleBiomeBatch();
|
||||
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
|
||||
}
|
||||
@@ -685,7 +699,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
if (!isRuntimeAvailable()) {
|
||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||
}
|
||||
ensureCachesCurrent();
|
||||
runtimeBiomeState();
|
||||
}
|
||||
|
||||
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||
@@ -717,7 +731,8 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
boolean cacheable
|
||||
) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
|
||||
engine.getCacheID(), packNoiseKey(x, y, z));
|
||||
if (cacheable) {
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
@@ -768,32 +783,71 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationSessionLease requireGenerationLease(String operation, String rejectionMessage) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(operation);
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException(rejectionMessage);
|
||||
}
|
||||
return lease;
|
||||
}
|
||||
|
||||
private boolean isRuntimeAvailable() {
|
||||
return !engine.isClosed() && engine.getComplex() != null;
|
||||
}
|
||||
|
||||
private void ensureCachesCurrent() {
|
||||
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
String operation
|
||||
) {
|
||||
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " could not route generation history at "
|
||||
+ blockX + "," + blockZ + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private void evictRuntimeCaches(int runtimeId) {
|
||||
runtimeBiomeStates.remove(runtimeId);
|
||||
noiseBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
structureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
surfaceStructureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
naturalSurfaceStructureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
}
|
||||
|
||||
private RuntimeBiomeState runtimeBiomeState() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
int runtimeId = engine.getCacheID();
|
||||
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
|
||||
return;
|
||||
RuntimeBiomeState existing = runtimeBiomeStates.get(runtimeId);
|
||||
if (existing != null) {
|
||||
if (existing.dimension() != dimension) {
|
||||
throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions.");
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
|
||||
return;
|
||||
synchronized (runtimeBiomeStates) {
|
||||
existing = runtimeBiomeStates.get(runtimeId);
|
||||
if (existing != null) {
|
||||
if (existing.dimension() != dimension) {
|
||||
throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions.");
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
|
||||
biomeCustomRegistry, engine, fallbackBiome);
|
||||
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
|
||||
biomeCustomRegistry, biomeRegistry, engine);
|
||||
noiseBiomeCache.clear();
|
||||
structureBiomeCache.clear();
|
||||
surfaceStructureBiomeCache.clear();
|
||||
naturalSurfaceStructureBiomeCache.clear();
|
||||
customBiomes = refreshedCustomBiomes;
|
||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
||||
cacheDimension = dimension;
|
||||
cacheRuntimeId = runtimeId;
|
||||
RuntimeBiomeState created = new RuntimeBiomeState(
|
||||
dimension,
|
||||
refreshedCustomBiomes,
|
||||
refreshedSpawnBiomes);
|
||||
runtimeBiomeStates.put(runtimeId, created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,6 +884,14 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
IrisDimensionCarvingResolver.State resolverState,
|
||||
DimensionStackLayout stackLayout
|
||||
) {
|
||||
int blockX = x << 2;
|
||||
int blockY = y << 2;
|
||||
int blockZ = z << 2;
|
||||
Optional<String> historicalKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
|
||||
blockX, blockY, blockZ);
|
||||
if (historicalKey.isPresent()) {
|
||||
return resolvePhysicalBiomeHolder(historicalKey.get());
|
||||
}
|
||||
BiomeResolution resolution = resolveBiomeResolution(
|
||||
x, y, z, resolverState, true, stackLayout);
|
||||
if (resolution == null) {
|
||||
@@ -858,7 +920,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiomeKey(
|
||||
Holder<Biome> holder = runtimeBiomeState().customBiomes().get(customBiomeKey(
|
||||
resolution.dimension, customBiome.getId()));
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
@@ -872,6 +934,18 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolveBiomeResolution(x, y, z, null, false);
|
||||
}
|
||||
|
||||
private Holder<Biome> resolvePhysicalBiomeHolder(String physicalKey) {
|
||||
Holder<Biome> holder = resolveBiomeHolder(biomeCustomRegistry, physicalKey);
|
||||
if (holder == null) {
|
||||
holder = resolveBiomeHolder(biomeRegistry, physicalKey);
|
||||
}
|
||||
if (holder == null) {
|
||||
throw new IllegalStateException("Historical Iris biome '" + physicalKey
|
||||
+ "' is not registered in the active world registry.");
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(
|
||||
int x,
|
||||
int y,
|
||||
@@ -1008,17 +1082,19 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(
|
||||
Registry<Biome> customRegistry,
|
||||
Engine engine,
|
||||
IrisDimension dimension,
|
||||
String customBiomeId
|
||||
) {
|
||||
if (customRegistry == null || dimension == null
|
||||
if (customRegistry == null || engine == null || dimension == null
|
||||
|| customBiomeId == null || customBiomeId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Identifier resourceLocation = Identifier.tryParse(customBiomeKey(dimension, customBiomeId));
|
||||
String physicalKey = engine.getData().customBiomeResourceKey(dimension, customBiomeId);
|
||||
Identifier resourceLocation = Identifier.tryParse(physicalKey);
|
||||
if (resourceLocation == null) {
|
||||
return null;
|
||||
throw new IllegalStateException("Invalid Iris custom biome resource key '" + physicalKey + "'.");
|
||||
}
|
||||
Biome biome = customRegistry.getValue(resourceLocation);
|
||||
if (biome == null) {
|
||||
@@ -1170,4 +1246,17 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
private record OwnedBiome(IrisDimension dimension, IrisBiome biome) {
|
||||
}
|
||||
|
||||
private record RuntimeNoiseKey(int runtimeId, long coordinateKey) {
|
||||
}
|
||||
|
||||
private record RuntimeColumnKey(int runtimeId, long coordinateKey) {
|
||||
}
|
||||
|
||||
private record RuntimeBiomeState(
|
||||
IrisDimension dimension,
|
||||
KMap<String, Holder<Biome>> customBiomes,
|
||||
Map<Biome, Holder<Biome>> vanillaSpawnBiomes
|
||||
) {
|
||||
}
|
||||
}
|
||||
+34
-40
@@ -62,9 +62,7 @@ final class ImportedFeatureStage {
|
||||
private static final String CYCLE_MARKER = "Feature order cycle found";
|
||||
|
||||
private final Engine engine;
|
||||
private volatile FeatureTable featureTable;
|
||||
private volatile IrisDimension inertDimension;
|
||||
private volatile int settledRuntimeId;
|
||||
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
|
||||
|
||||
ImportedFeatureStage(Engine engine) {
|
||||
this.engine = engine;
|
||||
@@ -76,7 +74,8 @@ final class ImportedFeatureStage {
|
||||
* does today.
|
||||
*/
|
||||
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
||||
FeatureTable table = featureTable;
|
||||
RuntimeFeatureState state = currentState();
|
||||
FeatureTable table = state == null ? null : state.featureTable();
|
||||
if (table == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -87,50 +86,46 @@ final class ImportedFeatureStage {
|
||||
/**
|
||||
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
|
||||
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
|
||||
*
|
||||
* <p>The volatile fast path is unlocked, so a prepared stage costs two reads per chunk. The build is
|
||||
* synchronized: every worldgen thread decorating a chunk calls this, and two threads that both found the
|
||||
* stage unprepared would each run {@code FeatureSorter}, whose cycle detection is the expensive part.
|
||||
*/
|
||||
void prepare(WorldGenLevel level) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
int runtimeId = engine.getCacheID();
|
||||
if (settled(dimension, runtimeId)) {
|
||||
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (settled(dimension, runtimeId)) {
|
||||
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||
return;
|
||||
}
|
||||
build(level, dimension);
|
||||
settledRuntimeId = runtimeId;
|
||||
runtimeStates.put(runtimeId, build(level, dimension));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean settled(IrisDimension dimension, int runtimeId) {
|
||||
if (settledRuntimeId != runtimeId) {
|
||||
return false;
|
||||
}
|
||||
FeatureTable current = featureTable;
|
||||
if (current != null && current.dimension() == dimension) {
|
||||
return true;
|
||||
}
|
||||
return inertDimension == dimension;
|
||||
void evictRuntime(int runtimeId) {
|
||||
runtimeStates.remove(runtimeId);
|
||||
}
|
||||
|
||||
private void build(WorldGenLevel level, IrisDimension dimension) {
|
||||
private RuntimeFeatureState currentState() {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
|
||||
return settled(state, dimension) ? state : null;
|
||||
}
|
||||
|
||||
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
|
||||
return state != null && state.dimension() == dimension;
|
||||
}
|
||||
|
||||
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
|
||||
IrisImportedFeatureControl control;
|
||||
try {
|
||||
control = NativeFeatureGenerationPolicy.control(engine);
|
||||
} catch (RuntimeException error) {
|
||||
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
|
||||
+ error);
|
||||
markInert(dimension);
|
||||
return;
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
if (!control.shouldGenerateFeatures()) {
|
||||
markInert(dimension);
|
||||
return;
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
FeatureTable built;
|
||||
try {
|
||||
@@ -138,23 +133,15 @@ final class ImportedFeatureStage {
|
||||
} catch (Throwable error) {
|
||||
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
|
||||
+ ": feature table construction failed: " + error);
|
||||
markInert(dimension);
|
||||
return;
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
if (built == null) {
|
||||
markInert(dimension);
|
||||
return;
|
||||
return new RuntimeFeatureState(dimension, null);
|
||||
}
|
||||
featureTable = built;
|
||||
inertDimension = null;
|
||||
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
|
||||
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
|
||||
+ " custom-biome derivative maps");
|
||||
}
|
||||
|
||||
private void markInert(IrisDimension dimension) {
|
||||
featureTable = null;
|
||||
inertDimension = dimension;
|
||||
return new RuntimeFeatureState(dimension, built);
|
||||
}
|
||||
|
||||
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
|
||||
@@ -217,7 +204,7 @@ final class ImportedFeatureStage {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
addKey(keys, engine.getDimension().getCustomBiomeKey(customBiome.getId()));
|
||||
addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
@@ -249,7 +236,10 @@ final class ImportedFeatureStage {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
derivatives.put(engine.getDimension().getCustomBiomeKey(customBiome.getId()), derivative);
|
||||
derivatives.put(
|
||||
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
|
||||
derivative
|
||||
);
|
||||
}
|
||||
}
|
||||
return derivatives;
|
||||
@@ -280,7 +270,8 @@ final class ImportedFeatureStage {
|
||||
* control is disabled or the table degraded.
|
||||
*/
|
||||
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
|
||||
FeatureTable table = featureTable;
|
||||
RuntimeFeatureState state = currentState();
|
||||
FeatureTable table = state == null ? null : state.featureTable();
|
||||
if (table == null) {
|
||||
return;
|
||||
}
|
||||
@@ -467,4 +458,7 @@ final class ImportedFeatureStage {
|
||||
List<FeatureSorter.StepFeatureData> steps,
|
||||
Map<String, Holder<Biome>> derivatives, boolean filtered) {
|
||||
}
|
||||
|
||||
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
|
||||
}
|
||||
}
|
||||
+307
-62
@@ -7,9 +7,11 @@ import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||
@@ -101,6 +103,7 @@ import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
@@ -133,8 +136,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private final int runtimeHeight;
|
||||
private final int runtimeSeaLevel;
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile IrisDimension spawnTableDimension;
|
||||
private volatile int spawnTableRuntimeId;
|
||||
private final ImportedFeatureStage importedFeatures;
|
||||
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
|
||||
private volatile ReachableStructureCache reachableStructureCache;
|
||||
@@ -158,9 +159,25 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
this.runtimeMinY = level.getMinY();
|
||||
this.runtimeHeight = level.getHeight();
|
||||
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
|
||||
if (engine instanceof IrisEngine irisEngine) {
|
||||
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
|
||||
}
|
||||
installNativeStructureVolumeIndex(level);
|
||||
}
|
||||
|
||||
private void evictRuntimeCaches(int runtimeId) {
|
||||
importedFeatures.evictRuntime(runtimeId);
|
||||
mergedSpawnTables.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||
ReachableStructureCache reachable = reachableStructureCache;
|
||||
if (reachable != null && reachable.runtimeId() == runtimeId) {
|
||||
reachableStructureCache = null;
|
||||
}
|
||||
StructureStepCache steps = structureStepCache;
|
||||
if (steps != null && steps.runtimeId() == runtimeId) {
|
||||
structureStepCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void installNativeStructureVolumeIndex(ServerLevel level) {
|
||||
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
|
||||
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
|
||||
@@ -186,7 +203,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) {
|
||||
return null;
|
||||
}
|
||||
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
SectionPos.blockToSectionCoord(pos.getX()),
|
||||
SectionPos.blockToSectionCoord(pos.getZ()),
|
||||
"bukkit_nms_structure_locate");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
||||
NativeStructureVanillaLocator.Candidate nativeCandidate =
|
||||
@@ -365,9 +387,16 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|| runtimeLevel.getChunkSource().getGeneratorState() != structureState) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = access.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_structures");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!allowsRoutedDiscreteGeneration(access, ChunkStatus.STRUCTURE_STARTS)) {
|
||||
return;
|
||||
}
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
|
||||
@@ -399,15 +428,28 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
BoundingBox footprint = start.getBoundingBox();
|
||||
if (!engine.getComplex().allowsNewGenerationFootprint(
|
||||
footprint.minX(),
|
||||
footprint.minZ(),
|
||||
footprint.maxX(),
|
||||
footprint.maxZ())) {
|
||||
access.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
NativeStructureOwnershipStore.discard(
|
||||
engine, structureId, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
continue;
|
||||
}
|
||||
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
||||
IrisNativeStructureDecision decision;
|
||||
try {
|
||||
@@ -634,7 +676,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (runtimeLevel.getChunkSource().getGenerator() != this) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_references");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
NativeStructureReferenceRepair.createReferences(
|
||||
@@ -644,7 +690,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_biomes");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
customBiomeSource.prepareVisibleBiomeBatch();
|
||||
@@ -659,31 +709,63 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
|
||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_build_surface");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_build_surface");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_apply_carvers");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_apply_carvers");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(
|
||||
ichunkaccess.getPos(),
|
||||
chunkPos,
|
||||
"bukkit_nms_chunk_pipeline");
|
||||
GenerationSessionLease lease;
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route;
|
||||
try {
|
||||
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
|
||||
route = openHistoryRoute(chunkPos.x(), chunkPos.z(), "bukkit_nms_chunk_pipeline");
|
||||
} catch (RuntimeException | Error failure) {
|
||||
stage.close();
|
||||
throw failure;
|
||||
}
|
||||
GenerationSessionLease lease;
|
||||
try {
|
||||
CompletableFuture<ChunkAccess> pipeline = delegate
|
||||
.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)
|
||||
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
|
||||
} catch (RuntimeException | Error failure) {
|
||||
closeHistoryRoute(route, failure);
|
||||
stage.close();
|
||||
throw failure;
|
||||
}
|
||||
try {
|
||||
CompletableFuture<ChunkAccess> delegatePipeline;
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
delegatePipeline = delegate.fillFromNoise(
|
||||
blender, randomstate, structuremanager, ichunkaccess);
|
||||
}
|
||||
CompletableFuture<ChunkAccess> pipeline = delegatePipeline
|
||||
.thenApply(filled -> {
|
||||
try (IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope =
|
||||
openHistoryRuntimeScope(route);
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
primeWorldgenHeightmaps(filled);
|
||||
return filled;
|
||||
}
|
||||
@@ -691,24 +773,78 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
|
||||
pipeline.whenComplete((filled, failure) -> {
|
||||
boolean cancelled = isCancellationFailure(failure);
|
||||
lease.close();
|
||||
stage.close();
|
||||
if (failure == null) {
|
||||
Throwable completionFailure = closeNoisePipelineResources(
|
||||
failure, lease, route, stage);
|
||||
if (completionFailure == null) {
|
||||
completion.complete(filled);
|
||||
} else if (cancelled) {
|
||||
completion.cancel(false);
|
||||
} else {
|
||||
completion.completeExceptionally(failure);
|
||||
completion.completeExceptionally(completionFailure);
|
||||
}
|
||||
});
|
||||
return completion;
|
||||
} catch (RuntimeException | Error failure) {
|
||||
lease.close();
|
||||
closeHistoryRoute(route, failure);
|
||||
stage.close();
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable closeNoisePipelineResources(
|
||||
Throwable failure,
|
||||
GenerationSessionLease lease,
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
BukkitChunkGenerator.GenerationStagePermit stage
|
||||
) {
|
||||
Throwable result = failure;
|
||||
try {
|
||||
lease.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
result = appendFailure(result, closeFailure);
|
||||
}
|
||||
try {
|
||||
if (route != null) {
|
||||
route.close();
|
||||
}
|
||||
} catch (Throwable closeFailure) {
|
||||
result = appendFailure(result, closeFailure);
|
||||
}
|
||||
try {
|
||||
stage.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
result = appendFailure(result, closeFailure);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void closeHistoryRoute(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
Throwable failure
|
||||
) {
|
||||
if (route == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
route.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable appendFailure(Throwable failure, Throwable closeFailure) {
|
||||
if (failure == null) {
|
||||
return closeFailure;
|
||||
}
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
private static boolean isCancellationFailure(Throwable failure) {
|
||||
Throwable current = failure;
|
||||
while (current instanceof CompletionException && current.getCause() != null) {
|
||||
@@ -739,6 +875,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns");
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_mob_spawns");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition);
|
||||
}
|
||||
}
|
||||
|
||||
private WeightedList<MobSpawnSettings.SpawnerData> getMobsAtWithActiveRuntime(
|
||||
Holder<Biome> holder,
|
||||
StructureManager structuremanager,
|
||||
MobCategory enumcreaturetype,
|
||||
BlockPos blockposition
|
||||
) {
|
||||
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
||||
@@ -759,18 +909,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
IrisDimension spawnDimension = engine.getDimension();
|
||||
int spawnRuntimeId = engine.getCacheID();
|
||||
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
|
||||
synchronized (mergedSpawnTables) {
|
||||
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
|
||||
mergedSpawnTables.clear();
|
||||
spawnTableDimension = spawnDimension;
|
||||
spawnTableRuntimeId = spawnRuntimeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
|
||||
SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), enumcreaturetype);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
@@ -786,19 +926,26 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
||||
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
chunkPos.x(), chunkPos.z(), "bukkit_nms_biome_decoration");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
|
||||
// feature-order cycle is reported once and degraded to features-off.
|
||||
importedFeatures.prepare(generatoraccessseed);
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
|
||||
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
|
||||
// dimension set importedFeatures.enabled.
|
||||
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)) {
|
||||
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
|
||||
// feature-order cycle is reported once and degraded to features-off.
|
||||
importedFeatures.prepare(generatoraccessseed);
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
|
||||
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
|
||||
// dimension set importedFeatures.enabled.
|
||||
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
}
|
||||
claimGeneratedSemantics(route, "bukkit_nms_biome_decoration");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,6 +997,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||
for (StructureStart start : starts) {
|
||||
BoundingBox footprint = start.getBoundingBox();
|
||||
if (!engine.getComplex().allowsNewGenerationFootprint(
|
||||
footprint.minX(),
|
||||
footprint.minZ(),
|
||||
footprint.maxX(),
|
||||
footprint.maxZ())) {
|
||||
continue;
|
||||
}
|
||||
NativeStructureOwnershipRecord ownership =
|
||||
NativeStructureOwnershipRecovery.resolve(
|
||||
engine, world.getLevel(), structureId, structure, start);
|
||||
@@ -1001,13 +1156,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
|
||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||
int runtimeId = engine.getCacheID();
|
||||
StructureStepCache cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
synchronized (this) {
|
||||
cached = structureStepCache;
|
||||
if (cached != null && cached.registry() == registry) {
|
||||
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
|
||||
return cached.structures();
|
||||
}
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
@@ -1022,7 +1178,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
grouped.set(step, List.copyOf(grouped.get(step)));
|
||||
}
|
||||
List<List<Structure>> resolved = List.copyOf(grouped);
|
||||
structureStepCache = new StructureStepCache(registry, resolved);
|
||||
structureStepCache = new StructureStepCache(runtimeId, registry, resolved);
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -1054,7 +1210,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
|
||||
ChunkPos chunkPos = chunkAccess.getPos();
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
chunkPos.getMinBlockX(), chunkPos.getMinBlockZ(), "bukkit_nms_heightmaps");
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||
BlockPos blockPos = sectionPos.origin();
|
||||
@@ -1085,22 +1244,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
ChunkPos center = region.getCenter();
|
||||
Holder<Biome> visibleBiome;
|
||||
if (engine.getDimensionStackContext() == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
} else {
|
||||
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
|
||||
center.getMinBlockX() + 8,
|
||||
center.getMinBlockZ() + 8);
|
||||
if (visibleBiome == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_spawn_original_mobs");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
center.x(), center.z(), "bukkit_nms_spawn_original_mobs");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_spawn_original_mobs");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!allowsRoutedDiscreteGeneration(region.getChunk(center.x(), center.z()), ChunkStatus.SPAWN)) {
|
||||
return;
|
||||
}
|
||||
Holder<Biome> visibleBiome;
|
||||
if (engine.getDimensionStackContext() == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
} else {
|
||||
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
|
||||
center.getMinBlockX() + 8,
|
||||
center.getMinBlockZ() + 8);
|
||||
if (visibleBiome == null) {
|
||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
}
|
||||
}
|
||||
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
|
||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||
NaturalSpawner.spawnMobsForChunkGeneration(
|
||||
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
|
||||
}
|
||||
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
|
||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||
NaturalSpawner.spawnMobsForChunkGeneration(
|
||||
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
|
||||
}
|
||||
|
||||
private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) {
|
||||
if (chunk.getPersistedStatus().isOrAfter(stage)) {
|
||||
return false;
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
return engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
|
||||
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
||||
@@ -1133,7 +1310,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||
i, j, "bukkit_nms_base_height");
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||
int height = engine.getDimensionStackContext() == null
|
||||
@@ -1148,7 +1327,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||
i, j, "bukkit_nms_base_column");
|
||||
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
boolean dimensionStack = engine.getDimensionStackContext() != null;
|
||||
int block = dimensionStack
|
||||
@@ -1181,6 +1362,69 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
String operation
|
||||
) {
|
||||
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||
return null;
|
||||
}
|
||||
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||
if (router == null) {
|
||||
if (platformGenerator != null && platformGenerator.getGenerationHistory() != null) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " has no generation-history runtime router.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return router.openRoute(chunkX, chunkZ);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " could not route generation history for chunk "
|
||||
+ chunkX + "," + chunkZ + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||
int blockX,
|
||||
int blockZ,
|
||||
String operation
|
||||
) {
|
||||
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " could not route generation history at "
|
||||
+ blockX + "," + blockZ + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route
|
||||
) {
|
||||
return route == null ? null : route.openRuntimeScope();
|
||||
}
|
||||
|
||||
private static void claimGeneratedSemantics(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
String operation
|
||||
) {
|
||||
if (route == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
route.claimGeneratedSemantics();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation
|
||||
+ " could not durably claim generated chunk semantics.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private BukkitChunkGenerator.GenerationStagePermit requireGenerationStage(String operation) {
|
||||
return platformGenerator == null
|
||||
? BukkitChunkGenerator.GenerationStagePermit.noop()
|
||||
@@ -1252,13 +1496,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
private record SpawnTableKey(int runtimeId, Biome biome, MobCategory category) {
|
||||
}
|
||||
|
||||
private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set<String> keys) {
|
||||
}
|
||||
|
||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
||||
private record StructureStepCache(int runtimeId, Registry<Structure> registry,
|
||||
List<List<Structure>> structures) {
|
||||
}
|
||||
|
||||
record StudioStructureState(
|
||||
|
||||
+113
-9
@@ -33,6 +33,7 @@ import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -57,6 +58,9 @@ import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.dynamic.DynamicType;
|
||||
import net.bytebuddy.utility.JavaModule;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -169,7 +173,6 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -181,11 +184,22 @@ public class NMSBinding implements INMSBinding {
|
||||
private final AtomicBoolean injected = new AtomicBoolean();
|
||||
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
||||
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
|
||||
private boolean pluginClassLoaderCloseDeferred;
|
||||
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
||||
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final AtomicCache<Method> byIdRef = new AtomicCache<>();
|
||||
|
||||
@Override
|
||||
public PlatformGenerationRegistry generationRegistry() {
|
||||
return new NmsGenerationRegistry(
|
||||
this::registry,
|
||||
"bukkit-generation-registry-v1",
|
||||
"bukkit-v26_2_R1-generated-registry-json-v1"
|
||||
);
|
||||
}
|
||||
|
||||
private static Object getFor(Class<?> type, Object source) {
|
||||
Object o = fieldFor(type, source);
|
||||
|
||||
@@ -1544,6 +1558,23 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
try {
|
||||
IrisLogging.info("Injecting Bukkit");
|
||||
Agent.requireClassLoaderCloseDeferral();
|
||||
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
|
||||
.getMethod("close").getDeclaringClass();
|
||||
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
|
||||
pluginClassLoaderTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
|
||||
.with(loaderListener)
|
||||
.type(ElementMatchers.is(loaderCloseType))
|
||||
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
|
||||
.on(ElementMatchers.named("close")
|
||||
.and(ElementMatchers.takesArguments(0))
|
||||
.and(ElementMatchers.returns(void.class)))))
|
||||
.installOn(Agent.getInstrumentation());
|
||||
loaderListener.requireInstalled();
|
||||
levelStorageAccessTransformer = new AgentBuilder.Default()
|
||||
.disableClassFormatChanges()
|
||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||
@@ -1579,18 +1610,22 @@ public class NMSBinding implements INMSBinding {
|
||||
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
||||
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
||||
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
||||
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
|
||||
serverLevelTransformer = null;
|
||||
levelStorageAccessTransformer = null;
|
||||
pluginClassLoaderTransformer = null;
|
||||
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
||||
partialServerLevel,
|
||||
partialStorageAccess
|
||||
partialStorageAccess,
|
||||
partialPluginClassLoader
|
||||
}) {
|
||||
if (partial == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||
} catch (Throwable ignored) {
|
||||
} catch (Throwable cleanupFailure) {
|
||||
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -1637,16 +1672,52 @@ public class NMSBinding implements INMSBinding {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
|
||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer();
|
||||
return ServerShutdownBoundary.await(
|
||||
public boolean isServerStopping() {
|
||||
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
||||
return new ServerShutdownBoundary(
|
||||
() -> server.hasFullyShutdown,
|
||||
server.getRunningThread(),
|
||||
timeout,
|
||||
unit
|
||||
server.getRunningThread()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deferPluginClassLoaderClose() {
|
||||
synchronized (injected) {
|
||||
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
|
||||
return;
|
||||
}
|
||||
Agent.retainClassLoader(getClass().getClassLoader());
|
||||
pluginClassLoaderCloseDeferred = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePluginClassLoaderClose() {
|
||||
ResettableClassFileTransformer transformer;
|
||||
boolean releaseLoader;
|
||||
synchronized (injected) {
|
||||
transformer = pluginClassLoaderTransformer;
|
||||
pluginClassLoaderTransformer = null;
|
||||
releaseLoader = pluginClassLoaderCloseDeferred;
|
||||
pluginClassLoaderCloseDeferred = false;
|
||||
}
|
||||
try {
|
||||
if (transformer != null
|
||||
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
|
||||
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
|
||||
}
|
||||
} finally {
|
||||
if (releaseLoader) {
|
||||
Agent.releaseClassLoader(getClass().getClassLoader());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
||||
KMap<Material, List<BlockProperty>> states = new KMap<>();
|
||||
@@ -1691,6 +1762,39 @@ public class NMSBinding implements INMSBinding {
|
||||
return new FlatLevelSource(settings);
|
||||
}
|
||||
|
||||
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
|
||||
private volatile boolean transformed;
|
||||
private volatile Throwable failure;
|
||||
|
||||
@Override
|
||||
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, DynamicType dynamicType) {
|
||||
transformed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String typeName, ClassLoader loader, JavaModule module,
|
||||
boolean loaded, Throwable throwable) {
|
||||
failure = throwable;
|
||||
}
|
||||
|
||||
private void requireInstalled() {
|
||||
if (!transformed || failure != null) {
|
||||
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PluginClassLoaderCloseAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
|
||||
Class<?> installer = Class.forName(
|
||||
"art.arcane.iris.util.project.agent.Installer", true, ClassLoader.getSystemClassLoader());
|
||||
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
|
||||
.invoke(null, loader));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChunkAccessAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||
|
||||
+182
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -42,10 +42,10 @@ public class CustomBiomeSourceStructureContractTest {
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
|
||||
assertFalse(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
|
||||
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
|
||||
assertTrue(source.contains("vanillaSpawnBiomes.get(biome.value())"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
|
||||
assertTrue(source.contains("runtimeBiomeState().vanillaSpawnBiomes().get(biome.value())"));
|
||||
assertTrue(source.contains("requireGenerationLease(\n \"bukkit_structure_biome\""));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
|
||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
|
||||
assertTrue(source.contains("requireGenerationLease(\n \"bukkit_visible_biome\""));
|
||||
assertTrue(source.contains("if (engine.isClosed())"));
|
||||
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
|
||||
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||
@@ -140,7 +140,7 @@ public class CustomBiomeSourceStructureContractTest {
|
||||
assertTrue(lookupEnd > lookupStart);
|
||||
String lookup = source.substring(lookupStart, lookupEnd);
|
||||
|
||||
assertTrue(lookup.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
|
||||
assertTrue(lookup.contains("requireGenerationLease(\n \"bukkit_visible_biome\""));
|
||||
assertTrue(lookup.contains("prepareVisibleBiomeBatch()"));
|
||||
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
|
||||
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()")
|
||||
|
||||
+164
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
-8
@@ -295,26 +295,29 @@ public class IrisChunkGeneratorFailureContractTest {
|
||||
|
||||
assertBefore(productionFill,
|
||||
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
|
||||
"GenerationHistoryRuntimeRouter.RuntimeRoute route");
|
||||
assertBefore(productionFill,
|
||||
"GenerationHistoryRuntimeRouter.RuntimeRoute route",
|
||||
"GenerationSessionLease lease");
|
||||
assertBefore(productionFill,
|
||||
"requireNoiseGenerationStage(",
|
||||
"\"bukkit_nms_chunk_pipeline\")");
|
||||
assertBefore(productionFill,
|
||||
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")",
|
||||
".fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)");
|
||||
"delegate.fillFromNoise(");
|
||||
assertTrue(productionFill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
|
||||
assertTrue(productionFill.contains("openHistoryRuntimeScope(route)"));
|
||||
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
|
||||
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
|
||||
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "lease.close();");
|
||||
assertBefore(completion, "lease.close();", "stage.close();");
|
||||
assertBefore(completion, "stage.close();", "completion.complete(filled)");
|
||||
assertBefore(completion, "stage.close();", "completion.cancel(false)");
|
||||
assertBefore(completion, "stage.close();", "completion.completeExceptionally(failure)");
|
||||
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "closeNoisePipelineResources(");
|
||||
assertBefore(completion, "closeNoisePipelineResources(", "completion.complete(filled)");
|
||||
assertBefore(completion, "closeNoisePipelineResources(", "completion.cancel(false)");
|
||||
assertBefore(completion, "closeNoisePipelineResources(", "completion.completeExceptionally(completionFailure)");
|
||||
assertTrue(completion.contains("else if (cancelled)"));
|
||||
assertFalse(completion.contains("pipeline.isCancelled()"));
|
||||
assertFalse(completion.contains("finally"));
|
||||
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)"));
|
||||
assertTrue(productionFill.contains("lease.close();\n stage.close();\n throw failure;"));
|
||||
assertTrue(productionFill.contains("lease.close();\n closeHistoryRoute(route, failure);\n stage.close();\n throw failure;"));
|
||||
assertTrue(productionFill.contains("return completion;"));
|
||||
assertFalse(productionFill.contains("return pipeline;"));
|
||||
assertFalse(productionFill.contains("pipeline.cancel("));
|
||||
@@ -422,6 +425,9 @@ public class IrisChunkGeneratorFailureContractTest {
|
||||
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
|
||||
String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
|
||||
String biomes = method(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface");
|
||||
String surface = method(source, "public void buildSurface(", "public void applyCarvers(");
|
||||
String carvers = method(source, "public void applyCarvers(", "public CompletableFuture<ChunkAccess> fillFromNoise");
|
||||
String mobs = method(source, "public void spawnOriginalMobs(", "private boolean allowsRoutedDiscreteGeneration(");
|
||||
String noise = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise", "private static boolean isCancellationFailure");
|
||||
String decoration = method(source,
|
||||
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
|
||||
@@ -430,6 +436,9 @@ public class IrisChunkGeneratorFailureContractTest {
|
||||
assertStageBeforeLease(structures, "bukkit_nms_create_structures");
|
||||
assertStageBeforeLease(references, "bukkit_nms_create_references");
|
||||
assertStageBeforeLease(biomes, "bukkit_nms_create_biomes");
|
||||
assertStageBeforeLease(surface, "bukkit_nms_build_surface");
|
||||
assertStageBeforeLease(carvers, "bukkit_nms_apply_carvers");
|
||||
assertStageBeforeLease(mobs, "bukkit_nms_spawn_original_mobs");
|
||||
assertBefore(noise,
|
||||
"requireNoiseGenerationStage(",
|
||||
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
|
||||
@@ -437,7 +446,7 @@ public class IrisChunkGeneratorFailureContractTest {
|
||||
assertBefore(decoration,
|
||||
"requireGenerationLease(\"bukkit_nms_biome_decoration\")",
|
||||
"importedFeatures.prepare(generatoraccessseed)");
|
||||
assertEquals(4, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
|
||||
assertEquals(7, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
|
||||
assertEquals(2, occurrences(source, "requireNoiseGenerationStage("));
|
||||
assertFalse(source.contains("GenerationSessionManager"));
|
||||
}
|
||||
|
||||
+34
-2
@@ -6,16 +6,48 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class NMSBindingShutdownBoundaryContractTest {
|
||||
@Test
|
||||
public void shutdownClassificationUsesTheServerStopFlagInsteadOfPluginEnablement() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
|
||||
String stopping = section(source, "public boolean isServerStopping()", "public ServerShutdownBoundary createServerShutdownBoundary");
|
||||
|
||||
assertTrue(stopping.contains("((CraftServer) Bukkit.getServer()).getServer().hasStopped()"));
|
||||
assertFalse(stopping.contains("isEnabled()"));
|
||||
assertFalse(stopping.contains("isRunning()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pluginLoaderGuardUsesSystemAgentIdentityAndIsRemovedAtRelease() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
|
||||
String injection = section(source, "public boolean injectBukkit()", "public void ensureServerLevelInjection()");
|
||||
String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class ChunkAccessAdvice");
|
||||
String release = section(source, "public void releasePluginClassLoaderClose()", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
||||
|
||||
assertTrue(injection.contains("Agent.requireClassLoaderCloseDeferral()"));
|
||||
assertTrue(injection.contains("getMethod(\"close\").getDeclaringClass()"));
|
||||
assertTrue(injection.contains("Advice.to(PluginClassLoaderCloseAdvice.class)"));
|
||||
assertTrue(guard.contains("skipOn = Advice.OnNonDefaultValue.class"));
|
||||
assertTrue(guard.contains("ClassLoader.getSystemClassLoader()"));
|
||||
assertTrue(guard.contains("getMethod(\"deferClassLoaderClose\", ClassLoader.class)"));
|
||||
assertTrue(guard.contains(".invoke(null, loader)"));
|
||||
assertFalse(guard.contains("Bukkit.getPluginManager()"));
|
||||
assertTrue(release.indexOf("transformer.reset(") > release.indexOf("pluginClassLoaderCloseDeferred = false"));
|
||||
assertTrue(release.indexOf("Agent.releaseClassLoader(") > release.indexOf("transformer.reset("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||
String boundary = section(source, "public boolean awaitServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
||||
String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
||||
|
||||
assertTrue(boundary.contains("ServerShutdownBoundary.await("));
|
||||
assertTrue(boundary.contains("new ServerShutdownBoundary("));
|
||||
assertTrue(boundary.contains("server.hasFullyShutdown"));
|
||||
assertTrue(boundary.contains("server.getRunningThread()"));
|
||||
assertTrue(boundary.contains("((CraftServer) Bukkit.getServer()).getServer()"));
|
||||
assertFalse(boundary.contains("getHandle()"));
|
||||
}
|
||||
|
||||
private static String section(String source, String startMarker, String endMarker) {
|
||||
|
||||
+58
@@ -19,6 +19,7 @@ import org.junit.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -80,6 +81,30 @@ public class NativeStructureVolumeIndexTest {
|
||||
assertEquals(coldResolutions, resolver.resolutions());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
|
||||
AtomicInteger runtimeId = new AtomicInteger(1);
|
||||
Engine engine = runtimeEngine(runtimeId);
|
||||
CountingResolver resolver = new CountingResolver(0, 0);
|
||||
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(289, resolver.resolutions());
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(289, resolver.resolutions());
|
||||
|
||||
runtimeId.set(2);
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(578, resolver.resolutions());
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(578, resolver.resolutions());
|
||||
|
||||
index.evictRuntime(1);
|
||||
runtimeId.set(1);
|
||||
index.resolve(engine, 0, 0, 15, 15);
|
||||
assertEquals(867, resolver.resolutions());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
|
||||
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||
@@ -188,12 +213,45 @@ public class NativeStructureVolumeIndexTest {
|
||||
assertTrue(source.contains("isStructureChunk("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void originResolutionRoutesAndCachesByScopedRuntime() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
|
||||
|
||||
assertTrue(source.contains("openHistoryCoordinateScope(engine, chunkX, chunkZ)"));
|
||||
assertTrue(source.contains("new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ))"));
|
||||
assertTrue(source.contains("addGenerationRuntimeRetirementListener(retirementListener)"));
|
||||
assertTrue(source.contains("originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
|
||||
assertTrue(source.contains("queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
|
||||
}
|
||||
|
||||
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
||||
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
|
||||
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
|
||||
}
|
||||
|
||||
private static Engine runtimeEngine(AtomicInteger runtimeId) {
|
||||
return (Engine) Proxy.newProxyInstance(
|
||||
Engine.class.getClassLoader(),
|
||||
new Class<?>[]{Engine.class},
|
||||
(proxy, method, arguments) -> {
|
||||
if (method.getName().equals("getCacheID")) {
|
||||
return runtimeId.get();
|
||||
}
|
||||
if (method.getName().equals("hashCode")) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (method.getName().equals("equals")) {
|
||||
return proxy == arguments[0];
|
||||
}
|
||||
if (method.getName().equals("toString")) {
|
||||
return "runtime-engine-" + runtimeId.get();
|
||||
}
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static int findDisjointWindow(int chunkX, int chunkZ) {
|
||||
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
|
||||
for (int candidate = 64; candidate < 16_384; candidate += 17) {
|
||||
|
||||
@@ -55,6 +55,7 @@ import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
||||
import art.arcane.iris.core.link.MultiverseCoreLink;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.nms.ServerShutdownBoundary;
|
||||
import art.arcane.iris.core.gui.BukkitGuiHost;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.service.BoardSVC;
|
||||
@@ -139,6 +140,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
@@ -160,6 +163,7 @@ import java.util.regex.Pattern;
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L;
|
||||
private static final PrintStream SHUTDOWN_ERRORS = new PrintStream(new FileOutputStream(FileDescriptor.err), true);
|
||||
private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
|
||||
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
|
||||
|
||||
@@ -190,6 +194,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||
private volatile boolean generatorDrainCompleted;
|
||||
private volatile PlaceholderRegistration papiRegistration;
|
||||
private volatile IrisPapiListener papiListener;
|
||||
private volatile IrisPapiState papiState;
|
||||
@@ -206,6 +212,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
private BukkitDebugDump debugDump;
|
||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||
private volatile Thread serverLifecycleThread;
|
||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||
|
||||
public static VolmitSender getSender() {
|
||||
if (sender == null) {
|
||||
@@ -427,6 +434,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
if (e == null) {
|
||||
return;
|
||||
}
|
||||
if (instance != null && instance.serverStopTeardownDeferred.get()) {
|
||||
e.printStackTrace(SHUTDOWN_ERRORS);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean debug = false;
|
||||
if (instance != null) {
|
||||
@@ -466,6 +477,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
public static void reportError(String context, Throwable e) {
|
||||
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
|
||||
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
|
||||
if (instance != null && instance.serverStopTeardownDeferred.get()) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] " + message);
|
||||
error.printStackTrace(SHUTDOWN_ERRORS);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (instance != null) {
|
||||
@@ -603,6 +619,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
sharedRuntimeClosed.set(false);
|
||||
startupBoundaryRestart.set(false);
|
||||
terminalCleanupCompleted.set(false);
|
||||
runtimeTeardownFailed.set(false);
|
||||
generatorDrainCompleted = false;
|
||||
deferredShutdownGenerators.clear();
|
||||
MultiBurst.burst.reopen();
|
||||
MultiBurst.ioBurst.reopen();
|
||||
@@ -742,6 +760,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
public void addShutdownHook() {
|
||||
removeShutdownHook();
|
||||
serverLifecycleThread = Thread.currentThread();
|
||||
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
|
||||
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
||||
try {
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||
@@ -963,6 +982,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
drainWorldGenerators(reason, timeoutSeconds);
|
||||
}
|
||||
|
||||
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (services != null && servicesDisabled.compareAndSet(false, true)) {
|
||||
// Only services whose onEnable actually completed; disabling a service that
|
||||
// never initialized runs teardown against uninitialized state.
|
||||
@@ -970,6 +993,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
try {
|
||||
service.onDisable();
|
||||
} catch (Throwable e) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
|
||||
}
|
||||
}
|
||||
@@ -979,8 +1003,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return;
|
||||
}
|
||||
|
||||
J.attempt(MultiBurst.burst::close);
|
||||
J.attempt(MultiBurst.ioBurst::close);
|
||||
try {
|
||||
MultiBurst.burst.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris generation workers.", failure);
|
||||
}
|
||||
try {
|
||||
MultiBurst.ioBurst.close();
|
||||
} catch (Throwable failure) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
||||
}
|
||||
clearQueues();
|
||||
IrisServices.clear();
|
||||
}
|
||||
@@ -988,6 +1022,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
|
||||
private void quiesceRuntimeForServerShutdown(String reason) {
|
||||
serverStopTeardownDeferred.set(true);
|
||||
INMS.get().deferPluginClassLoaderClose();
|
||||
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
||||
if (jigsawStudioService != null) {
|
||||
try {
|
||||
@@ -1060,29 +1095,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
return;
|
||||
}
|
||||
if (!awaitServerShutdownBoundary()) {
|
||||
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
|
||||
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
|
||||
return;
|
||||
}
|
||||
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
|
||||
}
|
||||
|
||||
private boolean awaitServerShutdownBoundary() {
|
||||
if (!INMS.isBound()) {
|
||||
ServerShutdownBoundary boundary = serverShutdownBoundary;
|
||||
if (boundary == null) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return INMS.get().awaitServerShutdownBoundary(
|
||||
return boundary.await(
|
||||
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError("Failed to await Paper's post-world-close shutdown boundary.", e);
|
||||
SHUTDOWN_ERRORS.println("[Iris] Failed to await Paper's post-world-close shutdown boundary.");
|
||||
e.printStackTrace(SHUTDOWN_ERRORS);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
|
||||
teardownRuntime(reason, timeoutSeconds);
|
||||
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup is incomplete; retaining services, pools and the plugin class loader until JVM exit.");
|
||||
return;
|
||||
}
|
||||
finishTerminalCleanup();
|
||||
}
|
||||
|
||||
@@ -1094,12 +1135,36 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
try {
|
||||
runPostShutdown();
|
||||
} catch (Throwable e) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
||||
} finally {
|
||||
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
|
||||
runtimeTeardownFailed.set(true);
|
||||
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
|
||||
}
|
||||
IrisPlatforms.unbind();
|
||||
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
|
||||
} else {
|
||||
try {
|
||||
INMS.get().releasePluginClassLoaderClose();
|
||||
} catch (Throwable failure) {
|
||||
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
||||
failure.printStackTrace(SHUTDOWN_ERRORS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasActiveShutdownResources() {
|
||||
if (!MultiBurst.burst.isTerminated() || !MultiBurst.ioBurst.isTerminated()) {
|
||||
return true;
|
||||
}
|
||||
return services != null
|
||||
&& services.get(PreservationSVC.class) instanceof PreservationSVC preservation
|
||||
&& preservation.hasActiveResources();
|
||||
}
|
||||
|
||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||
List<World> irisWorlds = new ArrayList<>();
|
||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||
@@ -1115,6 +1180,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
}
|
||||
}
|
||||
if (generators.isEmpty()) {
|
||||
generatorDrainCompleted = true;
|
||||
Iris.debug("No Iris worlds to freeze.");
|
||||
return;
|
||||
}
|
||||
@@ -1139,12 +1205,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||
try {
|
||||
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
|
||||
.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||
Iris.debug("All Iris chunk generators parked. Safe to unload.");
|
||||
generatorDrainCompleted = closes.size() == generators.size();
|
||||
if (generatorDrainCompleted) {
|
||||
Iris.debug("All Iris chunk generators parked. Safe to unload.");
|
||||
}
|
||||
} catch (TimeoutException e) {
|
||||
Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway.");
|
||||
Iris.reportError("Iris generator drain timed out after " + timeoutSeconds + "s.", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
Iris.warn("Iris generator drain interrupted; unload proceeding.");
|
||||
Iris.reportError("Iris generator drain was interrupted.", e);
|
||||
} catch (ExecutionException e) {
|
||||
Iris.reportError(e.getCause() == null ? e : e.getCause());
|
||||
}
|
||||
|
||||
+3
-3
@@ -765,13 +765,13 @@ public final class BukkitWorldReconciler {
|
||||
|
||||
@Override
|
||||
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
|
||||
File snapshotRoot = snapshotRoot(worldKey);
|
||||
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
|
||||
String configuredWorldName = configuredWorldName(worldKey);
|
||||
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
|
||||
if (irisDimension == null) {
|
||||
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
|
||||
}
|
||||
File snapshotRoot = snapshotRoot(worldKey);
|
||||
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
|
||||
}
|
||||
|
||||
private File snapshotRoot(NamespacedKey worldKey) {
|
||||
@@ -788,7 +788,7 @@ public final class BukkitWorldReconciler {
|
||||
throw new IllegalStateException("Iris world storage does not match the current platform layout for "
|
||||
+ worldKey + ".");
|
||||
}
|
||||
return IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
|
||||
return IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
-15
@@ -20,6 +20,7 @@ package art.arcane.iris.core;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
|
||||
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.BrokenPackException;
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
@@ -32,6 +33,13 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||
import art.arcane.iris.engine.history.GenerationHistory;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryPaths;
|
||||
import art.arcane.iris.engine.history.GenerationPackFingerprint;
|
||||
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
@@ -255,15 +263,21 @@ public final class IrisWorldGeneratorResolver {
|
||||
File levelRoot = IrisWorldStorage.levelRoot();
|
||||
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
|
||||
String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName());
|
||||
File pack = IrisWorldStorage.frozenDimensionRoot(
|
||||
File dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
|
||||
Bukkit.getWorldContainer(),
|
||||
levelRoot,
|
||||
configuredWorldName,
|
||||
worldKey
|
||||
)
|
||||
.map(IrisWorldStorage::requireFrozenPackRoot)
|
||||
.orElse(null);
|
||||
IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id);
|
||||
IrisDimension dimension = null;
|
||||
if (dimensionRoot != null) {
|
||||
long worldSeed = requireStoredWorldSeed(configuredWorldName, dimensionRoot);
|
||||
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
|
||||
File pack = requireActivePack(history);
|
||||
requireSnapshotLoadable(pack);
|
||||
dimension = requireHistoricalDimension(history, pack, id);
|
||||
}
|
||||
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||
if (dimension == null) {
|
||||
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
|
||||
@@ -445,7 +459,7 @@ public final class IrisWorldGeneratorResolver {
|
||||
return;
|
||||
}
|
||||
if (dimensionRoot != null
|
||||
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasFrozenPack(dimensionRoot))) {
|
||||
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasGenerationStorage(dimensionRoot))) {
|
||||
return;
|
||||
}
|
||||
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
|
||||
@@ -455,9 +469,10 @@ public final class IrisWorldGeneratorResolver {
|
||||
+ " type=<pack>; Iris registers them with Multiverse itself.");
|
||||
}
|
||||
|
||||
private static boolean hasFrozenPack(File dimensionRoot) {
|
||||
Path packRoot = dimensionRoot.toPath().toAbsolutePath().normalize().resolve("iris").resolve("pack");
|
||||
return Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS);
|
||||
private static boolean hasGenerationStorage(File dimensionRoot) {
|
||||
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot.toPath());
|
||||
return Files.isDirectory(paths.generationRoot(), LinkOption.NOFOLLOW_LINKS)
|
||||
|| Files.isDirectory(paths.legacyPackRoot(), LinkOption.NOFOLLOW_LINKS);
|
||||
}
|
||||
|
||||
private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) {
|
||||
@@ -475,7 +490,9 @@ public final class IrisWorldGeneratorResolver {
|
||||
throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for "
|
||||
+ worldKey + ".");
|
||||
}
|
||||
File snapshotRoot = IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
|
||||
long worldSeed = requireStoredWorldSeed(worldName, dimensionRoot);
|
||||
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
|
||||
File snapshotRoot = requireActivePack(history);
|
||||
try {
|
||||
requireSnapshotLoadable(snapshotRoot);
|
||||
} catch (BrokenPackException exception) {
|
||||
@@ -487,18 +504,14 @@ public final class IrisWorldGeneratorResolver {
|
||||
throw exception;
|
||||
}
|
||||
|
||||
IrisDimension dimension = IrisData.get(snapshotRoot).getDimensionLoader().load(id, false);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Frozen Iris pack snapshot at " + snapshotRoot
|
||||
+ " does not contain dimension " + id + ".");
|
||||
}
|
||||
IrisDimension dimension = requireHistoricalDimension(history, snapshotRoot, id);
|
||||
|
||||
Iris.debug("Assuming IrisDimension: " + dimension.getName());
|
||||
|
||||
IrisWorld world = IrisWorld.builder()
|
||||
.platformIdentity(worldKey.toString())
|
||||
.name(worldName)
|
||||
.seed(1337)
|
||||
.seed(history.activeEpoch().worldSeed())
|
||||
.worldFolder(dimensionRoot)
|
||||
.minHeight(dimension.getMinHeight())
|
||||
.maxHeight(dimension.getMaxHeight())
|
||||
@@ -506,7 +519,133 @@ public final class IrisWorldGeneratorResolver {
|
||||
|
||||
Iris.debug("Generator Config: " + world);
|
||||
|
||||
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey());
|
||||
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey(), history);
|
||||
}
|
||||
|
||||
private static GenerationHistory requireGenerationHistory(
|
||||
File dimensionRoot,
|
||||
String dimensionKey,
|
||||
long worldSeed
|
||||
) {
|
||||
Path root = dimensionRoot.toPath().toAbsolutePath().normalize();
|
||||
try {
|
||||
Optional<GenerationHistory> current = GenerationHistory.openIfPresent(root, worldSeed);
|
||||
GenerationHistory history = current.isPresent()
|
||||
? current.get()
|
||||
: adoptLegacyGenerationHistory(root, dimensionKey, worldSeed);
|
||||
List<GenerationRegistryContract> retainedContracts = history.manifest()
|
||||
.epochs()
|
||||
.stream()
|
||||
.map(GenerationEpoch::registryContract)
|
||||
.toList();
|
||||
history.requireRegistryDefinitions(
|
||||
GenerationRegistryContractFactory.captureRequiredDefinitions(retainedContracts)
|
||||
);
|
||||
return history;
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static GenerationHistory adoptLegacyGenerationHistory(
|
||||
Path dimensionRoot,
|
||||
String dimensionKey,
|
||||
long worldSeed
|
||||
) throws IOException {
|
||||
Path legacyPack = GenerationHistoryPaths.forDimension(dimensionRoot).legacyPackRoot();
|
||||
requireSnapshotLoadable(legacyPack.toFile());
|
||||
String fingerprint = GenerationPackFingerprint.compute(
|
||||
legacyPack,
|
||||
GenerationPackFingerprint.CURRENT_VERSION
|
||||
);
|
||||
IrisData data = IrisData.openDatapackCompiler(legacyPack.toFile());
|
||||
try {
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||
if (dimension == null) {
|
||||
throw new IOException("Legacy Iris pack does not contain dimension " + dimensionKey + ".");
|
||||
}
|
||||
GenerationEpoch.DimensionContract dimensionContract = captureDimensionContract(data, dimension);
|
||||
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
|
||||
data,
|
||||
dimension,
|
||||
fingerprint,
|
||||
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
|
||||
);
|
||||
return GenerationHistory.adoptLegacyPack(
|
||||
dimensionRoot,
|
||||
fingerprint,
|
||||
worldSeed,
|
||||
dimensionContract,
|
||||
registryContract
|
||||
);
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisDimension requireHistoricalDimension(
|
||||
GenerationHistory history,
|
||||
File packRoot,
|
||||
String dimensionKey
|
||||
) {
|
||||
GenerationEpoch epoch = history.activeEpoch();
|
||||
if (!epoch.dimensionContract().dimensionKey().equals(dimensionKey)) {
|
||||
throw new IllegalStateException("Configured dimension " + dimensionKey
|
||||
+ " does not match active generation history dimension "
|
||||
+ epoch.dimensionContract().dimensionKey() + ".");
|
||||
}
|
||||
IrisData data = IrisData.get(packRoot);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Immutable Iris generation pack at " + packRoot
|
||||
+ " does not contain dimension " + dimensionKey + ".");
|
||||
}
|
||||
GenerationEpoch.DimensionContract actualContract = captureDimensionContract(data, dimension);
|
||||
if (!epoch.dimensionContract().equals(actualContract)) {
|
||||
throw new IllegalStateException("Immutable Iris generation pack dimension contract changed at "
|
||||
+ packRoot + ".");
|
||||
}
|
||||
return dimension;
|
||||
}
|
||||
|
||||
private static GenerationEpoch.DimensionContract captureDimensionContract(
|
||||
IrisData data,
|
||||
IrisDimension dimension
|
||||
) {
|
||||
String dimensionTypeKey = IrisPlatforms.get()
|
||||
.registries()
|
||||
.generationRegistry()
|
||||
.dimensionTypeResourceKey(
|
||||
data.getDataFolder().getName(),
|
||||
dimension.getLoadKey(),
|
||||
dimension.getDimensionTypeKey()
|
||||
);
|
||||
return GenerationEpochContractFactory.create(
|
||||
dimension,
|
||||
dimension.getLoadKey(),
|
||||
dimensionTypeKey
|
||||
);
|
||||
}
|
||||
|
||||
private static File requireActivePack(GenerationHistory history) {
|
||||
try {
|
||||
return history.activePackRoot().toFile();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Active Iris generation pack is unusable.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static long requireStoredWorldSeed(String worldName, File dimensionRoot) {
|
||||
Long configuredSeed = IrisWorlds.readBukkitWorldSeed(worldName);
|
||||
if (configuredSeed != null) {
|
||||
return configuredSeed;
|
||||
}
|
||||
try {
|
||||
return WorldReplacementSeed.readAuthoritativeSeed(dimensionRoot.toPath());
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris world " + worldName
|
||||
+ " has no authoritative generation seed.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private record FreshValidation(
|
||||
|
||||
+18
-11
@@ -164,24 +164,28 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
boolean configurationApplied = false;
|
||||
try {
|
||||
Files.createDirectory(paths.stage());
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
|
||||
requiredSender,
|
||||
requiredDimension,
|
||||
paths.stage().toFile()
|
||||
);
|
||||
if (installed == null) {
|
||||
throw new IOException("Iris could not stage the dimension pack.");
|
||||
}
|
||||
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
|
||||
long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
|
||||
paths.target(),
|
||||
paths.stage(),
|
||||
seedSelection
|
||||
);
|
||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
|
||||
requiredSender,
|
||||
requiredDimension,
|
||||
paths.stage().toFile(),
|
||||
effectiveSeed
|
||||
);
|
||||
if (installed == null) {
|
||||
throw new IOException("Iris could not stage the dimension pack.");
|
||||
}
|
||||
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
|
||||
if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
|
||||
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId);
|
||||
}
|
||||
File stagedPack = paths.stage().resolve("iris/pack").toFile();
|
||||
File stagedPack = IrisWorldStorage.requireActiveGenerationPackRoot(
|
||||
paths.stage().toFile(),
|
||||
effectiveSeed
|
||||
);
|
||||
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
|
||||
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
|
||||
transaction = new Transaction(
|
||||
@@ -806,7 +810,10 @@ public final class PendingWorldReplacementManager implements Listener {
|
||||
validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind());
|
||||
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
|
||||
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
|
||||
paths.target().resolve("iris/pack"));
|
||||
IrisWorldStorage.requireActiveGenerationPackRoot(
|
||||
paths.target().toFile(),
|
||||
transaction.seed()
|
||||
).toPath());
|
||||
if (!transaction.packFingerprint().equals(fingerprint)) {
|
||||
throw new IOException("The replacement pack changed before runtime verification.");
|
||||
}
|
||||
|
||||
+2
-2
@@ -176,7 +176,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
return z ^ (z >>> 31);
|
||||
}
|
||||
|
||||
@Director(description = "Update the pack of a world (UNSAFE!)", descriptionKey = "iris.director.commanddeveloper.director.update_pack_world_unsafe", name = "update-world", aliases = "^world")
|
||||
@Director(description = "Stage a world generation update", descriptionKey = "iris.director.commanddeveloper.director.stage_world_generation_update", name = "update-world", aliases = "^world")
|
||||
public void updateWorld(
|
||||
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
|
||||
World world,
|
||||
@@ -202,7 +202,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
||||
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
|
||||
pack.getLoadKey()
|
||||
)) {
|
||||
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder);
|
||||
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
|
||||
} catch (LifecycleOperationCoordinator.BusyException e) {
|
||||
sender().sendMessage(C.YELLOW + e.getMessage());
|
||||
}
|
||||
|
||||
+26
-19
@@ -28,8 +28,8 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.history.GenerationSemanticQueries;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
@@ -41,8 +41,10 @@ import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.HydrologyTypeHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.engine.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.LocatableObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.ReachableRegionHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
@@ -90,7 +92,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
|
||||
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
||||
public void region(
|
||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look")
|
||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
||||
IrisRegion region,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -138,10 +140,6 @@ public class CommandFind implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
commandSender.sendMessage(C.YELLOW + "Hydrology is not active in this world.");
|
||||
return;
|
||||
}
|
||||
HydrologyFeatureQuery query;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
@@ -150,15 +148,19 @@ public class CommandFind implements DirectorExecutor {
|
||||
return;
|
||||
}
|
||||
Location origin = target.getLocation();
|
||||
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime.maximumFeatureSearchDistance(
|
||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||
int requestedDistance = runtime == null
|
||||
? 8192
|
||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime == null
|
||||
? requestedDistance
|
||||
: runtime.maximumFeatureSearchDistance(
|
||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
||||
J.a(() -> {
|
||||
try {
|
||||
HydrologyFeatureRef feature = runtime.nearestFeature(
|
||||
query.types(),
|
||||
query.profileKey(),
|
||||
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
||||
activeEngine,
|
||||
query,
|
||||
origin.getBlockX(),
|
||||
origin.getBlockZ(),
|
||||
maximumDistance,
|
||||
@@ -171,15 +173,14 @@ public class CommandFind implements DirectorExecutor {
|
||||
+ maximumDistance + " blocks.");
|
||||
return;
|
||||
}
|
||||
int worldY = feature.y() + activeEngine.getDimension().getMinHeight();
|
||||
String label = type + " hydrology feature";
|
||||
if (!teleport) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + worldY + ", " + feature.z() + ".");
|
||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
||||
return;
|
||||
}
|
||||
Location destination = new Location(
|
||||
target.getWorld(), feature.x(), worldY, feature.z());
|
||||
target.getWorld(), feature.x(), feature.y(), feature.z());
|
||||
prepareStructureTeleport(
|
||||
target, target.getWorld(), commandSender, label, destination, true);
|
||||
} catch (Throwable error) {
|
||||
@@ -233,6 +234,12 @@ public class CommandFind implements DirectorExecutor {
|
||||
registered, decision, nativePlacement, locatableNativePlacement,
|
||||
locatableEditablePlacement, nativeGenerationEnabled, reachable);
|
||||
|
||||
if (route != StructureLookupRoute.NATIVE && route != StructureLookupRoute.IRIS
|
||||
&& GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)) {
|
||||
locateIrisStructure(e, structureKey, commandSender);
|
||||
return;
|
||||
}
|
||||
|
||||
if (route == StructureLookupRoute.UNKNOWN) {
|
||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
|
||||
return;
|
||||
@@ -672,7 +679,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
J.a(() -> {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
|
||||
GenerationSemanticQueries.nearestStructure(engine, structure, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
sendStructureMessage(target, commandSender,
|
||||
C.YELLOW + "Unable to locate " + structure
|
||||
@@ -697,7 +704,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
|
||||
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
|
||||
public void object(
|
||||
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = ObjectHandler.class)
|
||||
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = LocatableObjectHandler.class)
|
||||
String object,
|
||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
|
||||
boolean teleport
|
||||
@@ -721,7 +728,7 @@ public class CommandFind implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
if (e.hasObjectPlacement(object)) {
|
||||
if (GenerationFindCatalog.hasObjectPlacement(e, object)) {
|
||||
EngineBukkitOps.gotoObject(e, object, player(), teleport);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -279,12 +279,14 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
||||
|
||||
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
|
||||
dispatchSenders.set(sender);
|
||||
DirectorContext.touch(new VolmitSender(sender));
|
||||
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
||||
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||
return DirectorExecutionResult.notHandled();
|
||||
} finally {
|
||||
DirectorContext.remove();
|
||||
dispatchSenders.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,40 @@ import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisShutdownOrderingTest {
|
||||
@Test
|
||||
public void serverStopHoldsThePluginLoaderUntilTerminalCleanupCompletes() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
String quiesce = section(source, "private void quiesceRuntimeForServerShutdown", "private void startPostStopFinisher");
|
||||
String cleanup = section(source, "private void finishTerminalCleanup()", "private void drainWorldGenerators");
|
||||
String teardown = section(source, "private void teardownRuntime", "private void quiesceRuntimeForServerShutdown");
|
||||
String deferred = section(source, "private void finishDeferredRuntimeTeardown", "private void finishTerminalCleanup");
|
||||
|
||||
assertOrdered(quiesce, "serverStopTeardownDeferred.set(true)", "deferPluginClassLoaderClose()",
|
||||
"jigsawStudioService.quiesceForServerShutdown()");
|
||||
assertOrdered(cleanup, "runPostShutdown()", "hasActiveShutdownResources()", "IrisPlatforms.unbind()",
|
||||
"runtimeTeardownFailed.get()", "releasePluginClassLoaderClose()");
|
||||
assertTrue(cleanup.contains("!MultiBurst.burst.isTerminated() || !MultiBurst.ioBurst.isTerminated()"));
|
||||
assertTrue(cleanup.contains("preservation.hasActiveResources()"));
|
||||
assertOrdered(teardown, "drainWorldGenerators(reason, timeoutSeconds)", "!generatorDrainCompleted",
|
||||
"return;", "service.onDisable()");
|
||||
assertOrdered(deferred, "teardownRuntime(reason, timeoutSeconds)", "!generatorDrainCompleted",
|
||||
"SHUTDOWN_ERRORS.println", "return;", "finishTerminalCleanup()");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deferredShutdownErrorsBypassClosedLoggingServices() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
String throwableReport = section(source, "public static void reportError(Throwable e)",
|
||||
"public static void reportError(String context, Throwable e)");
|
||||
String contextualReport = section(source, "public static void reportError(String context, Throwable e)",
|
||||
"public static void dump()");
|
||||
|
||||
assertOrdered(throwableReport, "serverStopTeardownDeferred.get()",
|
||||
"e.printStackTrace(SHUTDOWN_ERRORS)", "boolean debug = false");
|
||||
assertOrdered(contextualReport, "serverStopTeardownDeferred.get()",
|
||||
"SHUTDOWN_ERRORS.println(", "error.printStackTrace(SHUTDOWN_ERRORS)", "Iris.error(message)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
@@ -18,6 +52,22 @@ public class IrisShutdownOrderingTest {
|
||||
assertFalse("Shutdown must not close Mantle plates before generation drains", drain.contains("saveAllNow()"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shutdownBoundaryIsPreparedBeforeHookRegistrationAndDoesNotUseClosedBindings() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
String registration = section(source, "public void addShutdownHook()", "public void removeShutdownHook()");
|
||||
String boundary = section(source, "private boolean awaitServerShutdownBoundary()",
|
||||
"private void finishDeferredRuntimeTeardown");
|
||||
|
||||
assertOrdered(registration, "createServerShutdownBoundary()", "new Thread(this::runShutdownHook",
|
||||
"Runtime.getRuntime().addShutdownHook");
|
||||
assertTrue(boundary.contains("boundary.await("));
|
||||
assertFalse(boundary.contains("INMS.get()"));
|
||||
assertFalse(boundary.contains("Iris.reportError"));
|
||||
assertTrue(source.contains("new FileOutputStream(FileDescriptor.err)"));
|
||||
assertOrdered(boundary, "SHUTDOWN_ERRORS.println(", "e.printStackTrace(SHUTDOWN_ERRORS)", "return false;");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception {
|
||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||
@@ -43,6 +93,7 @@ public class IrisShutdownOrderingTest {
|
||||
"finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)",
|
||||
"return;",
|
||||
"awaitServerShutdownBoundary()",
|
||||
"SHUTDOWN_ERRORS.println",
|
||||
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
|
||||
assertOrdered(finisher,
|
||||
"finisher.setDaemon(false)",
|
||||
@@ -85,8 +136,8 @@ public class IrisShutdownOrderingTest {
|
||||
assertOrdered(teardown,
|
||||
"drainWorldGenerators(reason, timeoutSeconds)",
|
||||
"service.onDisable()",
|
||||
"MultiBurst.burst::close",
|
||||
"MultiBurst.ioBurst::close");
|
||||
"MultiBurst.burst.close()",
|
||||
"MultiBurst.ioBurst.close()");
|
||||
assertTrue("Engine service must retain its generator close behind deferred service teardown",
|
||||
engineDisable.contains("startClose("));
|
||||
assertTrue("Studio service must retain its generator close behind deferred service teardown",
|
||||
|
||||
+13
-7
@@ -195,7 +195,7 @@ public class IrisWorldGeneratorResolverTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configuredWorldResolutionUsesOnlyFrozenWorldLocalSnapshot() throws Exception {
|
||||
public void configuredWorldResolutionUsesOnlyActiveImmutableGenerationPack() throws Exception {
|
||||
String source = Files.readString(Path.of(
|
||||
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n");
|
||||
int resolverStart = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator(");
|
||||
@@ -208,26 +208,32 @@ public class IrisWorldGeneratorResolverTest {
|
||||
"Frozen Iris world storage does not match the current platform layout",
|
||||
currentPlatformRoot
|
||||
);
|
||||
int snapshotRoot = resolver.indexOf("IrisWorldStorage.requireFrozenPackRoot(dimensionRoot)");
|
||||
int history = resolver.indexOf("requireGenerationHistory(dimensionRoot, id, worldSeed)");
|
||||
int snapshotRoot = resolver.indexOf("requireActivePack(history)", history);
|
||||
int validation = resolver.indexOf("requireSnapshotLoadable(snapshotRoot)");
|
||||
int exactLoad = resolver.indexOf(
|
||||
"IrisData.get(snapshotRoot).getDimensionLoader().load(id, false)"
|
||||
);
|
||||
int exactLoad = resolver.indexOf("requireHistoricalDimension(history, snapshotRoot, id)");
|
||||
int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())");
|
||||
int historicalSeed = resolver.indexOf(".seed(history.activeEpoch().worldSeed())");
|
||||
int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)");
|
||||
|
||||
assertTrue(dimensionRoot >= 0);
|
||||
assertTrue(currentPlatformRoot > dimensionRoot);
|
||||
assertTrue(layoutRefusal > currentPlatformRoot);
|
||||
assertTrue(snapshotRoot > layoutRefusal);
|
||||
assertTrue(history > layoutRefusal);
|
||||
assertTrue(snapshotRoot > history);
|
||||
assertTrue(validation > snapshotRoot);
|
||||
assertTrue(exactLoad > validation);
|
||||
assertTrue(canonicalIdentity > exactLoad);
|
||||
assertTrue(resolvedStorage > canonicalIdentity);
|
||||
assertTrue(historicalSeed > canonicalIdentity);
|
||||
assertTrue(resolvedStorage > historicalSeed);
|
||||
assertFalse(resolver.contains("loadDimension("));
|
||||
assertFalse(resolver.contains("loadAnyDimension("));
|
||||
assertFalse(resolver.contains("replaceIntoWorld("));
|
||||
assertFalse(resolver.contains("installIntoWorld("));
|
||||
assertFalse(resolver.contains(".seed(1337)"));
|
||||
assertTrue(resolver.contains("GenerationHistory.openIfPresent(root, worldSeed)"));
|
||||
assertTrue(resolver.contains("GenerationHistory.adoptLegacyPack("));
|
||||
assertTrue(resolver.contains("GenerationRegistryContractFactory.captureRequiredDefinitions("));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+26
@@ -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"));
|
||||
}
|
||||
}
|
||||
+26
@@ -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()"));
|
||||
}
|
||||
}
|
||||
+27
@@ -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("));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -30,7 +30,7 @@ public class BukkitEngineLifecycleContractTest {
|
||||
String baseHeight = method(source, "public int getBaseHeight(");
|
||||
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
|
||||
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
|
||||
assertTrue(baseHeight.contains("catch (GenerationSessionException e)"));
|
||||
assertTrue(baseHeight.contains("catch (GenerationSessionException | IOException e)"));
|
||||
|
||||
String generation = method(source, "public void generateNoise(");
|
||||
assertTrue(generation.contains("throw new IllegalStateException"));
|
||||
|
||||
+155
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user