mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Merged generation history
- Integrated immutable generation epochs after dimension stacking - Preserved historical terrain, biome, structure, and locate behavior - Validated combined Bukkit and modded generation contracts
This commit is contained in:
commit
20ad49b9d2
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.DimensionStackLayout;
|
||||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -40,6 +42,7 @@ import org.bukkit.craftbukkit.CraftWorld;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -65,14 +68,11 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
private final Registry<Biome> biomeRegistry;
|
private final Registry<Biome> biomeRegistry;
|
||||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||||
private final Holder<Biome> fallbackBiome;
|
private final Holder<Biome> fallbackBiome;
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<RuntimeNoiseKey, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<RuntimeNoiseKey, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<RuntimeColumnKey, Holder<Biome>> surfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<Long, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<RuntimeColumnKey, Holder<Biome>> naturalSurfaceStructureBiomeCache = new ConcurrentHashMap<>();
|
||||||
private volatile KMap<String, Holder<Biome>> customBiomes;
|
private final ConcurrentHashMap<Integer, RuntimeBiomeState> runtimeBiomeStates = new ConcurrentHashMap<>();
|
||||||
private volatile Map<Biome, Holder<Biome>> vanillaSpawnBiomes;
|
|
||||||
private volatile IrisDimension cacheDimension;
|
|
||||||
private volatile int cacheRuntimeId;
|
|
||||||
|
|
||||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
@@ -80,10 +80,10 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
||||||
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
||||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
if (engine instanceof IrisEngine irisEngine) {
|
||||||
this.vanillaSpawnBiomes = fillVanillaSpawnBiomes(this.biomeCustomRegistry, this.biomeRegistry, engine);
|
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
|
||||||
this.cacheDimension = engine.getDimension();
|
}
|
||||||
this.cacheRuntimeId = engine.getCacheID();
|
runtimeBiomeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Holder<Biome>> getAllBiomes(
|
private static List<Holder<Biome>> getAllBiomes(
|
||||||
@@ -133,7 +133,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (irisBiome.isCustom()) {
|
if (irisBiome.isCustom()) {
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(
|
Holder<Biome> customHolder = resolveCustomBiomeHolder(
|
||||||
customRegistry, ownedBiome.dimension(), customBiome.getId());
|
customRegistry, engine, ownedBiome.dimension(), customBiome.getId());
|
||||||
if (customHolder == null) {
|
if (customHolder == null) {
|
||||||
throw new IllegalStateException("Iris custom visible biome '"
|
throw new IllegalStateException("Iris custom visible biome '"
|
||||||
+ customBiomeKey(ownedBiome.dimension(), customBiome.getId())
|
+ customBiomeKey(ownedBiome.dimension(), customBiome.getId())
|
||||||
@@ -257,7 +257,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (!isRuntimeAvailable()) {
|
if (!isRuntimeAvailable()) {
|
||||||
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
|
throw new IllegalStateException("Iris possible biome lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
ensureCachesCurrent();
|
runtimeBiomeState();
|
||||||
World world = BukkitWorldBinding.world(engine.getWorld());
|
World world = BukkitWorldBinding.world(engine.getWorld());
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
throw new IllegalStateException("Iris biome source has no bound Bukkit world");
|
||||||
@@ -284,7 +284,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||||
String key = customBiomeKey(ownedBiome.dimension(), j.getId());
|
String key = customBiomeKey(ownedBiome.dimension(), j.getId());
|
||||||
Holder<Biome> holder = resolveCustomBiomeHolder(
|
Holder<Biome> holder = resolveCustomBiomeHolder(
|
||||||
customRegistry, ownedBiome.dimension(), j.getId());
|
customRegistry, engine, ownedBiome.dimension(), j.getId());
|
||||||
if (holder == null) {
|
if (holder == null) {
|
||||||
if (fallback != null) {
|
if (fallback != null) {
|
||||||
m.put(key, fallback);
|
m.put(key, fallback);
|
||||||
@@ -318,7 +318,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(
|
Holder<Biome> customHolder = resolveCustomBiomeHolder(
|
||||||
customRegistry, ownedBiome.dimension(), customBiome.getId());
|
customRegistry, engine, ownedBiome.dimension(), customBiome.getId());
|
||||||
if (customHolder != null) {
|
if (customHolder != null) {
|
||||||
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
spawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||||
}
|
}
|
||||||
@@ -340,8 +340,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (!isRuntimeAvailable()) {
|
if (!isRuntimeAvailable()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ensureCachesCurrent();
|
return runtimeBiomeState().vanillaSpawnBiomes().get(biome.value());
|
||||||
return vanillaSpawnBiomes.get(biome.value());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +353,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (!isRuntimeAvailable()) {
|
if (!isRuntimeAvailable()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ensureCachesCurrent();
|
runtimeBiomeState();
|
||||||
int quartX = QuartPos.fromBlock(blockX);
|
int quartX = QuartPos.fromBlock(blockX);
|
||||||
int quartZ = QuartPos.fromBlock(blockZ);
|
int quartZ = QuartPos.fromBlock(blockZ);
|
||||||
int sampleX = QuartPos.toBlock(quartX);
|
int sampleX = QuartPos.toBlock(quartX);
|
||||||
@@ -390,15 +389,17 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_structure_biome");
|
int blockX = x << 2;
|
||||||
if (lease == null) {
|
int blockZ = z << 2;
|
||||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||||
}
|
openHistoryCoordinateScope(blockX, blockZ, "bukkit_structure_biome");
|
||||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
GenerationSessionLease lease = requireGenerationLease(
|
||||||
|
"bukkit_structure_biome",
|
||||||
|
"Iris structure biome lookup was rejected during an engine transition");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
if (!isRuntimeAvailable()) {
|
if (!isRuntimeAvailable()) {
|
||||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
ensureCachesCurrent();
|
|
||||||
return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
|
return getStructureNoiseBiomeWithActiveGenerationLease(x, y, z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,7 +410,8 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean cacheable = isBiomeCacheable(x, z);
|
boolean cacheable = isBiomeCacheable(x, z);
|
||||||
long cacheKey = packNoiseKey(x, y, z);
|
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
|
||||||
|
engine.getCacheID(), packNoiseKey(x, y, z));
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||||
if (cachedHolder != null) {
|
if (cachedHolder != null) {
|
||||||
@@ -455,7 +457,6 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (!isRuntimeAvailable()) {
|
if (!isRuntimeAvailable()) {
|
||||||
throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime");
|
throw new IllegalStateException("Iris structure ring biome lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
ensureCachesCurrent();
|
|
||||||
return findNaturalSurfaceBiomeHorizontal(
|
return findNaturalSurfaceBiomeHorizontal(
|
||||||
x, y, z, searchRadius, quartStep, allowed, random);
|
x, y, z, searchRadius, quartStep, allowed, random);
|
||||||
}
|
}
|
||||||
@@ -533,7 +534,6 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (!isRuntimeAvailable()) {
|
if (!isRuntimeAvailable()) {
|
||||||
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
ensureCachesCurrent();
|
|
||||||
int minQuartY = QuartPos.fromBlock(y - radius);
|
int minQuartY = QuartPos.fromBlock(y - radius);
|
||||||
boolean monumentQuery = radius == 29
|
boolean monumentQuery = radius == 29
|
||||||
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
&& y == engine.getMinHeight() + engine.getDimension().getFluidHeight();
|
||||||
@@ -556,20 +556,27 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
private Holder<Biome> getSurfaceStructureBiomeHolder(int x, int z) {
|
||||||
long columnKey = packColumnKey(x, z);
|
int blockX = x << 2;
|
||||||
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
|
int blockZ = z << 2;
|
||||||
if (surfaceHolder != null) {
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||||
return surfaceHolder;
|
openHistoryCoordinateScope(blockX, blockZ, "bukkit_surface_structure_biome")) {
|
||||||
|
RuntimeColumnKey columnKey = new RuntimeColumnKey(
|
||||||
|
engine.getCacheID(), packColumnKey(x, z));
|
||||||
|
Holder<Biome> surfaceHolder = surfaceStructureBiomeCache.get(columnKey);
|
||||||
|
if (surfaceHolder != null) {
|
||||||
|
return surfaceHolder;
|
||||||
|
}
|
||||||
|
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
|
||||||
|
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(
|
||||||
|
columnKey, resolvedSurfaceHolder);
|
||||||
|
if (existingSurfaceHolder != null) {
|
||||||
|
return existingSurfaceHolder;
|
||||||
|
}
|
||||||
|
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||||
|
surfaceStructureBiomeCache.clear();
|
||||||
|
}
|
||||||
|
return resolvedSurfaceHolder;
|
||||||
}
|
}
|
||||||
Holder<Biome> resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
|
|
||||||
Holder<Biome> existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(columnKey, resolvedSurfaceHolder);
|
|
||||||
if (existingSurfaceHolder != null) {
|
|
||||||
return existingSurfaceHolder;
|
|
||||||
}
|
|
||||||
if (surfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
|
||||||
surfaceStructureBiomeCache.clear();
|
|
||||||
}
|
|
||||||
return resolvedSurfaceHolder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<BlockPos, Holder<Biome>> findNaturalSurfaceBiomeHorizontal(
|
private Pair<BlockPos, Holder<Biome>> findNaturalSurfaceBiomeHorizontal(
|
||||||
@@ -609,21 +616,27 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) {
|
private Holder<Biome> getNaturalSurfaceStructureBiomeHolder(int x, int z) {
|
||||||
long columnKey = packColumnKey(x, z);
|
int blockX = x << 2;
|
||||||
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
|
int blockZ = z << 2;
|
||||||
if (cachedHolder != null) {
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
|
||||||
return cachedHolder;
|
openHistoryCoordinateScope(blockX, blockZ, "bukkit_natural_structure_biome")) {
|
||||||
|
RuntimeColumnKey columnKey = new RuntimeColumnKey(
|
||||||
|
engine.getCacheID(), packColumnKey(x, z));
|
||||||
|
Holder<Biome> cachedHolder = naturalSurfaceStructureBiomeCache.get(columnKey);
|
||||||
|
if (cachedHolder != null) {
|
||||||
|
return cachedHolder;
|
||||||
|
}
|
||||||
|
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
|
||||||
|
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
|
||||||
|
columnKey, resolvedHolder);
|
||||||
|
if (existingHolder != null) {
|
||||||
|
return existingHolder;
|
||||||
|
}
|
||||||
|
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||||
|
naturalSurfaceStructureBiomeCache.clear();
|
||||||
|
}
|
||||||
|
return resolvedHolder;
|
||||||
}
|
}
|
||||||
Holder<Biome> resolvedHolder = resolveNaturalSurfaceStructureBiomeHolder(x, z);
|
|
||||||
Holder<Biome> existingHolder = naturalSurfaceStructureBiomeCache.putIfAbsent(
|
|
||||||
columnKey, resolvedHolder);
|
|
||||||
if (existingHolder != null) {
|
|
||||||
return existingHolder;
|
|
||||||
}
|
|
||||||
if (naturalSurfaceStructureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
|
||||||
naturalSurfaceStructureBiomeCache.clear();
|
|
||||||
}
|
|
||||||
return resolvedHolder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
private boolean isGuaranteedSurfaceBiome(int quartY) {
|
||||||
@@ -671,11 +684,12 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease("bukkit_visible_biome");
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
if (lease == null) {
|
x << 2, z << 2, "bukkit_visible_biome");
|
||||||
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
GenerationSessionLease lease = requireGenerationLease(
|
||||||
}
|
"bukkit_visible_biome",
|
||||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
"Iris visible biome lookup was rejected during an engine transition");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
prepareVisibleBiomeBatch();
|
prepareVisibleBiomeBatch();
|
||||||
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
|
return getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler);
|
||||||
}
|
}
|
||||||
@@ -685,7 +699,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
if (!isRuntimeAvailable()) {
|
if (!isRuntimeAvailable()) {
|
||||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
ensureCachesCurrent();
|
runtimeBiomeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||||
@@ -717,7 +731,8 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
IrisDimensionCarvingResolver.State resolverState,
|
IrisDimensionCarvingResolver.State resolverState,
|
||||||
boolean cacheable
|
boolean cacheable
|
||||||
) {
|
) {
|
||||||
long cacheKey = packNoiseKey(x, y, z);
|
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
|
||||||
|
engine.getCacheID(), packNoiseKey(x, y, z));
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||||
if (cachedHolder != null) {
|
if (cachedHolder != null) {
|
||||||
@@ -768,32 +783,71 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GenerationSessionLease requireGenerationLease(String operation, String rejectionMessage) {
|
||||||
|
GenerationSessionLease lease = tryAcquireGenerationLease(operation);
|
||||||
|
if (lease == null) {
|
||||||
|
throw new IllegalStateException(rejectionMessage);
|
||||||
|
}
|
||||||
|
return lease;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isRuntimeAvailable() {
|
private boolean isRuntimeAvailable() {
|
||||||
return !engine.isClosed() && engine.getComplex() != null;
|
return !engine.isClosed() && engine.getComplex() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureCachesCurrent() {
|
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||||
|
int blockX,
|
||||||
|
int blockZ,
|
||||||
|
String operation
|
||||||
|
) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris " + operation
|
||||||
|
+ " could not route generation history at "
|
||||||
|
+ blockX + "," + blockZ + ".", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void evictRuntimeCaches(int runtimeId) {
|
||||||
|
runtimeBiomeStates.remove(runtimeId);
|
||||||
|
noiseBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
structureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
surfaceStructureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
naturalSurfaceStructureBiomeCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeBiomeState runtimeBiomeState() {
|
||||||
IrisDimension dimension = engine.getDimension();
|
IrisDimension dimension = engine.getDimension();
|
||||||
int runtimeId = engine.getCacheID();
|
int runtimeId = engine.getCacheID();
|
||||||
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
|
RuntimeBiomeState existing = runtimeBiomeStates.get(runtimeId);
|
||||||
return;
|
if (existing != null) {
|
||||||
|
if (existing.dimension() != dimension) {
|
||||||
|
throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions.");
|
||||||
|
}
|
||||||
|
return existing;
|
||||||
}
|
}
|
||||||
synchronized (this) {
|
synchronized (runtimeBiomeStates) {
|
||||||
if (cacheDimension == dimension && cacheRuntimeId == runtimeId) {
|
existing = runtimeBiomeStates.get(runtimeId);
|
||||||
return;
|
if (existing != null) {
|
||||||
|
if (existing.dimension() != dimension) {
|
||||||
|
throw new IllegalStateException("Iris biome runtime cache ID is shared by different dimensions.");
|
||||||
|
}
|
||||||
|
return existing;
|
||||||
}
|
}
|
||||||
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
|
KMap<String, Holder<Biome>> refreshedCustomBiomes = fillCustomBiomes(
|
||||||
biomeCustomRegistry, engine, fallbackBiome);
|
biomeCustomRegistry, engine, fallbackBiome);
|
||||||
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
|
Map<Biome, Holder<Biome>> refreshedSpawnBiomes = fillVanillaSpawnBiomes(
|
||||||
biomeCustomRegistry, biomeRegistry, engine);
|
biomeCustomRegistry, biomeRegistry, engine);
|
||||||
noiseBiomeCache.clear();
|
RuntimeBiomeState created = new RuntimeBiomeState(
|
||||||
structureBiomeCache.clear();
|
dimension,
|
||||||
surfaceStructureBiomeCache.clear();
|
refreshedCustomBiomes,
|
||||||
naturalSurfaceStructureBiomeCache.clear();
|
refreshedSpawnBiomes);
|
||||||
customBiomes = refreshedCustomBiomes;
|
runtimeBiomeStates.put(runtimeId, created);
|
||||||
vanillaSpawnBiomes = refreshedSpawnBiomes;
|
return created;
|
||||||
cacheDimension = dimension;
|
|
||||||
cacheRuntimeId = runtimeId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,6 +884,14 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
IrisDimensionCarvingResolver.State resolverState,
|
IrisDimensionCarvingResolver.State resolverState,
|
||||||
DimensionStackLayout stackLayout
|
DimensionStackLayout stackLayout
|
||||||
) {
|
) {
|
||||||
|
int blockX = x << 2;
|
||||||
|
int blockY = y << 2;
|
||||||
|
int blockZ = z << 2;
|
||||||
|
Optional<String> historicalKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
|
||||||
|
blockX, blockY, blockZ);
|
||||||
|
if (historicalKey.isPresent()) {
|
||||||
|
return resolvePhysicalBiomeHolder(historicalKey.get());
|
||||||
|
}
|
||||||
BiomeResolution resolution = resolveBiomeResolution(
|
BiomeResolution resolution = resolveBiomeResolution(
|
||||||
x, y, z, resolverState, true, stackLayout);
|
x, y, z, resolverState, true, stackLayout);
|
||||||
if (resolution == null) {
|
if (resolution == null) {
|
||||||
@@ -858,7 +920,7 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||||
if (customBiome != null) {
|
if (customBiome != null) {
|
||||||
Holder<Biome> holder = customBiomes.get(customBiomeKey(
|
Holder<Biome> holder = runtimeBiomeState().customBiomes().get(customBiomeKey(
|
||||||
resolution.dimension, customBiome.getId()));
|
resolution.dimension, customBiome.getId()));
|
||||||
if (holder != null) {
|
if (holder != null) {
|
||||||
return holder;
|
return holder;
|
||||||
@@ -872,6 +934,18 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
return resolveBiomeResolution(x, y, z, null, false);
|
return resolveBiomeResolution(x, y, z, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Holder<Biome> resolvePhysicalBiomeHolder(String physicalKey) {
|
||||||
|
Holder<Biome> holder = resolveBiomeHolder(biomeCustomRegistry, physicalKey);
|
||||||
|
if (holder == null) {
|
||||||
|
holder = resolveBiomeHolder(biomeRegistry, physicalKey);
|
||||||
|
}
|
||||||
|
if (holder == null) {
|
||||||
|
throw new IllegalStateException("Historical Iris biome '" + physicalKey
|
||||||
|
+ "' is not registered in the active world registry.");
|
||||||
|
}
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
|
||||||
private BiomeResolution resolveBiomeResolution(
|
private BiomeResolution resolveBiomeResolution(
|
||||||
int x,
|
int x,
|
||||||
int y,
|
int y,
|
||||||
@@ -1008,17 +1082,19 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
private static Holder<Biome> resolveCustomBiomeHolder(
|
private static Holder<Biome> resolveCustomBiomeHolder(
|
||||||
Registry<Biome> customRegistry,
|
Registry<Biome> customRegistry,
|
||||||
|
Engine engine,
|
||||||
IrisDimension dimension,
|
IrisDimension dimension,
|
||||||
String customBiomeId
|
String customBiomeId
|
||||||
) {
|
) {
|
||||||
if (customRegistry == null || dimension == null
|
if (customRegistry == null || engine == null || dimension == null
|
||||||
|| customBiomeId == null || customBiomeId.isBlank()) {
|
|| customBiomeId == null || customBiomeId.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Identifier resourceLocation = Identifier.tryParse(customBiomeKey(dimension, customBiomeId));
|
String physicalKey = engine.getData().customBiomeResourceKey(dimension, customBiomeId);
|
||||||
|
Identifier resourceLocation = Identifier.tryParse(physicalKey);
|
||||||
if (resourceLocation == null) {
|
if (resourceLocation == null) {
|
||||||
return null;
|
throw new IllegalStateException("Invalid Iris custom biome resource key '" + physicalKey + "'.");
|
||||||
}
|
}
|
||||||
Biome biome = customRegistry.getValue(resourceLocation);
|
Biome biome = customRegistry.getValue(resourceLocation);
|
||||||
if (biome == null) {
|
if (biome == null) {
|
||||||
@@ -1170,4 +1246,17 @@ public class CustomBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
private record OwnedBiome(IrisDimension dimension, IrisBiome biome) {
|
private record OwnedBiome(IrisDimension dimension, IrisBiome biome) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RuntimeNoiseKey(int runtimeId, long coordinateKey) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RuntimeColumnKey(int runtimeId, long coordinateKey) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RuntimeBiomeState(
|
||||||
|
IrisDimension dimension,
|
||||||
|
KMap<String, Holder<Biome>> customBiomes,
|
||||||
|
Map<Biome, Holder<Biome>> vanillaSpawnBiomes
|
||||||
|
) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+34
-40
@@ -62,9 +62,7 @@ final class ImportedFeatureStage {
|
|||||||
private static final String CYCLE_MARKER = "Feature order cycle found";
|
private static final String CYCLE_MARKER = "Feature order cycle found";
|
||||||
|
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
private volatile FeatureTable featureTable;
|
private final ConcurrentHashMap<Integer, RuntimeFeatureState> runtimeStates = new ConcurrentHashMap<>();
|
||||||
private volatile IrisDimension inertDimension;
|
|
||||||
private volatile int settledRuntimeId;
|
|
||||||
|
|
||||||
ImportedFeatureStage(Engine engine) {
|
ImportedFeatureStage(Engine engine) {
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
@@ -76,7 +74,8 @@ final class ImportedFeatureStage {
|
|||||||
* does today.
|
* does today.
|
||||||
*/
|
*/
|
||||||
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
||||||
FeatureTable table = featureTable;
|
RuntimeFeatureState state = currentState();
|
||||||
|
FeatureTable table = state == null ? null : state.featureTable();
|
||||||
if (table == null) {
|
if (table == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -87,50 +86,46 @@ final class ImportedFeatureStage {
|
|||||||
/**
|
/**
|
||||||
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
|
* Builds the table on the first decorated chunk, or leaves the stage inert. A feature-order cycle is
|
||||||
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
|
* reported once here and degrades to features-off; it never reaches chunk generation as a crash.
|
||||||
*
|
|
||||||
* <p>The volatile fast path is unlocked, so a prepared stage costs two reads per chunk. The build is
|
|
||||||
* synchronized: every worldgen thread decorating a chunk calls this, and two threads that both found the
|
|
||||||
* stage unprepared would each run {@code FeatureSorter}, whose cycle detection is the expensive part.
|
|
||||||
*/
|
*/
|
||||||
void prepare(WorldGenLevel level) {
|
void prepare(WorldGenLevel level) {
|
||||||
IrisDimension dimension = engine.getDimension();
|
IrisDimension dimension = engine.getDimension();
|
||||||
int runtimeId = engine.getCacheID();
|
int runtimeId = engine.getCacheID();
|
||||||
if (settled(dimension, runtimeId)) {
|
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (settled(dimension, runtimeId)) {
|
if (settled(runtimeStates.get(runtimeId), dimension)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
build(level, dimension);
|
runtimeStates.put(runtimeId, build(level, dimension));
|
||||||
settledRuntimeId = runtimeId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean settled(IrisDimension dimension, int runtimeId) {
|
void evictRuntime(int runtimeId) {
|
||||||
if (settledRuntimeId != runtimeId) {
|
runtimeStates.remove(runtimeId);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
FeatureTable current = featureTable;
|
|
||||||
if (current != null && current.dimension() == dimension) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return inertDimension == dimension;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void build(WorldGenLevel level, IrisDimension dimension) {
|
private RuntimeFeatureState currentState() {
|
||||||
|
IrisDimension dimension = engine.getDimension();
|
||||||
|
RuntimeFeatureState state = runtimeStates.get(engine.getCacheID());
|
||||||
|
return settled(state, dimension) ? state : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean settled(RuntimeFeatureState state, IrisDimension dimension) {
|
||||||
|
return state != null && state.dimension() == dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeFeatureState build(WorldGenLevel level, IrisDimension dimension) {
|
||||||
IrisImportedFeatureControl control;
|
IrisImportedFeatureControl control;
|
||||||
try {
|
try {
|
||||||
control = NativeFeatureGenerationPolicy.control(engine);
|
control = NativeFeatureGenerationPolicy.control(engine);
|
||||||
} catch (RuntimeException error) {
|
} catch (RuntimeException error) {
|
||||||
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
|
IrisLogging.error("Iris could not read importedFeatures for this dimension; features off: "
|
||||||
+ error);
|
+ error);
|
||||||
markInert(dimension);
|
return new RuntimeFeatureState(dimension, null);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!control.shouldGenerateFeatures()) {
|
if (!control.shouldGenerateFeatures()) {
|
||||||
markInert(dimension);
|
return new RuntimeFeatureState(dimension, null);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
FeatureTable built;
|
FeatureTable built;
|
||||||
try {
|
try {
|
||||||
@@ -138,23 +133,15 @@ final class ImportedFeatureStage {
|
|||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
|
IrisLogging.error("Iris importedFeatures is off for " + dimensionKey()
|
||||||
+ ": feature table construction failed: " + error);
|
+ ": feature table construction failed: " + error);
|
||||||
markInert(dimension);
|
return new RuntimeFeatureState(dimension, null);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (built == null) {
|
if (built == null) {
|
||||||
markInert(dimension);
|
return new RuntimeFeatureState(dimension, null);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
featureTable = built;
|
|
||||||
inertDimension = null;
|
|
||||||
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
|
IrisLogging.info("Iris importedFeatures on for " + dimensionKey() + ": " + built.biomes().size()
|
||||||
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
|
+ " biomes, " + built.steps().size() + " steps, " + built.derivatives().size()
|
||||||
+ " custom-biome derivative maps");
|
+ " custom-biome derivative maps");
|
||||||
}
|
return new RuntimeFeatureState(dimension, built);
|
||||||
|
|
||||||
private void markInert(IrisDimension dimension) {
|
|
||||||
featureTable = null;
|
|
||||||
inertDimension = dimension;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
|
private FeatureTable buildTable(WorldGenLevel level, IrisImportedFeatureControl control,
|
||||||
@@ -217,7 +204,7 @@ final class ImportedFeatureStage {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
addKey(keys, engine.getDimension().getCustomBiomeKey(customBiome.getId()));
|
addKey(keys, engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
@@ -249,7 +236,10 @@ final class ImportedFeatureStage {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
derivatives.put(engine.getDimension().getCustomBiomeKey(customBiome.getId()), derivative);
|
derivatives.put(
|
||||||
|
engine.getData().customBiomeResourceKey(engine.getDimension(), customBiome),
|
||||||
|
derivative
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return derivatives;
|
return derivatives;
|
||||||
@@ -280,7 +270,8 @@ final class ImportedFeatureStage {
|
|||||||
* control is disabled or the table degraded.
|
* control is disabled or the table degraded.
|
||||||
*/
|
*/
|
||||||
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
|
void run(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator owner) {
|
||||||
FeatureTable table = featureTable;
|
RuntimeFeatureState state = currentState();
|
||||||
|
FeatureTable table = state == null ? null : state.featureTable();
|
||||||
if (table == null) {
|
if (table == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -467,4 +458,7 @@ final class ImportedFeatureStage {
|
|||||||
List<FeatureSorter.StepFeatureData> steps,
|
List<FeatureSorter.StepFeatureData> steps,
|
||||||
Map<String, Holder<Biome>> derivatives, boolean filtered) {
|
Map<String, Holder<Biome>> derivatives, boolean filtered) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RuntimeFeatureState(IrisDimension dimension, FeatureTable featureTable) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+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.GenerationSessionLease;
|
||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.IrisEngine;
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
|
||||||
@@ -101,6 +103,7 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -133,8 +136,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
private final int runtimeHeight;
|
private final int runtimeHeight;
|
||||||
private final int runtimeSeaLevel;
|
private final int runtimeSeaLevel;
|
||||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||||
private volatile IrisDimension spawnTableDimension;
|
|
||||||
private volatile int spawnTableRuntimeId;
|
|
||||||
private final ImportedFeatureStage importedFeatures;
|
private final ImportedFeatureStage importedFeatures;
|
||||||
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
|
private final AtomicReference<StudioStructureState> retainedStudioStructureState = new AtomicReference<>();
|
||||||
private volatile ReachableStructureCache reachableStructureCache;
|
private volatile ReachableStructureCache reachableStructureCache;
|
||||||
@@ -158,9 +159,25 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
this.runtimeMinY = level.getMinY();
|
this.runtimeMinY = level.getMinY();
|
||||||
this.runtimeHeight = level.getHeight();
|
this.runtimeHeight = level.getHeight();
|
||||||
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
|
this.runtimeSeaLevel = runtimeMinY + engine.getDimension().getFluidHeight();
|
||||||
|
if (engine instanceof IrisEngine irisEngine) {
|
||||||
|
irisEngine.addGenerationRuntimeRetirementListener(this::evictRuntimeCaches);
|
||||||
|
}
|
||||||
installNativeStructureVolumeIndex(level);
|
installNativeStructureVolumeIndex(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void evictRuntimeCaches(int runtimeId) {
|
||||||
|
importedFeatures.evictRuntime(runtimeId);
|
||||||
|
mergedSpawnTables.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
ReachableStructureCache reachable = reachableStructureCache;
|
||||||
|
if (reachable != null && reachable.runtimeId() == runtimeId) {
|
||||||
|
reachableStructureCache = null;
|
||||||
|
}
|
||||||
|
StructureStepCache steps = structureStepCache;
|
||||||
|
if (steps != null && steps.runtimeId() == runtimeId) {
|
||||||
|
structureStepCache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void installNativeStructureVolumeIndex(ServerLevel level) {
|
private void installNativeStructureVolumeIndex(ServerLevel level) {
|
||||||
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
|
WeakReference<ServerLevel> levelReference = new WeakReference<>(level);
|
||||||
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
|
WeakReference<ChunkGenerator> generatorReference = new WeakReference<>(this);
|
||||||
@@ -186,7 +203,12 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) {
|
if (level != runtimeLevel || level.getChunkSource().getGenerator() != this) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try (GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
SectionPos.blockToSectionCoord(pos.getX()),
|
||||||
|
SectionPos.blockToSectionCoord(pos.getZ()),
|
||||||
|
"bukkit_nms_structure_locate");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_structure_locate");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
||||||
NativeStructureVanillaLocator.Candidate nativeCandidate =
|
NativeStructureVanillaLocator.Candidate nativeCandidate =
|
||||||
@@ -365,9 +387,16 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|| runtimeLevel.getChunkSource().getGeneratorState() != structureState) {
|
|| runtimeLevel.getChunkSource().getGeneratorState() != structureState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ChunkPos chunkPos = access.getPos();
|
||||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures");
|
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_structures");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_structures");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_structures");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
if (!allowsRoutedDiscreteGeneration(access, ChunkStatus.STRUCTURE_STARTS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
Map<Structure, StructureStart> previousStarts = new HashMap<>(access.getAllStarts());
|
||||||
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
super.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||||
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
|
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
|
||||||
@@ -399,15 +428,28 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (configuredStarts.containsKey(structure)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Identifier id = registry.getKey(structure);
|
Identifier id = registry.getKey(structure);
|
||||||
String structureId = id == null ? null : id.toString();
|
String structureId = id == null ? null : id.toString();
|
||||||
if (structureId == null) {
|
if (structureId == null) {
|
||||||
throw NativeStructureGenerationException.failure(
|
throw NativeStructureGenerationException.failure(
|
||||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||||
}
|
}
|
||||||
|
BoundingBox footprint = start.getBoundingBox();
|
||||||
|
if (!engine.getComplex().allowsNewGenerationFootprint(
|
||||||
|
footprint.minX(),
|
||||||
|
footprint.minZ(),
|
||||||
|
footprint.maxX(),
|
||||||
|
footprint.maxZ())) {
|
||||||
|
access.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||||
|
if (configuredStarts.containsKey(structure)) {
|
||||||
|
NativeStructureOwnershipStore.discard(
|
||||||
|
engine, structureId, chunkPos.x(), chunkPos.z());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (configuredStarts.containsKey(structure)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
||||||
IrisNativeStructureDecision decision;
|
IrisNativeStructureDecision decision;
|
||||||
try {
|
try {
|
||||||
@@ -634,7 +676,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
if (runtimeLevel.getChunkSource().getGenerator() != this) {
|
if (runtimeLevel.getChunkSource().getGenerator() != this) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references");
|
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_references");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_references");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references");
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_references");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
NativeStructureReferenceRepair.createReferences(
|
NativeStructureReferenceRepair.createReferences(
|
||||||
@@ -644,7 +690,11 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||||
|
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes");
|
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_create_biomes");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
chunkPos.x(), chunkPos.z(), "bukkit_nms_create_biomes");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_create_biomes");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
customBiomeSource.prepareVisibleBiomeBatch();
|
customBiomeSource.prepareVisibleBiomeBatch();
|
||||||
@@ -659,31 +709,63 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
|
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
|
||||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||||
|
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_build_surface");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
chunkPos.x(), chunkPos.z(), "bukkit_nms_build_surface");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_build_surface");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||||
|
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_apply_carvers");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
chunkPos.x(), chunkPos.z(), "bukkit_nms_apply_carvers");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_apply_carvers");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||||
|
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||||
BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(
|
BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(
|
||||||
ichunkaccess.getPos(),
|
chunkPos,
|
||||||
"bukkit_nms_chunk_pipeline");
|
"bukkit_nms_chunk_pipeline");
|
||||||
GenerationSessionLease lease;
|
GenerationHistoryRuntimeRouter.RuntimeRoute route;
|
||||||
try {
|
try {
|
||||||
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
|
route = openHistoryRoute(chunkPos.x(), chunkPos.z(), "bukkit_nms_chunk_pipeline");
|
||||||
} catch (RuntimeException | Error failure) {
|
} catch (RuntimeException | Error failure) {
|
||||||
stage.close();
|
stage.close();
|
||||||
throw failure;
|
throw failure;
|
||||||
}
|
}
|
||||||
|
GenerationSessionLease lease;
|
||||||
try {
|
try {
|
||||||
CompletableFuture<ChunkAccess> pipeline = delegate
|
lease = requireGenerationLease("bukkit_nms_chunk_pipeline");
|
||||||
.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)
|
} catch (RuntimeException | Error failure) {
|
||||||
|
closeHistoryRoute(route, failure);
|
||||||
|
stage.close();
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
CompletableFuture<ChunkAccess> delegatePipeline;
|
||||||
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
delegatePipeline = delegate.fillFromNoise(
|
||||||
|
blender, randomstate, structuremanager, ichunkaccess);
|
||||||
|
}
|
||||||
|
CompletableFuture<ChunkAccess> pipeline = delegatePipeline
|
||||||
.thenApply(filled -> {
|
.thenApply(filled -> {
|
||||||
try (IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope =
|
||||||
|
openHistoryRuntimeScope(route);
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
primeWorldgenHeightmaps(filled);
|
primeWorldgenHeightmaps(filled);
|
||||||
return filled;
|
return filled;
|
||||||
}
|
}
|
||||||
@@ -691,24 +773,78 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
|
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
|
||||||
pipeline.whenComplete((filled, failure) -> {
|
pipeline.whenComplete((filled, failure) -> {
|
||||||
boolean cancelled = isCancellationFailure(failure);
|
boolean cancelled = isCancellationFailure(failure);
|
||||||
lease.close();
|
Throwable completionFailure = closeNoisePipelineResources(
|
||||||
stage.close();
|
failure, lease, route, stage);
|
||||||
if (failure == null) {
|
if (completionFailure == null) {
|
||||||
completion.complete(filled);
|
completion.complete(filled);
|
||||||
} else if (cancelled) {
|
} else if (cancelled) {
|
||||||
completion.cancel(false);
|
completion.cancel(false);
|
||||||
} else {
|
} else {
|
||||||
completion.completeExceptionally(failure);
|
completion.completeExceptionally(completionFailure);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return completion;
|
return completion;
|
||||||
} catch (RuntimeException | Error failure) {
|
} catch (RuntimeException | Error failure) {
|
||||||
lease.close();
|
lease.close();
|
||||||
|
closeHistoryRoute(route, failure);
|
||||||
stage.close();
|
stage.close();
|
||||||
throw failure;
|
throw failure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Throwable closeNoisePipelineResources(
|
||||||
|
Throwable failure,
|
||||||
|
GenerationSessionLease lease,
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||||
|
BukkitChunkGenerator.GenerationStagePermit stage
|
||||||
|
) {
|
||||||
|
Throwable result = failure;
|
||||||
|
try {
|
||||||
|
lease.close();
|
||||||
|
} catch (Throwable closeFailure) {
|
||||||
|
result = appendFailure(result, closeFailure);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (route != null) {
|
||||||
|
route.close();
|
||||||
|
}
|
||||||
|
} catch (Throwable closeFailure) {
|
||||||
|
result = appendFailure(result, closeFailure);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
stage.close();
|
||||||
|
} catch (Throwable closeFailure) {
|
||||||
|
result = appendFailure(result, closeFailure);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void closeHistoryRoute(
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||||
|
Throwable failure
|
||||||
|
) {
|
||||||
|
if (route == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
route.close();
|
||||||
|
} catch (Throwable closeFailure) {
|
||||||
|
if (failure != closeFailure) {
|
||||||
|
failure.addSuppressed(closeFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Throwable appendFailure(Throwable failure, Throwable closeFailure) {
|
||||||
|
if (failure == null) {
|
||||||
|
return closeFailure;
|
||||||
|
}
|
||||||
|
if (failure != closeFailure) {
|
||||||
|
failure.addSuppressed(closeFailure);
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isCancellationFailure(Throwable failure) {
|
private static boolean isCancellationFailure(Throwable failure) {
|
||||||
Throwable current = failure;
|
Throwable current = failure;
|
||||||
while (current instanceof CompletionException && current.getCause() != null) {
|
while (current instanceof CompletionException && current.getCause() != null) {
|
||||||
@@ -739,6 +875,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||||
|
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||||
|
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns");
|
||||||
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_mob_spawns");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WeightedList<MobSpawnSettings.SpawnerData> getMobsAtWithActiveRuntime(
|
||||||
|
Holder<Biome> holder,
|
||||||
|
StructureManager structuremanager,
|
||||||
|
MobCategory enumcreaturetype,
|
||||||
|
BlockPos blockposition
|
||||||
|
) {
|
||||||
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
|
||||||
if (vanillaSpawnBiome == null) {
|
if (vanillaSpawnBiome == null) {
|
||||||
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
||||||
@@ -759,18 +909,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
return explicitSpawns;
|
return explicitSpawns;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisDimension spawnDimension = engine.getDimension();
|
|
||||||
int spawnRuntimeId = engine.getCacheID();
|
int spawnRuntimeId = engine.getCacheID();
|
||||||
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
|
SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), enumcreaturetype);
|
||||||
synchronized (mergedSpawnTables) {
|
|
||||||
if (spawnTableDimension != spawnDimension || spawnTableRuntimeId != spawnRuntimeId) {
|
|
||||||
mergedSpawnTables.clear();
|
|
||||||
spawnTableDimension = spawnDimension;
|
|
||||||
spawnTableRuntimeId = spawnRuntimeId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpawnTableKey key = new SpawnTableKey(holder.value(), enumcreaturetype);
|
|
||||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
|
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,19 +926,26 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
||||||
|
ChunkPos chunkPos = ichunkaccess.getPos();
|
||||||
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration");
|
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_biome_decoration");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
chunkPos.x(), chunkPos.z(), "bukkit_nms_biome_decoration");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
|
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)) {
|
||||||
// feature-order cycle is reported once and degraded to features-off.
|
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
|
||||||
importedFeatures.prepare(generatoraccessseed);
|
// feature-order cycle is reported once and degraded to features-off.
|
||||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
importedFeatures.prepare(generatoraccessseed);
|
||||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||||
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
|
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||||
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
|
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
|
||||||
// dimension set importedFeatures.enabled.
|
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
|
||||||
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
|
// dimension set importedFeatures.enabled.
|
||||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
|
||||||
|
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||||
|
}
|
||||||
|
claimGeneratedSemantics(route, "bukkit_nms_biome_decoration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,6 +997,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||||
for (StructureStart start : starts) {
|
for (StructureStart start : starts) {
|
||||||
|
BoundingBox footprint = start.getBoundingBox();
|
||||||
|
if (!engine.getComplex().allowsNewGenerationFootprint(
|
||||||
|
footprint.minX(),
|
||||||
|
footprint.minZ(),
|
||||||
|
footprint.maxX(),
|
||||||
|
footprint.maxZ())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
NativeStructureOwnershipRecord ownership =
|
NativeStructureOwnershipRecord ownership =
|
||||||
NativeStructureOwnershipRecovery.resolve(
|
NativeStructureOwnershipRecovery.resolve(
|
||||||
engine, world.getLevel(), structureId, structure, start);
|
engine, world.getLevel(), structureId, structure, start);
|
||||||
@@ -1001,13 +1156,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
|
||||||
|
int runtimeId = engine.getCacheID();
|
||||||
StructureStepCache cached = structureStepCache;
|
StructureStepCache cached = structureStepCache;
|
||||||
if (cached != null && cached.registry() == registry) {
|
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
|
||||||
return cached.structures();
|
return cached.structures();
|
||||||
}
|
}
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
cached = structureStepCache;
|
cached = structureStepCache;
|
||||||
if (cached != null && cached.registry() == registry) {
|
if (cached != null && cached.runtimeId() == runtimeId && cached.registry() == registry) {
|
||||||
return cached.structures();
|
return cached.structures();
|
||||||
}
|
}
|
||||||
int steps = GenerationStep.Decoration.values().length;
|
int steps = GenerationStep.Decoration.values().length;
|
||||||
@@ -1022,7 +1178,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
grouped.set(step, List.copyOf(grouped.get(step)));
|
grouped.set(step, List.copyOf(grouped.get(step)));
|
||||||
}
|
}
|
||||||
List<List<Structure>> resolved = List.copyOf(grouped);
|
List<List<Structure>> resolved = List.copyOf(grouped);
|
||||||
structureStepCache = new StructureStepCache(registry, resolved);
|
structureStepCache = new StructureStepCache(runtimeId, registry, resolved);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1054,7 +1210,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
|
ChunkPos chunkPos = chunkAccess.getPos();
|
||||||
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
|
chunkPos.getMinBlockX(), chunkPos.getMinBlockZ(), "bukkit_nms_heightmaps");
|
||||||
|
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_heightmaps");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||||
BlockPos blockPos = sectionPos.origin();
|
BlockPos blockPos = sectionPos.origin();
|
||||||
@@ -1085,22 +1244,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||||
ChunkPos center = region.getCenter();
|
ChunkPos center = region.getCenter();
|
||||||
Holder<Biome> visibleBiome;
|
try (BukkitChunkGenerator.GenerationStagePermit stage = requireGenerationStage("bukkit_nms_spawn_original_mobs");
|
||||||
if (engine.getDimensionStackContext() == null) {
|
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
center.x(), center.z(), "bukkit_nms_spawn_original_mobs");
|
||||||
} else {
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
|
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_spawn_original_mobs");
|
||||||
center.getMinBlockX() + 8,
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
center.getMinBlockZ() + 8);
|
if (!allowsRoutedDiscreteGeneration(region.getChunk(center.x(), center.z()), ChunkStatus.SPAWN)) {
|
||||||
if (visibleBiome == null) {
|
return;
|
||||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
|
||||||
}
|
}
|
||||||
|
Holder<Biome> visibleBiome;
|
||||||
|
if (engine.getDimensionStackContext() == null) {
|
||||||
|
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||||
|
} else {
|
||||||
|
visibleBiome = customBiomeSource.getVisibleSurfaceBiome(
|
||||||
|
center.getMinBlockX() + 8,
|
||||||
|
center.getMinBlockZ() + 8);
|
||||||
|
if (visibleBiome == null) {
|
||||||
|
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
|
||||||
|
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
|
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||||
|
NaturalSpawner.spawnMobsForChunkGeneration(
|
||||||
|
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
|
||||||
}
|
}
|
||||||
Holder<Biome> vanillaBiome = customBiomeSource.getVanillaSpawnBiome(visibleBiome);
|
}
|
||||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
|
||||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
private boolean allowsRoutedDiscreteGeneration(ChunkAccess chunk, ChunkStatus stage) {
|
||||||
NaturalSpawner.spawnMobsForChunkGeneration(
|
if (chunk.getPersistedStatus().isOrAfter(stage)) {
|
||||||
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
|
return false;
|
||||||
|
}
|
||||||
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
return engine.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
private static WeightedList<MobSpawnSettings.SpawnerData> mergeSpawnTables(
|
||||||
@@ -1133,7 +1310,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||||
|
i, j, "bukkit_nms_base_height");
|
||||||
|
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
boolean ignoreFluid = !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||||
int height = engine.getDimensionStackContext() == null
|
int height = engine.getDimensionStackContext() == null
|
||||||
@@ -1148,7 +1327,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||||
try (GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
|
||||||
|
i, j, "bukkit_nms_base_column");
|
||||||
|
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
|
||||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
boolean dimensionStack = engine.getDimensionStackContext() != null;
|
boolean dimensionStack = engine.getDimensionStackContext() != null;
|
||||||
int block = dimensionStack
|
int block = dimensionStack
|
||||||
@@ -1181,6 +1362,69 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
String operation
|
||||||
|
) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||||
|
if (router == null) {
|
||||||
|
if (platformGenerator != null && platformGenerator.getGenerationHistory() != null) {
|
||||||
|
throw new IllegalStateException("Iris " + operation
|
||||||
|
+ " has no generation-history runtime router.");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return router.openRoute(chunkX, chunkZ);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris " + operation
|
||||||
|
+ " could not route generation history for chunk "
|
||||||
|
+ chunkX + "," + chunkZ + ".", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||||
|
int blockX,
|
||||||
|
int blockZ,
|
||||||
|
String operation
|
||||||
|
) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return irisEngine.openGenerationHistoryCoordinateScope(blockX, blockZ);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris " + operation
|
||||||
|
+ " could not route generation history at "
|
||||||
|
+ blockX + "," + blockZ + ".", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope(
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route
|
||||||
|
) {
|
||||||
|
return route == null ? null : route.openRuntimeScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void claimGeneratedSemantics(
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||||
|
String operation
|
||||||
|
) {
|
||||||
|
if (route == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
route.claimGeneratedSemantics();
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris " + operation
|
||||||
|
+ " could not durably claim generated chunk semantics.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private BukkitChunkGenerator.GenerationStagePermit requireGenerationStage(String operation) {
|
private BukkitChunkGenerator.GenerationStagePermit requireGenerationStage(String operation) {
|
||||||
return platformGenerator == null
|
return platformGenerator == null
|
||||||
? BukkitChunkGenerator.GenerationStagePermit.noop()
|
? BukkitChunkGenerator.GenerationStagePermit.noop()
|
||||||
@@ -1252,13 +1496,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
private record SpawnTableKey(int runtimeId, Biome biome, MobCategory category) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set<String> keys) {
|
private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set<String> keys) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record StructureStepCache(Registry<Structure> registry, List<List<Structure>> structures) {
|
private record StructureStepCache(int runtimeId, Registry<Structure> registry,
|
||||||
|
List<List<Structure>> structures) {
|
||||||
}
|
}
|
||||||
|
|
||||||
record StudioStructureState(
|
record StudioStructureState(
|
||||||
|
|||||||
+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.Hunk;
|
||||||
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||||
import art.arcane.volmlib.util.json.JSONObject;
|
import art.arcane.volmlib.util.json.JSONObject;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
@@ -57,6 +58,9 @@ import it.unimi.dsi.fastutil.shorts.ShortList;
|
|||||||
import net.bytebuddy.ByteBuddy;
|
import net.bytebuddy.ByteBuddy;
|
||||||
import net.bytebuddy.agent.builder.AgentBuilder;
|
import net.bytebuddy.agent.builder.AgentBuilder;
|
||||||
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
||||||
|
import net.bytebuddy.description.type.TypeDescription;
|
||||||
|
import net.bytebuddy.dynamic.DynamicType;
|
||||||
|
import net.bytebuddy.utility.JavaModule;
|
||||||
import net.bytebuddy.asm.Advice;
|
import net.bytebuddy.asm.Advice;
|
||||||
import net.bytebuddy.matcher.ElementMatchers;
|
import net.bytebuddy.matcher.ElementMatchers;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
@@ -169,7 +173,6 @@ import java.util.List;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@@ -181,11 +184,22 @@ public class NMSBinding implements INMSBinding {
|
|||||||
private final AtomicBoolean injected = new AtomicBoolean();
|
private final AtomicBoolean injected = new AtomicBoolean();
|
||||||
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
|
||||||
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
private volatile ResettableClassFileTransformer serverLevelTransformer;
|
||||||
|
private volatile ResettableClassFileTransformer pluginClassLoaderTransformer;
|
||||||
|
private boolean pluginClassLoaderCloseDeferred;
|
||||||
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
||||||
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
||||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||||
private final AtomicCache<Method> byIdRef = new AtomicCache<>();
|
private final AtomicCache<Method> byIdRef = new AtomicCache<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlatformGenerationRegistry generationRegistry() {
|
||||||
|
return new NmsGenerationRegistry(
|
||||||
|
this::registry,
|
||||||
|
"bukkit-generation-registry-v1",
|
||||||
|
"bukkit-v26_2_R1-generated-registry-json-v1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static Object getFor(Class<?> type, Object source) {
|
private static Object getFor(Class<?> type, Object source) {
|
||||||
Object o = fieldFor(type, source);
|
Object o = fieldFor(type, source);
|
||||||
|
|
||||||
@@ -1544,6 +1558,23 @@ public class NMSBinding implements INMSBinding {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
IrisLogging.info("Injecting Bukkit");
|
IrisLogging.info("Injecting Bukkit");
|
||||||
|
Agent.requireClassLoaderCloseDeferral();
|
||||||
|
Class<?> loaderCloseType = getClass().getClassLoader().getClass()
|
||||||
|
.getMethod("close").getDeclaringClass();
|
||||||
|
PluginClassLoaderInjectionListener loaderListener = new PluginClassLoaderInjectionListener();
|
||||||
|
pluginClassLoaderTransformer = new AgentBuilder.Default()
|
||||||
|
.disableClassFormatChanges()
|
||||||
|
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||||
|
.with(AgentBuilder.RedefinitionStrategy.Listener.ErrorEscalating.FAIL_FAST)
|
||||||
|
.with(loaderListener)
|
||||||
|
.type(ElementMatchers.is(loaderCloseType))
|
||||||
|
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
|
||||||
|
builder.visit(Advice.to(PluginClassLoaderCloseAdvice.class)
|
||||||
|
.on(ElementMatchers.named("close")
|
||||||
|
.and(ElementMatchers.takesArguments(0))
|
||||||
|
.and(ElementMatchers.returns(void.class)))))
|
||||||
|
.installOn(Agent.getInstrumentation());
|
||||||
|
loaderListener.requireInstalled();
|
||||||
levelStorageAccessTransformer = new AgentBuilder.Default()
|
levelStorageAccessTransformer = new AgentBuilder.Default()
|
||||||
.disableClassFormatChanges()
|
.disableClassFormatChanges()
|
||||||
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
||||||
@@ -1579,18 +1610,22 @@ public class NMSBinding implements INMSBinding {
|
|||||||
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
IrisLogging.reportError(C.RED + "Failed to inject Bukkit", e);
|
||||||
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
|
||||||
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
|
||||||
|
ResettableClassFileTransformer partialPluginClassLoader = pluginClassLoaderTransformer;
|
||||||
serverLevelTransformer = null;
|
serverLevelTransformer = null;
|
||||||
levelStorageAccessTransformer = null;
|
levelStorageAccessTransformer = null;
|
||||||
|
pluginClassLoaderTransformer = null;
|
||||||
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
|
||||||
partialServerLevel,
|
partialServerLevel,
|
||||||
partialStorageAccess
|
partialStorageAccess,
|
||||||
|
partialPluginClassLoader
|
||||||
}) {
|
}) {
|
||||||
if (partial == null) {
|
if (partial == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
|
||||||
} catch (Throwable ignored) {
|
} catch (Throwable cleanupFailure) {
|
||||||
|
IrisLogging.reportError("Failed to remove partial Bukkit lifecycle injection", cleanupFailure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -1637,16 +1672,52 @@ public class NMSBinding implements INMSBinding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
|
public boolean isServerStopping() {
|
||||||
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getHandle().getServer();
|
return ((CraftServer) Bukkit.getServer()).getServer().hasStopped();
|
||||||
return ServerShutdownBoundary.await(
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServerShutdownBoundary createServerShutdownBoundary() {
|
||||||
|
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
|
||||||
|
return new ServerShutdownBoundary(
|
||||||
() -> server.hasFullyShutdown,
|
() -> server.hasFullyShutdown,
|
||||||
server.getRunningThread(),
|
server.getRunningThread()
|
||||||
timeout,
|
|
||||||
unit
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deferPluginClassLoaderClose() {
|
||||||
|
synchronized (injected) {
|
||||||
|
if (pluginClassLoaderTransformer == null || pluginClassLoaderCloseDeferred) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Agent.retainClassLoader(getClass().getClassLoader());
|
||||||
|
pluginClassLoaderCloseDeferred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releasePluginClassLoaderClose() {
|
||||||
|
ResettableClassFileTransformer transformer;
|
||||||
|
boolean releaseLoader;
|
||||||
|
synchronized (injected) {
|
||||||
|
transformer = pluginClassLoaderTransformer;
|
||||||
|
pluginClassLoaderTransformer = null;
|
||||||
|
releaseLoader = pluginClassLoaderCloseDeferred;
|
||||||
|
pluginClassLoaderCloseDeferred = false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (transformer != null
|
||||||
|
&& !transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)) {
|
||||||
|
throw new IllegalStateException("Failed to remove Iris plugin class loader lifecycle injection");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (releaseLoader) {
|
||||||
|
Agent.releaseClassLoader(getClass().getClassLoader());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
||||||
KMap<Material, List<BlockProperty>> states = new KMap<>();
|
KMap<Material, List<BlockProperty>> states = new KMap<>();
|
||||||
@@ -1691,6 +1762,39 @@ public class NMSBinding implements INMSBinding {
|
|||||||
return new FlatLevelSource(settings);
|
return new FlatLevelSource(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class PluginClassLoaderInjectionListener extends AgentBuilder.Listener.Adapter {
|
||||||
|
private volatile boolean transformed;
|
||||||
|
private volatile Throwable failure;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransformation(TypeDescription type, ClassLoader loader, JavaModule module,
|
||||||
|
boolean loaded, DynamicType dynamicType) {
|
||||||
|
transformed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String typeName, ClassLoader loader, JavaModule module,
|
||||||
|
boolean loaded, Throwable throwable) {
|
||||||
|
failure = throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireInstalled() {
|
||||||
|
if (!transformed || failure != null) {
|
||||||
|
throw new IllegalStateException("Iris plugin class loader lifecycle injection failed", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PluginClassLoaderCloseAdvice {
|
||||||
|
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||||
|
static boolean enter(@Advice.This ClassLoader loader) throws ReflectiveOperationException {
|
||||||
|
Class<?> installer = Class.forName(
|
||||||
|
"art.arcane.iris.util.project.agent.Installer", true, ClassLoader.getSystemClassLoader());
|
||||||
|
return Boolean.TRUE.equals(installer.getMethod("deferClassLoaderClose", ClassLoader.class)
|
||||||
|
.invoke(null, loader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class ChunkAccessAdvice {
|
private static class ChunkAccessAdvice {
|
||||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||||
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||||
|
|||||||
+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\")"));
|
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_spawn_biome\")"));
|
||||||
assertFalse(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
|
assertFalse(source.contains("Iris spawn biome lookup was rejected during an engine transition"));
|
||||||
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
|
assertFalse(source.contains("Iris spawn biome lookup has no active engine runtime"));
|
||||||
assertTrue(source.contains("vanillaSpawnBiomes.get(biome.value())"));
|
assertTrue(source.contains("runtimeBiomeState().vanillaSpawnBiomes().get(biome.value())"));
|
||||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_structure_biome\")"));
|
assertTrue(source.contains("requireGenerationLease(\n \"bukkit_structure_biome\""));
|
||||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
|
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_biomes_within\")"));
|
||||||
assertTrue(source.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
|
assertTrue(source.contains("requireGenerationLease(\n \"bukkit_visible_biome\""));
|
||||||
assertTrue(source.contains("if (engine.isClosed())"));
|
assertTrue(source.contains("if (engine.isClosed())"));
|
||||||
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
|
assertTrue(source.contains("engine.isClosing() || e.isExpectedTeardown()"));
|
||||||
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||||
@@ -140,7 +140,7 @@ public class CustomBiomeSourceStructureContractTest {
|
|||||||
assertTrue(lookupEnd > lookupStart);
|
assertTrue(lookupEnd > lookupStart);
|
||||||
String lookup = source.substring(lookupStart, lookupEnd);
|
String lookup = source.substring(lookupStart, lookupEnd);
|
||||||
|
|
||||||
assertTrue(lookup.contains("tryAcquireGenerationLease(\"bukkit_visible_biome\")"));
|
assertTrue(lookup.contains("requireGenerationLease(\n \"bukkit_visible_biome\""));
|
||||||
assertTrue(lookup.contains("prepareVisibleBiomeBatch()"));
|
assertTrue(lookup.contains("prepareVisibleBiomeBatch()"));
|
||||||
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
|
assertTrue(lookup.contains("getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, sampler)"));
|
||||||
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()")
|
assertTrue(lookup.indexOf("prepareVisibleBiomeBatch()")
|
||||||
|
|||||||
+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,
|
assertBefore(productionFill,
|
||||||
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
|
"BukkitChunkGenerator.GenerationStagePermit stage = requireNoiseGenerationStage(",
|
||||||
|
"GenerationHistoryRuntimeRouter.RuntimeRoute route");
|
||||||
|
assertBefore(productionFill,
|
||||||
|
"GenerationHistoryRuntimeRouter.RuntimeRoute route",
|
||||||
"GenerationSessionLease lease");
|
"GenerationSessionLease lease");
|
||||||
assertBefore(productionFill,
|
assertBefore(productionFill,
|
||||||
"requireNoiseGenerationStage(",
|
"requireNoiseGenerationStage(",
|
||||||
"\"bukkit_nms_chunk_pipeline\")");
|
"\"bukkit_nms_chunk_pipeline\")");
|
||||||
assertBefore(productionFill,
|
assertBefore(productionFill,
|
||||||
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")",
|
"lease = requireGenerationLease(\"bukkit_nms_chunk_pipeline\")",
|
||||||
".fillFromNoise(blender, randomstate, structuremanager, ichunkaccess)");
|
"delegate.fillFromNoise(");
|
||||||
assertTrue(productionFill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
|
assertTrue(productionFill.contains("IrisContext.open(engine, lease.sessionId(), null)"));
|
||||||
|
assertTrue(productionFill.contains("openHistoryRuntimeScope(route)"));
|
||||||
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
|
assertBefore(productionFill, "primeWorldgenHeightmaps(filled)", "pipeline.whenComplete(");
|
||||||
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
|
assertTrue(productionFill.contains("CompletableFuture<ChunkAccess> completion = new CompletableFuture<>()"));
|
||||||
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "lease.close();");
|
assertBefore(completion, "boolean cancelled = isCancellationFailure(failure);", "closeNoisePipelineResources(");
|
||||||
assertBefore(completion, "lease.close();", "stage.close();");
|
assertBefore(completion, "closeNoisePipelineResources(", "completion.complete(filled)");
|
||||||
assertBefore(completion, "stage.close();", "completion.complete(filled)");
|
assertBefore(completion, "closeNoisePipelineResources(", "completion.cancel(false)");
|
||||||
assertBefore(completion, "stage.close();", "completion.cancel(false)");
|
assertBefore(completion, "closeNoisePipelineResources(", "completion.completeExceptionally(completionFailure)");
|
||||||
assertBefore(completion, "stage.close();", "completion.completeExceptionally(failure)");
|
|
||||||
assertTrue(completion.contains("else if (cancelled)"));
|
assertTrue(completion.contains("else if (cancelled)"));
|
||||||
assertFalse(completion.contains("pipeline.isCancelled()"));
|
assertFalse(completion.contains("pipeline.isCancelled()"));
|
||||||
assertFalse(completion.contains("finally"));
|
assertFalse(completion.contains("finally"));
|
||||||
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)"));
|
assertTrue(productionFill.contains("catch (RuntimeException | Error failure)"));
|
||||||
assertTrue(productionFill.contains("lease.close();\n stage.close();\n throw failure;"));
|
assertTrue(productionFill.contains("lease.close();\n closeHistoryRoute(route, failure);\n stage.close();\n throw failure;"));
|
||||||
assertTrue(productionFill.contains("return completion;"));
|
assertTrue(productionFill.contains("return completion;"));
|
||||||
assertFalse(productionFill.contains("return pipeline;"));
|
assertFalse(productionFill.contains("return pipeline;"));
|
||||||
assertFalse(productionFill.contains("pipeline.cancel("));
|
assertFalse(productionFill.contains("pipeline.cancel("));
|
||||||
@@ -422,6 +425,9 @@ public class IrisChunkGeneratorFailureContractTest {
|
|||||||
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
|
String structures = method(source, "public void createStructures(", "private void adjustGeneratedStructures");
|
||||||
String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
|
String references = method(source, "public void createReferences(", "public CompletableFuture<ChunkAccess> createBiomes");
|
||||||
String biomes = method(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface");
|
String biomes = method(source, "public CompletableFuture<ChunkAccess> createBiomes", "public void buildSurface");
|
||||||
|
String surface = method(source, "public void buildSurface(", "public void applyCarvers(");
|
||||||
|
String carvers = method(source, "public void applyCarvers(", "public CompletableFuture<ChunkAccess> fillFromNoise");
|
||||||
|
String mobs = method(source, "public void spawnOriginalMobs(", "private boolean allowsRoutedDiscreteGeneration(");
|
||||||
String noise = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise", "private static boolean isCancellationFailure");
|
String noise = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise", "private static boolean isCancellationFailure");
|
||||||
String decoration = method(source,
|
String decoration = method(source,
|
||||||
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
|
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
|
||||||
@@ -430,6 +436,9 @@ public class IrisChunkGeneratorFailureContractTest {
|
|||||||
assertStageBeforeLease(structures, "bukkit_nms_create_structures");
|
assertStageBeforeLease(structures, "bukkit_nms_create_structures");
|
||||||
assertStageBeforeLease(references, "bukkit_nms_create_references");
|
assertStageBeforeLease(references, "bukkit_nms_create_references");
|
||||||
assertStageBeforeLease(biomes, "bukkit_nms_create_biomes");
|
assertStageBeforeLease(biomes, "bukkit_nms_create_biomes");
|
||||||
|
assertStageBeforeLease(surface, "bukkit_nms_build_surface");
|
||||||
|
assertStageBeforeLease(carvers, "bukkit_nms_apply_carvers");
|
||||||
|
assertStageBeforeLease(mobs, "bukkit_nms_spawn_original_mobs");
|
||||||
assertBefore(noise,
|
assertBefore(noise,
|
||||||
"requireNoiseGenerationStage(",
|
"requireNoiseGenerationStage(",
|
||||||
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
|
"requireGenerationLease(\"bukkit_nms_chunk_pipeline\")");
|
||||||
@@ -437,7 +446,7 @@ public class IrisChunkGeneratorFailureContractTest {
|
|||||||
assertBefore(decoration,
|
assertBefore(decoration,
|
||||||
"requireGenerationLease(\"bukkit_nms_biome_decoration\")",
|
"requireGenerationLease(\"bukkit_nms_biome_decoration\")",
|
||||||
"importedFeatures.prepare(generatoraccessseed)");
|
"importedFeatures.prepare(generatoraccessseed)");
|
||||||
assertEquals(4, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
|
assertEquals(7, occurrences(source, "requireGenerationStage(\"bukkit_nms_"));
|
||||||
assertEquals(2, occurrences(source, "requireNoiseGenerationStage("));
|
assertEquals(2, occurrences(source, "requireNoiseGenerationStage("));
|
||||||
assertFalse(source.contains("GenerationSessionManager"));
|
assertFalse(source.contains("GenerationSessionManager"));
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-2
@@ -6,16 +6,48 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
|
||||||
public class NMSBindingShutdownBoundaryContractTest {
|
public class NMSBindingShutdownBoundaryContractTest {
|
||||||
|
@Test
|
||||||
|
public void shutdownClassificationUsesTheServerStopFlagInsteadOfPluginEnablement() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
|
||||||
|
String stopping = section(source, "public boolean isServerStopping()", "public ServerShutdownBoundary createServerShutdownBoundary");
|
||||||
|
|
||||||
|
assertTrue(stopping.contains("((CraftServer) Bukkit.getServer()).getServer().hasStopped()"));
|
||||||
|
assertFalse(stopping.contains("isEnabled()"));
|
||||||
|
assertFalse(stopping.contains("isRunning()"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void pluginLoaderGuardUsesSystemAgentIdentityAndIsRemovedAtRelease() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
|
||||||
|
String injection = section(source, "public boolean injectBukkit()", "public void ensureServerLevelInjection()");
|
||||||
|
String guard = section(source, "private static class PluginClassLoaderCloseAdvice", "private static class ChunkAccessAdvice");
|
||||||
|
String release = section(source, "public void releasePluginClassLoaderClose()", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
||||||
|
|
||||||
|
assertTrue(injection.contains("Agent.requireClassLoaderCloseDeferral()"));
|
||||||
|
assertTrue(injection.contains("getMethod(\"close\").getDeclaringClass()"));
|
||||||
|
assertTrue(injection.contains("Advice.to(PluginClassLoaderCloseAdvice.class)"));
|
||||||
|
assertTrue(guard.contains("skipOn = Advice.OnNonDefaultValue.class"));
|
||||||
|
assertTrue(guard.contains("ClassLoader.getSystemClassLoader()"));
|
||||||
|
assertTrue(guard.contains("getMethod(\"deferClassLoaderClose\", ClassLoader.class)"));
|
||||||
|
assertTrue(guard.contains(".invoke(null, loader)"));
|
||||||
|
assertFalse(guard.contains("Bukkit.getPluginManager()"));
|
||||||
|
assertTrue(release.indexOf("transformer.reset(") > release.indexOf("pluginClassLoaderCloseDeferred = false"));
|
||||||
|
assertTrue(release.indexOf("Agent.releaseClassLoader(") > release.indexOf("transformer.reset("));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
|
public void shutdownBoundaryUsesPaperFullyShutdownStateAndServerThread() throws Exception {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource"))).replace("\r\n", "\n");
|
||||||
String boundary = section(source, "public boolean awaitServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
String boundary = section(source, "public ServerShutdownBoundary createServerShutdownBoundary", "public KMap<Material, List<BlockProperty>> getBlockProperties");
|
||||||
|
|
||||||
assertTrue(boundary.contains("ServerShutdownBoundary.await("));
|
assertTrue(boundary.contains("new ServerShutdownBoundary("));
|
||||||
assertTrue(boundary.contains("server.hasFullyShutdown"));
|
assertTrue(boundary.contains("server.hasFullyShutdown"));
|
||||||
assertTrue(boundary.contains("server.getRunningThread()"));
|
assertTrue(boundary.contains("server.getRunningThread()"));
|
||||||
|
assertTrue(boundary.contains("((CraftServer) Bukkit.getServer()).getServer()"));
|
||||||
|
assertFalse(boundary.contains("getHandle()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String section(String source, String startMarker, String endMarker) {
|
private static String section(String source, String startMarker, String endMarker) {
|
||||||
|
|||||||
+58
@@ -19,6 +19,7 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@@ -80,6 +81,30 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
assertEquals(coldResolutions, resolver.resolutions());
|
assertEquals(coldResolutions, resolver.resolutions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void runtimeBucketsStayIsolatedAndRetirementEvictsOnlyTheirEntries() {
|
||||||
|
AtomicInteger runtimeId = new AtomicInteger(1);
|
||||||
|
Engine engine = runtimeEngine(runtimeId);
|
||||||
|
CountingResolver resolver = new CountingResolver(0, 0);
|
||||||
|
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
|
||||||
|
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(289, resolver.resolutions());
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(289, resolver.resolutions());
|
||||||
|
|
||||||
|
runtimeId.set(2);
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(578, resolver.resolutions());
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(578, resolver.resolutions());
|
||||||
|
|
||||||
|
index.evictRuntime(1);
|
||||||
|
runtimeId.set(1);
|
||||||
|
index.resolve(engine, 0, 0, 15, 15);
|
||||||
|
assertEquals(867, resolver.resolutions());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
|
public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() {
|
||||||
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0));
|
||||||
@@ -188,12 +213,45 @@ public class NativeStructureVolumeIndexTest {
|
|||||||
assertTrue(source.contains("isStructureChunk("));
|
assertTrue(source.contains("isStructureChunk("));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void originResolutionRoutesAndCachesByScopedRuntime() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
|
||||||
|
|
||||||
|
assertTrue(source.contains("openHistoryCoordinateScope(engine, chunkX, chunkZ)"));
|
||||||
|
assertTrue(source.contains("new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ))"));
|
||||||
|
assertTrue(source.contains("addGenerationRuntimeRetirementListener(retirementListener)"));
|
||||||
|
assertTrue(source.contains("originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
|
||||||
|
assertTrue(source.contains("queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)"));
|
||||||
|
}
|
||||||
|
|
||||||
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) {
|
||||||
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty()));
|
||||||
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
|
SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z);
|
||||||
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
|
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Engine runtimeEngine(AtomicInteger runtimeId) {
|
||||||
|
return (Engine) Proxy.newProxyInstance(
|
||||||
|
Engine.class.getClassLoader(),
|
||||||
|
new Class<?>[]{Engine.class},
|
||||||
|
(proxy, method, arguments) -> {
|
||||||
|
if (method.getName().equals("getCacheID")) {
|
||||||
|
return runtimeId.get();
|
||||||
|
}
|
||||||
|
if (method.getName().equals("hashCode")) {
|
||||||
|
return System.identityHashCode(proxy);
|
||||||
|
}
|
||||||
|
if (method.getName().equals("equals")) {
|
||||||
|
return proxy == arguments[0];
|
||||||
|
}
|
||||||
|
if (method.getName().equals("toString")) {
|
||||||
|
return "runtime-engine-" + runtimeId.get();
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static int findDisjointWindow(int chunkX, int chunkZ) {
|
private static int findDisjointWindow(int chunkX, int chunkZ) {
|
||||||
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
|
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
|
||||||
for (int candidate = 64; candidate < 16_384; candidate += 17) {
|
for (int candidate = 64; candidate < 16_384; candidate += 17) {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
|||||||
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
||||||
import art.arcane.iris.core.link.MultiverseCoreLink;
|
import art.arcane.iris.core.link.MultiverseCoreLink;
|
||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
|
import art.arcane.iris.core.nms.ServerShutdownBoundary;
|
||||||
import art.arcane.iris.core.gui.BukkitGuiHost;
|
import art.arcane.iris.core.gui.BukkitGuiHost;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||||
import art.arcane.iris.core.service.BoardSVC;
|
import art.arcane.iris.core.service.BoardSVC;
|
||||||
@@ -139,6 +140,8 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.PrintStream;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -160,6 +163,7 @@ import java.util.regex.Pattern;
|
|||||||
@SuppressWarnings("CanBeFinal")
|
@SuppressWarnings("CanBeFinal")
|
||||||
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||||
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L;
|
private static final long SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS = 300L;
|
||||||
|
private static final PrintStream SHUTDOWN_ERRORS = new PrintStream(new FileOutputStream(FileDescriptor.err), true);
|
||||||
private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
|
private static final long SERVER_STOP_PREGEN_TIMEOUT_MILLIS = 30000L;
|
||||||
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
|
private static final Queue<Runnable> syncJobs = new ShurikenQueue<>();
|
||||||
|
|
||||||
@@ -190,6 +194,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||||
|
private volatile boolean generatorDrainCompleted;
|
||||||
private volatile PlaceholderRegistration papiRegistration;
|
private volatile PlaceholderRegistration papiRegistration;
|
||||||
private volatile IrisPapiListener papiListener;
|
private volatile IrisPapiListener papiListener;
|
||||||
private volatile IrisPapiState papiState;
|
private volatile IrisPapiState papiState;
|
||||||
@@ -206,6 +212,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private BukkitDebugDump debugDump;
|
private BukkitDebugDump debugDump;
|
||||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||||
private volatile Thread serverLifecycleThread;
|
private volatile Thread serverLifecycleThread;
|
||||||
|
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||||
|
|
||||||
public static VolmitSender getSender() {
|
public static VolmitSender getSender() {
|
||||||
if (sender == null) {
|
if (sender == null) {
|
||||||
@@ -427,6 +434,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
if (e == null) {
|
if (e == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (instance != null && instance.serverStopTeardownDeferred.get()) {
|
||||||
|
e.printStackTrace(SHUTDOWN_ERRORS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
boolean debug = false;
|
boolean debug = false;
|
||||||
if (instance != null) {
|
if (instance != null) {
|
||||||
@@ -466,6 +477,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
public static void reportError(String context, Throwable e) {
|
public static void reportError(String context, Throwable e) {
|
||||||
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
|
Throwable error = e == null ? new IllegalStateException("Unknown Iris failure") : e;
|
||||||
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
|
String message = context == null || context.isBlank() ? "Unhandled Iris failure." : context;
|
||||||
|
if (instance != null && instance.serverStopTeardownDeferred.get()) {
|
||||||
|
SHUTDOWN_ERRORS.println("[Iris] " + message);
|
||||||
|
error.printStackTrace(SHUTDOWN_ERRORS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (instance != null) {
|
if (instance != null) {
|
||||||
@@ -603,6 +619,8 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
sharedRuntimeClosed.set(false);
|
sharedRuntimeClosed.set(false);
|
||||||
startupBoundaryRestart.set(false);
|
startupBoundaryRestart.set(false);
|
||||||
terminalCleanupCompleted.set(false);
|
terminalCleanupCompleted.set(false);
|
||||||
|
runtimeTeardownFailed.set(false);
|
||||||
|
generatorDrainCompleted = false;
|
||||||
deferredShutdownGenerators.clear();
|
deferredShutdownGenerators.clear();
|
||||||
MultiBurst.burst.reopen();
|
MultiBurst.burst.reopen();
|
||||||
MultiBurst.ioBurst.reopen();
|
MultiBurst.ioBurst.reopen();
|
||||||
@@ -742,6 +760,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
public void addShutdownHook() {
|
public void addShutdownHook() {
|
||||||
removeShutdownHook();
|
removeShutdownHook();
|
||||||
serverLifecycleThread = Thread.currentThread();
|
serverLifecycleThread = Thread.currentThread();
|
||||||
|
serverShutdownBoundary = INMS.isBound() ? INMS.get().createServerShutdownBoundary() : null;
|
||||||
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
shutdownHook = new Thread(this::runShutdownHook, "Iris-ShutdownHook");
|
||||||
try {
|
try {
|
||||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||||
@@ -963,6 +982,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
drainWorldGenerators(reason, timeoutSeconds);
|
drainWorldGenerators(reason, timeoutSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (services != null && servicesDisabled.compareAndSet(false, true)) {
|
if (services != null && servicesDisabled.compareAndSet(false, true)) {
|
||||||
// Only services whose onEnable actually completed; disabling a service that
|
// Only services whose onEnable actually completed; disabling a service that
|
||||||
// never initialized runs teardown against uninitialized state.
|
// never initialized runs teardown against uninitialized state.
|
||||||
@@ -970,6 +993,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
try {
|
try {
|
||||||
service.onDisable();
|
service.onDisable();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
runtimeTeardownFailed.set(true);
|
||||||
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
|
Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -979,8 +1003,18 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
J.attempt(MultiBurst.burst::close);
|
try {
|
||||||
J.attempt(MultiBurst.ioBurst::close);
|
MultiBurst.burst.close();
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
runtimeTeardownFailed.set(true);
|
||||||
|
Iris.reportError("Failed to close Iris generation workers.", failure);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
MultiBurst.ioBurst.close();
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
runtimeTeardownFailed.set(true);
|
||||||
|
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
||||||
|
}
|
||||||
clearQueues();
|
clearQueues();
|
||||||
IrisServices.clear();
|
IrisServices.clear();
|
||||||
}
|
}
|
||||||
@@ -988,6 +1022,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
private void quiesceRuntimeForServerShutdown(String reason) {
|
private void quiesceRuntimeForServerShutdown(String reason) {
|
||||||
serverStopTeardownDeferred.set(true);
|
serverStopTeardownDeferred.set(true);
|
||||||
|
INMS.get().deferPluginClassLoaderClose();
|
||||||
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
JigsawStudioService jigsawStudioService = IrisServices.getOrNull(JigsawStudioService.class);
|
||||||
if (jigsawStudioService != null) {
|
if (jigsawStudioService != null) {
|
||||||
try {
|
try {
|
||||||
@@ -1060,29 +1095,35 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!awaitServerShutdownBoundary()) {
|
if (!awaitServerShutdownBoundary()) {
|
||||||
Iris.warn("Iris skipped JVM-hook runtime teardown because Paper did not reach its post-world-close boundary.");
|
SHUTDOWN_ERRORS.println("[Iris] Paper did not reach its post-world-close boundary; retaining the runtime and plugin class loader until JVM exit.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
|
finishDeferredRuntimeTeardown("shutdown-hook", 30L);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean awaitServerShutdownBoundary() {
|
private boolean awaitServerShutdownBoundary() {
|
||||||
if (!INMS.isBound()) {
|
ServerShutdownBoundary boundary = serverShutdownBoundary;
|
||||||
|
if (boundary == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return INMS.get().awaitServerShutdownBoundary(
|
return boundary.await(
|
||||||
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
|
SERVER_SHUTDOWN_BOUNDARY_TIMEOUT_SECONDS,
|
||||||
TimeUnit.SECONDS
|
TimeUnit.SECONDS
|
||||||
);
|
);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.reportError("Failed to await Paper's post-world-close shutdown boundary.", e);
|
SHUTDOWN_ERRORS.println("[Iris] Failed to await Paper's post-world-close shutdown boundary.");
|
||||||
|
e.printStackTrace(SHUTDOWN_ERRORS);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
|
private void finishDeferredRuntimeTeardown(String reason, long timeoutSeconds) {
|
||||||
teardownRuntime(reason, timeoutSeconds);
|
teardownRuntime(reason, timeoutSeconds);
|
||||||
|
if (serverStopTeardownDeferred.get() && !generatorDrainCompleted) {
|
||||||
|
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup is incomplete; retaining services, pools and the plugin class loader until JVM exit.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
finishTerminalCleanup();
|
finishTerminalCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,12 +1135,36 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
try {
|
try {
|
||||||
runPostShutdown();
|
runPostShutdown();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
runtimeTeardownFailed.set(true);
|
||||||
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
|
||||||
|
runtimeTeardownFailed.set(true);
|
||||||
|
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
|
||||||
|
}
|
||||||
IrisPlatforms.unbind();
|
IrisPlatforms.unbind();
|
||||||
|
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
|
||||||
|
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
INMS.get().releasePluginClassLoaderClose();
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
||||||
|
failure.printStackTrace(SHUTDOWN_ERRORS);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasActiveShutdownResources() {
|
||||||
|
if (!MultiBurst.burst.isTerminated() || !MultiBurst.ioBurst.isTerminated()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return services != null
|
||||||
|
&& services.get(PreservationSVC.class) instanceof PreservationSVC preservation
|
||||||
|
&& preservation.hasActiveResources();
|
||||||
|
}
|
||||||
|
|
||||||
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
private void drainWorldGenerators(String reason, long timeoutSeconds) {
|
||||||
List<World> irisWorlds = new ArrayList<>();
|
List<World> irisWorlds = new ArrayList<>();
|
||||||
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
List<PlatformChunkGenerator> generators = new ArrayList<>();
|
||||||
@@ -1115,6 +1180,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (generators.isEmpty()) {
|
if (generators.isEmpty()) {
|
||||||
|
generatorDrainCompleted = true;
|
||||||
Iris.debug("No Iris worlds to freeze.");
|
Iris.debug("No Iris worlds to freeze.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1139,12 +1205,15 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
try {
|
try {
|
||||||
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
|
CompletableFuture.allOf(closes.toArray(new CompletableFuture<?>[0]))
|
||||||
.get(timeoutSeconds, TimeUnit.SECONDS);
|
.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
Iris.debug("All Iris chunk generators parked. Safe to unload.");
|
generatorDrainCompleted = closes.size() == generators.size();
|
||||||
|
if (generatorDrainCompleted) {
|
||||||
|
Iris.debug("All Iris chunk generators parked. Safe to unload.");
|
||||||
|
}
|
||||||
} catch (TimeoutException e) {
|
} catch (TimeoutException e) {
|
||||||
Iris.warn("Iris generator drain timed out after " + timeoutSeconds + "s; unload proceeding anyway.");
|
Iris.reportError("Iris generator drain timed out after " + timeoutSeconds + "s.", e);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
Iris.warn("Iris generator drain interrupted; unload proceeding.");
|
Iris.reportError("Iris generator drain was interrupted.", e);
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
Iris.reportError(e.getCause() == null ? e : e.getCause());
|
Iris.reportError(e.getCause() == null ? e : e.getCause());
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -765,13 +765,13 @@ public final class BukkitWorldReconciler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
|
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
|
||||||
File snapshotRoot = snapshotRoot(worldKey);
|
|
||||||
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
|
|
||||||
String configuredWorldName = configuredWorldName(worldKey);
|
String configuredWorldName = configuredWorldName(worldKey);
|
||||||
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
|
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
|
||||||
if (irisDimension == null) {
|
if (irisDimension == null) {
|
||||||
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
|
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
|
||||||
}
|
}
|
||||||
|
File snapshotRoot = snapshotRoot(worldKey);
|
||||||
|
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private File snapshotRoot(NamespacedKey worldKey) {
|
private File snapshotRoot(NamespacedKey worldKey) {
|
||||||
@@ -788,7 +788,7 @@ public final class BukkitWorldReconciler {
|
|||||||
throw new IllegalStateException("Iris world storage does not match the current platform layout for "
|
throw new IllegalStateException("Iris world storage does not match the current platform layout for "
|
||||||
+ worldKey + ".");
|
+ worldKey + ".");
|
||||||
}
|
}
|
||||||
return IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
|
return IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+154
-15
@@ -20,6 +20,7 @@ package art.arcane.iris.core;
|
|||||||
|
|
||||||
import art.arcane.iris.Iris;
|
import art.arcane.iris.Iris;
|
||||||
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
|
import art.arcane.iris.core.lifecycle.WorldLifecycleStaging;
|
||||||
|
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.pack.BrokenPackException;
|
import art.arcane.iris.core.pack.BrokenPackException;
|
||||||
import art.arcane.iris.core.pack.PackDownloader;
|
import art.arcane.iris.core.pack.PackDownloader;
|
||||||
@@ -32,6 +33,13 @@ import art.arcane.iris.spi.IrisLogging;
|
|||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryPaths;
|
||||||
|
import art.arcane.iris.engine.history.GenerationPackFingerprint;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
import art.arcane.iris.util.common.plugin.VolmitPlugin;
|
||||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||||
@@ -255,15 +263,21 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
File levelRoot = IrisWorldStorage.levelRoot();
|
File levelRoot = IrisWorldStorage.levelRoot();
|
||||||
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
|
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName(), levelRoot);
|
||||||
String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName());
|
String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName());
|
||||||
File pack = IrisWorldStorage.frozenDimensionRoot(
|
File dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
|
||||||
Bukkit.getWorldContainer(),
|
Bukkit.getWorldContainer(),
|
||||||
levelRoot,
|
levelRoot,
|
||||||
configuredWorldName,
|
configuredWorldName,
|
||||||
worldKey
|
worldKey
|
||||||
)
|
)
|
||||||
.map(IrisWorldStorage::requireFrozenPackRoot)
|
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id);
|
IrisDimension dimension = null;
|
||||||
|
if (dimensionRoot != null) {
|
||||||
|
long worldSeed = requireStoredWorldSeed(configuredWorldName, dimensionRoot);
|
||||||
|
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
|
||||||
|
File pack = requireActivePack(history);
|
||||||
|
requireSnapshotLoadable(pack);
|
||||||
|
dimension = requireHistoricalDimension(history, pack, id);
|
||||||
|
}
|
||||||
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
|
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
|
||||||
@@ -445,7 +459,7 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (dimensionRoot != null
|
if (dimensionRoot != null
|
||||||
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasFrozenPack(dimensionRoot))) {
|
&& (IRIS_DIMENSION_NAMESPACE.equals(worldKey.getNamespace()) || hasGenerationStorage(dimensionRoot))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
|
NamespacedKey messageKey = messageWorldKey(worldName, levelRoot.getName());
|
||||||
@@ -455,9 +469,10 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
+ " type=<pack>; Iris registers them with Multiverse itself.");
|
+ " type=<pack>; Iris registers them with Multiverse itself.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean hasFrozenPack(File dimensionRoot) {
|
private static boolean hasGenerationStorage(File dimensionRoot) {
|
||||||
Path packRoot = dimensionRoot.toPath().toAbsolutePath().normalize().resolve("iris").resolve("pack");
|
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(dimensionRoot.toPath());
|
||||||
return Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS);
|
return Files.isDirectory(paths.generationRoot(), LinkOption.NOFOLLOW_LINKS)
|
||||||
|
|| Files.isDirectory(paths.legacyPackRoot(), LinkOption.NOFOLLOW_LINKS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) {
|
private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) {
|
||||||
@@ -475,7 +490,9 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for "
|
throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for "
|
||||||
+ worldKey + ".");
|
+ worldKey + ".");
|
||||||
}
|
}
|
||||||
File snapshotRoot = IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
|
long worldSeed = requireStoredWorldSeed(worldName, dimensionRoot);
|
||||||
|
GenerationHistory history = requireGenerationHistory(dimensionRoot, id, worldSeed);
|
||||||
|
File snapshotRoot = requireActivePack(history);
|
||||||
try {
|
try {
|
||||||
requireSnapshotLoadable(snapshotRoot);
|
requireSnapshotLoadable(snapshotRoot);
|
||||||
} catch (BrokenPackException exception) {
|
} catch (BrokenPackException exception) {
|
||||||
@@ -487,18 +504,14 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisDimension dimension = IrisData.get(snapshotRoot).getDimensionLoader().load(id, false);
|
IrisDimension dimension = requireHistoricalDimension(history, snapshotRoot, id);
|
||||||
if (dimension == null) {
|
|
||||||
throw new IllegalStateException("Frozen Iris pack snapshot at " + snapshotRoot
|
|
||||||
+ " does not contain dimension " + id + ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
Iris.debug("Assuming IrisDimension: " + dimension.getName());
|
Iris.debug("Assuming IrisDimension: " + dimension.getName());
|
||||||
|
|
||||||
IrisWorld world = IrisWorld.builder()
|
IrisWorld world = IrisWorld.builder()
|
||||||
.platformIdentity(worldKey.toString())
|
.platformIdentity(worldKey.toString())
|
||||||
.name(worldName)
|
.name(worldName)
|
||||||
.seed(1337)
|
.seed(history.activeEpoch().worldSeed())
|
||||||
.worldFolder(dimensionRoot)
|
.worldFolder(dimensionRoot)
|
||||||
.minHeight(dimension.getMinHeight())
|
.minHeight(dimension.getMinHeight())
|
||||||
.maxHeight(dimension.getMaxHeight())
|
.maxHeight(dimension.getMaxHeight())
|
||||||
@@ -506,7 +519,133 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
|
|
||||||
Iris.debug("Generator Config: " + world);
|
Iris.debug("Generator Config: " + world);
|
||||||
|
|
||||||
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey());
|
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey(), history);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistory requireGenerationHistory(
|
||||||
|
File dimensionRoot,
|
||||||
|
String dimensionKey,
|
||||||
|
long worldSeed
|
||||||
|
) {
|
||||||
|
Path root = dimensionRoot.toPath().toAbsolutePath().normalize();
|
||||||
|
try {
|
||||||
|
Optional<GenerationHistory> current = GenerationHistory.openIfPresent(root, worldSeed);
|
||||||
|
GenerationHistory history = current.isPresent()
|
||||||
|
? current.get()
|
||||||
|
: adoptLegacyGenerationHistory(root, dimensionKey, worldSeed);
|
||||||
|
List<GenerationRegistryContract> retainedContracts = history.manifest()
|
||||||
|
.epochs()
|
||||||
|
.stream()
|
||||||
|
.map(GenerationEpoch::registryContract)
|
||||||
|
.toList();
|
||||||
|
history.requireRegistryDefinitions(
|
||||||
|
GenerationRegistryContractFactory.captureRequiredDefinitions(retainedContracts)
|
||||||
|
);
|
||||||
|
return history;
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistory adoptLegacyGenerationHistory(
|
||||||
|
Path dimensionRoot,
|
||||||
|
String dimensionKey,
|
||||||
|
long worldSeed
|
||||||
|
) throws IOException {
|
||||||
|
Path legacyPack = GenerationHistoryPaths.forDimension(dimensionRoot).legacyPackRoot();
|
||||||
|
requireSnapshotLoadable(legacyPack.toFile());
|
||||||
|
String fingerprint = GenerationPackFingerprint.compute(
|
||||||
|
legacyPack,
|
||||||
|
GenerationPackFingerprint.CURRENT_VERSION
|
||||||
|
);
|
||||||
|
IrisData data = IrisData.openDatapackCompiler(legacyPack.toFile());
|
||||||
|
try {
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IOException("Legacy Iris pack does not contain dimension " + dimensionKey + ".");
|
||||||
|
}
|
||||||
|
GenerationEpoch.DimensionContract dimensionContract = captureDimensionContract(data, dimension);
|
||||||
|
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
|
||||||
|
data,
|
||||||
|
dimension,
|
||||||
|
fingerprint,
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
|
||||||
|
);
|
||||||
|
return GenerationHistory.adoptLegacyPack(
|
||||||
|
dimensionRoot,
|
||||||
|
fingerprint,
|
||||||
|
worldSeed,
|
||||||
|
dimensionContract,
|
||||||
|
registryContract
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisDimension requireHistoricalDimension(
|
||||||
|
GenerationHistory history,
|
||||||
|
File packRoot,
|
||||||
|
String dimensionKey
|
||||||
|
) {
|
||||||
|
GenerationEpoch epoch = history.activeEpoch();
|
||||||
|
if (!epoch.dimensionContract().dimensionKey().equals(dimensionKey)) {
|
||||||
|
throw new IllegalStateException("Configured dimension " + dimensionKey
|
||||||
|
+ " does not match active generation history dimension "
|
||||||
|
+ epoch.dimensionContract().dimensionKey() + ".");
|
||||||
|
}
|
||||||
|
IrisData data = IrisData.get(packRoot);
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IllegalStateException("Immutable Iris generation pack at " + packRoot
|
||||||
|
+ " does not contain dimension " + dimensionKey + ".");
|
||||||
|
}
|
||||||
|
GenerationEpoch.DimensionContract actualContract = captureDimensionContract(data, dimension);
|
||||||
|
if (!epoch.dimensionContract().equals(actualContract)) {
|
||||||
|
throw new IllegalStateException("Immutable Iris generation pack dimension contract changed at "
|
||||||
|
+ packRoot + ".");
|
||||||
|
}
|
||||||
|
return dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationEpoch.DimensionContract captureDimensionContract(
|
||||||
|
IrisData data,
|
||||||
|
IrisDimension dimension
|
||||||
|
) {
|
||||||
|
String dimensionTypeKey = IrisPlatforms.get()
|
||||||
|
.registries()
|
||||||
|
.generationRegistry()
|
||||||
|
.dimensionTypeResourceKey(
|
||||||
|
data.getDataFolder().getName(),
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
dimension.getDimensionTypeKey()
|
||||||
|
);
|
||||||
|
return GenerationEpochContractFactory.create(
|
||||||
|
dimension,
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
dimensionTypeKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File requireActivePack(GenerationHistory history) {
|
||||||
|
try {
|
||||||
|
return history.activePackRoot().toFile();
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Active Iris generation pack is unusable.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long requireStoredWorldSeed(String worldName, File dimensionRoot) {
|
||||||
|
Long configuredSeed = IrisWorlds.readBukkitWorldSeed(worldName);
|
||||||
|
if (configuredSeed != null) {
|
||||||
|
return configuredSeed;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return WorldReplacementSeed.readAuthoritativeSeed(dimensionRoot.toPath());
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris world " + worldName
|
||||||
|
+ " has no authoritative generation seed.", failure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record FreshValidation(
|
private record FreshValidation(
|
||||||
|
|||||||
+18
-11
@@ -164,24 +164,28 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
boolean configurationApplied = false;
|
boolean configurationApplied = false;
|
||||||
try {
|
try {
|
||||||
Files.createDirectory(paths.stage());
|
Files.createDirectory(paths.stage());
|
||||||
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
|
|
||||||
requiredSender,
|
|
||||||
requiredDimension,
|
|
||||||
paths.stage().toFile()
|
|
||||||
);
|
|
||||||
if (installed == null) {
|
|
||||||
throw new IOException("Iris could not stage the dimension pack.");
|
|
||||||
}
|
|
||||||
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
|
|
||||||
long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
|
long effectiveSeed = WorldReplacementSeed.stageAuthoritativeSeed(
|
||||||
paths.target(),
|
paths.target(),
|
||||||
paths.stage(),
|
paths.stage(),
|
||||||
seedSelection
|
seedSelection
|
||||||
);
|
);
|
||||||
|
IrisDimension installed = Iris.service(StudioSVC.class).installIntoWorld(
|
||||||
|
requiredSender,
|
||||||
|
requiredDimension,
|
||||||
|
paths.stage().toFile(),
|
||||||
|
effectiveSeed
|
||||||
|
);
|
||||||
|
if (installed == null) {
|
||||||
|
throw new IOException("Iris could not stage the dimension pack.");
|
||||||
|
}
|
||||||
|
requireCompatibleEnvironment(target.slotKind(), installed.getEnvironment());
|
||||||
if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
|
if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
|
||||||
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId);
|
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId);
|
||||||
}
|
}
|
||||||
File stagedPack = paths.stage().resolve("iris/pack").toFile();
|
File stagedPack = IrisWorldStorage.requireActiveGenerationPackRoot(
|
||||||
|
paths.stage().toFile(),
|
||||||
|
effectiveSeed
|
||||||
|
);
|
||||||
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
|
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
|
||||||
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
|
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
|
||||||
transaction = new Transaction(
|
transaction = new Transaction(
|
||||||
@@ -806,7 +810,10 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind());
|
validatePublishedWorldRuntime(runtimeState, transaction, target.slotKind());
|
||||||
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
|
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
|
||||||
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
|
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
|
||||||
paths.target().resolve("iris/pack"));
|
IrisWorldStorage.requireActiveGenerationPackRoot(
|
||||||
|
paths.target().toFile(),
|
||||||
|
transaction.seed()
|
||||||
|
).toPath());
|
||||||
if (!transaction.packFingerprint().equals(fingerprint)) {
|
if (!transaction.packFingerprint().equals(fingerprint)) {
|
||||||
throw new IOException("The replacement pack changed before runtime verification.");
|
throw new IOException("The replacement pack changed before runtime verification.");
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -176,7 +176,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
|||||||
return z ^ (z >>> 31);
|
return z ^ (z >>> 31);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Director(description = "Update the pack of a world (UNSAFE!)", descriptionKey = "iris.director.commanddeveloper.director.update_pack_world_unsafe", name = "update-world", aliases = "^world")
|
@Director(description = "Stage a world generation update", descriptionKey = "iris.director.commanddeveloper.director.stage_world_generation_update", name = "update-world", aliases = "^world")
|
||||||
public void updateWorld(
|
public void updateWorld(
|
||||||
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
|
@Param(description = "The world to update", descriptionKey = "iris.director.commanddeveloper.param.world_update", contextual = true, contextualOverride = true)
|
||||||
World world,
|
World world,
|
||||||
@@ -202,7 +202,7 @@ public class CommandDeveloper implements DirectorExecutor {
|
|||||||
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
|
LifecycleOperationCoordinator.OperationKind.PACK_PUBLISH,
|
||||||
pack.getLoadKey()
|
pack.getLoadKey()
|
||||||
)) {
|
)) {
|
||||||
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder);
|
Iris.service(StudioSVC.class).replaceIntoWorld(sender(), pack, folder, world.getSeed());
|
||||||
} catch (LifecycleOperationCoordinator.BusyException e) {
|
} catch (LifecycleOperationCoordinator.BusyException e) {
|
||||||
sender().sendMessage(C.YELLOW + e.getMessage());
|
sender().sendMessage(C.YELLOW + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
+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.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.StructureReachability;
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
|
import art.arcane.iris.engine.history.GenerationSemanticQueries;
|
||||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
|
||||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
@@ -41,8 +41,10 @@ import art.arcane.iris.spi.IrisPlatforms;
|
|||||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||||
import art.arcane.iris.util.common.director.specialhandlers.HydrologyTypeHandler;
|
import art.arcane.iris.util.common.director.specialhandlers.HydrologyTypeHandler;
|
||||||
import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
import art.arcane.iris.engine.history.GenerationFindCatalog;
|
||||||
|
import art.arcane.iris.util.common.director.specialhandlers.LocatableObjectHandler;
|
||||||
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
|
import art.arcane.iris.util.common.director.specialhandlers.ReachableBiomeHandler;
|
||||||
|
import art.arcane.iris.util.common.director.specialhandlers.ReachableRegionHandler;
|
||||||
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
||||||
import art.arcane.iris.util.common.format.C;
|
import art.arcane.iris.util.common.format.C;
|
||||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||||
@@ -90,7 +92,7 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
|
|
||||||
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
@Director(description = "Find a region", descriptionKey = "iris.director.commandfind.director.find_region")
|
||||||
public void region(
|
public void region(
|
||||||
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look")
|
@Param(description = "The region to look for", descriptionKey = "iris.director.commandfind.param.region_look", customHandler = ReachableRegionHandler.class)
|
||||||
IrisRegion region,
|
IrisRegion region,
|
||||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
|
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_2", defaultValue = "true")
|
||||||
boolean teleport
|
boolean teleport
|
||||||
@@ -138,10 +140,6 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
IrisHydrologyRuntime runtime = activeEngine.getComplex().getHydrologyRuntime();
|
||||||
if (runtime == null) {
|
|
||||||
commandSender.sendMessage(C.YELLOW + "Hydrology is not active in this world.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
HydrologyFeatureQuery query;
|
HydrologyFeatureQuery query;
|
||||||
try {
|
try {
|
||||||
query = HydrologyFeatureQuery.parse(type);
|
query = HydrologyFeatureQuery.parse(type);
|
||||||
@@ -150,15 +148,19 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Location origin = target.getLocation();
|
Location origin = target.getLocation();
|
||||||
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
int requestedDistance = runtime == null
|
||||||
int maximumDistance = runtime.maximumFeatureSearchDistance(
|
? 8192
|
||||||
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||||
|
int maximumDistance = runtime == null
|
||||||
|
? requestedDistance
|
||||||
|
: runtime.maximumFeatureSearchDistance(
|
||||||
|
origin.getBlockX(), origin.getBlockZ(), requestedDistance);
|
||||||
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
commandSender.sendMessage(C.GRAY + "Searching accepted hydrology plans for " + type + "...");
|
||||||
J.a(() -> {
|
J.a(() -> {
|
||||||
try {
|
try {
|
||||||
HydrologyFeatureRef feature = runtime.nearestFeature(
|
GenerationSemanticQueries.RiverResult feature = GenerationSemanticQueries.nearestRiver(
|
||||||
query.types(),
|
activeEngine,
|
||||||
query.profileKey(),
|
query,
|
||||||
origin.getBlockX(),
|
origin.getBlockX(),
|
||||||
origin.getBlockZ(),
|
origin.getBlockZ(),
|
||||||
maximumDistance,
|
maximumDistance,
|
||||||
@@ -171,15 +173,14 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
+ maximumDistance + " blocks.");
|
+ maximumDistance + " blocks.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int worldY = feature.y() + activeEngine.getDimension().getMinHeight();
|
|
||||||
String label = type + " hydrology feature";
|
String label = type + " hydrology feature";
|
||||||
if (!teleport) {
|
if (!teleport) {
|
||||||
sendStructureMessage(target, commandSender,
|
sendStructureMessage(target, commandSender,
|
||||||
C.GREEN + "Found " + label + " at " + feature.x() + ", " + worldY + ", " + feature.z() + ".");
|
C.GREEN + "Found " + label + " at " + feature.x() + ", " + feature.y() + ", " + feature.z() + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Location destination = new Location(
|
Location destination = new Location(
|
||||||
target.getWorld(), feature.x(), worldY, feature.z());
|
target.getWorld(), feature.x(), feature.y(), feature.z());
|
||||||
prepareStructureTeleport(
|
prepareStructureTeleport(
|
||||||
target, target.getWorld(), commandSender, label, destination, true);
|
target, target.getWorld(), commandSender, label, destination, true);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
@@ -233,6 +234,12 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
registered, decision, nativePlacement, locatableNativePlacement,
|
registered, decision, nativePlacement, locatableNativePlacement,
|
||||||
locatableEditablePlacement, nativeGenerationEnabled, reachable);
|
locatableEditablePlacement, nativeGenerationEnabled, reachable);
|
||||||
|
|
||||||
|
if (route != StructureLookupRoute.NATIVE && route != StructureLookupRoute.IRIS
|
||||||
|
&& GenerationFindCatalog.hasRetainedStructurePlacement(e, structureKey)) {
|
||||||
|
locateIrisStructure(e, structureKey, commandSender);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (route == StructureLookupRoute.UNKNOWN) {
|
if (route == StructureLookupRoute.UNKNOWN) {
|
||||||
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
|
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_FIND_UNKNOWN_STRUCTURE, MessageArgument.untrusted("structureKey", structureKey)));
|
||||||
return;
|
return;
|
||||||
@@ -672,7 +679,7 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
J.a(() -> {
|
J.a(() -> {
|
||||||
try {
|
try {
|
||||||
IrisStructureLocator.LocateResult result =
|
IrisStructureLocator.LocateResult result =
|
||||||
IrisStructureLocator.locate(engine, structure, blockX, blockZ, 1024);
|
GenerationSemanticQueries.nearestStructure(engine, structure, blockX, blockZ, 1024);
|
||||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||||
sendStructureMessage(target, commandSender,
|
sendStructureMessage(target, commandSender,
|
||||||
C.YELLOW + "Unable to locate " + structure
|
C.YELLOW + "Unable to locate " + structure
|
||||||
@@ -697,7 +704,7 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
|
|
||||||
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
|
@Director(description = "Find an object", descriptionKey = "iris.director.commandfind.director.find_object")
|
||||||
public void object(
|
public void object(
|
||||||
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = ObjectHandler.class)
|
@Param(description = "The object to look for", descriptionKey = "iris.director.commandfind.param.object_look", customHandler = LocatableObjectHandler.class)
|
||||||
String object,
|
String object,
|
||||||
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
|
@Param(description = "Should you be teleported", descriptionKey = "iris.director.commandfind.param.should_you_be_teleported_4", defaultValue = "true")
|
||||||
boolean teleport
|
boolean teleport
|
||||||
@@ -721,7 +728,7 @@ public class CommandFind implements DirectorExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.hasObjectPlacement(object)) {
|
if (GenerationFindCatalog.hasObjectPlacement(e, object)) {
|
||||||
EngineBukkitOps.gotoObject(e, object, player(), teleport);
|
EngineBukkitOps.gotoObject(e, object, player(), teleport);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,12 +279,14 @@ public class CommandSVC implements IrisService, CommandExecutor, TabCompleter, D
|
|||||||
|
|
||||||
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
|
private DirectorExecutionResult runDirector(CommandSender sender, String label, String[] args) {
|
||||||
dispatchSenders.set(sender);
|
dispatchSenders.set(sender);
|
||||||
|
DirectorContext.touch(new VolmitSender(sender));
|
||||||
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
try (LanguageAudience.Scope audience = LanguageAudience.open(sender instanceof Player player ? player.getUniqueId() : null)) {
|
||||||
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
|
return getDirector().execute(new DirectorInvocation(new BukkitDirectorSender(sender), label, Arrays.asList(args)));
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
Iris.warn("Director command execution failed: " + e.getClass().getSimpleName() + " " + e.getMessage());
|
||||||
return DirectorExecutionResult.notHandled();
|
return DirectorExecutionResult.notHandled();
|
||||||
} finally {
|
} finally {
|
||||||
|
DirectorContext.remove();
|
||||||
dispatchSenders.remove();
|
dispatchSenders.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,40 @@ import static org.junit.Assert.assertFalse;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class IrisShutdownOrderingTest {
|
public class IrisShutdownOrderingTest {
|
||||||
|
@Test
|
||||||
|
public void serverStopHoldsThePluginLoaderUntilTerminalCleanupCompletes() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||||
|
String quiesce = section(source, "private void quiesceRuntimeForServerShutdown", "private void startPostStopFinisher");
|
||||||
|
String cleanup = section(source, "private void finishTerminalCleanup()", "private void drainWorldGenerators");
|
||||||
|
String teardown = section(source, "private void teardownRuntime", "private void quiesceRuntimeForServerShutdown");
|
||||||
|
String deferred = section(source, "private void finishDeferredRuntimeTeardown", "private void finishTerminalCleanup");
|
||||||
|
|
||||||
|
assertOrdered(quiesce, "serverStopTeardownDeferred.set(true)", "deferPluginClassLoaderClose()",
|
||||||
|
"jigsawStudioService.quiesceForServerShutdown()");
|
||||||
|
assertOrdered(cleanup, "runPostShutdown()", "hasActiveShutdownResources()", "IrisPlatforms.unbind()",
|
||||||
|
"runtimeTeardownFailed.get()", "releasePluginClassLoaderClose()");
|
||||||
|
assertTrue(cleanup.contains("!MultiBurst.burst.isTerminated() || !MultiBurst.ioBurst.isTerminated()"));
|
||||||
|
assertTrue(cleanup.contains("preservation.hasActiveResources()"));
|
||||||
|
assertOrdered(teardown, "drainWorldGenerators(reason, timeoutSeconds)", "!generatorDrainCompleted",
|
||||||
|
"return;", "service.onDisable()");
|
||||||
|
assertOrdered(deferred, "teardownRuntime(reason, timeoutSeconds)", "!generatorDrainCompleted",
|
||||||
|
"SHUTDOWN_ERRORS.println", "return;", "finishTerminalCleanup()");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deferredShutdownErrorsBypassClosedLoggingServices() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||||
|
String throwableReport = section(source, "public static void reportError(Throwable e)",
|
||||||
|
"public static void reportError(String context, Throwable e)");
|
||||||
|
String contextualReport = section(source, "public static void reportError(String context, Throwable e)",
|
||||||
|
"public static void dump()");
|
||||||
|
|
||||||
|
assertOrdered(throwableReport, "serverStopTeardownDeferred.get()",
|
||||||
|
"e.printStackTrace(SHUTDOWN_ERRORS)", "boolean debug = false");
|
||||||
|
assertOrdered(contextualReport, "serverStopTeardownDeferred.get()",
|
||||||
|
"SHUTDOWN_ERRORS.println(", "error.printStackTrace(SHUTDOWN_ERRORS)", "Iris.error(message)");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception {
|
public void drainWorldGenerators_closesGeneratorsWithoutEagerMantleSave() throws Exception {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||||
@@ -18,6 +52,22 @@ public class IrisShutdownOrderingTest {
|
|||||||
assertFalse("Shutdown must not close Mantle plates before generation drains", drain.contains("saveAllNow()"));
|
assertFalse("Shutdown must not close Mantle plates before generation drains", drain.contains("saveAllNow()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shutdownBoundaryIsPreparedBeforeHookRegistrationAndDoesNotUseClosedBindings() throws Exception {
|
||||||
|
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||||
|
String registration = section(source, "public void addShutdownHook()", "public void removeShutdownHook()");
|
||||||
|
String boundary = section(source, "private boolean awaitServerShutdownBoundary()",
|
||||||
|
"private void finishDeferredRuntimeTeardown");
|
||||||
|
|
||||||
|
assertOrdered(registration, "createServerShutdownBoundary()", "new Thread(this::runShutdownHook",
|
||||||
|
"Runtime.getRuntime().addShutdownHook");
|
||||||
|
assertTrue(boundary.contains("boundary.await("));
|
||||||
|
assertFalse(boundary.contains("INMS.get()"));
|
||||||
|
assertFalse(boundary.contains("Iris.reportError"));
|
||||||
|
assertTrue(source.contains("new FileOutputStream(FileDescriptor.err)"));
|
||||||
|
assertOrdered(boundary, "SHUTDOWN_ERRORS.println(", "e.printStackTrace(SHUTDOWN_ERRORS)", "return false;");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception {
|
public void serverStop_defersRuntimeTeardownUntilPaperShutdownBoundary() throws Exception {
|
||||||
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
String source = Files.readString(Path.of(System.getProperty("iris.startupSource")));
|
||||||
@@ -43,6 +93,7 @@ public class IrisShutdownOrderingTest {
|
|||||||
"finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)",
|
"finishDeferredRuntimeTeardown(\"startup-boundary-restart-hook\", 30L)",
|
||||||
"return;",
|
"return;",
|
||||||
"awaitServerShutdownBoundary()",
|
"awaitServerShutdownBoundary()",
|
||||||
|
"SHUTDOWN_ERRORS.println",
|
||||||
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
|
"finishDeferredRuntimeTeardown(\"shutdown-hook\", 30L)");
|
||||||
assertOrdered(finisher,
|
assertOrdered(finisher,
|
||||||
"finisher.setDaemon(false)",
|
"finisher.setDaemon(false)",
|
||||||
@@ -85,8 +136,8 @@ public class IrisShutdownOrderingTest {
|
|||||||
assertOrdered(teardown,
|
assertOrdered(teardown,
|
||||||
"drainWorldGenerators(reason, timeoutSeconds)",
|
"drainWorldGenerators(reason, timeoutSeconds)",
|
||||||
"service.onDisable()",
|
"service.onDisable()",
|
||||||
"MultiBurst.burst::close",
|
"MultiBurst.burst.close()",
|
||||||
"MultiBurst.ioBurst::close");
|
"MultiBurst.ioBurst.close()");
|
||||||
assertTrue("Engine service must retain its generator close behind deferred service teardown",
|
assertTrue("Engine service must retain its generator close behind deferred service teardown",
|
||||||
engineDisable.contains("startClose("));
|
engineDisable.contains("startClose("));
|
||||||
assertTrue("Studio service must retain its generator close behind deferred service teardown",
|
assertTrue("Studio service must retain its generator close behind deferred service teardown",
|
||||||
|
|||||||
+13
-7
@@ -195,7 +195,7 @@ public class IrisWorldGeneratorResolverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void configuredWorldResolutionUsesOnlyFrozenWorldLocalSnapshot() throws Exception {
|
public void configuredWorldResolutionUsesOnlyActiveImmutableGenerationPack() throws Exception {
|
||||||
String source = Files.readString(Path.of(
|
String source = Files.readString(Path.of(
|
||||||
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n");
|
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java")).replace("\r\n", "\n");
|
||||||
int resolverStart = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator(");
|
int resolverStart = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator(");
|
||||||
@@ -208,26 +208,32 @@ public class IrisWorldGeneratorResolverTest {
|
|||||||
"Frozen Iris world storage does not match the current platform layout",
|
"Frozen Iris world storage does not match the current platform layout",
|
||||||
currentPlatformRoot
|
currentPlatformRoot
|
||||||
);
|
);
|
||||||
int snapshotRoot = resolver.indexOf("IrisWorldStorage.requireFrozenPackRoot(dimensionRoot)");
|
int history = resolver.indexOf("requireGenerationHistory(dimensionRoot, id, worldSeed)");
|
||||||
|
int snapshotRoot = resolver.indexOf("requireActivePack(history)", history);
|
||||||
int validation = resolver.indexOf("requireSnapshotLoadable(snapshotRoot)");
|
int validation = resolver.indexOf("requireSnapshotLoadable(snapshotRoot)");
|
||||||
int exactLoad = resolver.indexOf(
|
int exactLoad = resolver.indexOf("requireHistoricalDimension(history, snapshotRoot, id)");
|
||||||
"IrisData.get(snapshotRoot).getDimensionLoader().load(id, false)"
|
|
||||||
);
|
|
||||||
int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())");
|
int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())");
|
||||||
|
int historicalSeed = resolver.indexOf(".seed(history.activeEpoch().worldSeed())");
|
||||||
int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)");
|
int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)");
|
||||||
|
|
||||||
assertTrue(dimensionRoot >= 0);
|
assertTrue(dimensionRoot >= 0);
|
||||||
assertTrue(currentPlatformRoot > dimensionRoot);
|
assertTrue(currentPlatformRoot > dimensionRoot);
|
||||||
assertTrue(layoutRefusal > currentPlatformRoot);
|
assertTrue(layoutRefusal > currentPlatformRoot);
|
||||||
assertTrue(snapshotRoot > layoutRefusal);
|
assertTrue(history > layoutRefusal);
|
||||||
|
assertTrue(snapshotRoot > history);
|
||||||
assertTrue(validation > snapshotRoot);
|
assertTrue(validation > snapshotRoot);
|
||||||
assertTrue(exactLoad > validation);
|
assertTrue(exactLoad > validation);
|
||||||
assertTrue(canonicalIdentity > exactLoad);
|
assertTrue(canonicalIdentity > exactLoad);
|
||||||
assertTrue(resolvedStorage > canonicalIdentity);
|
assertTrue(historicalSeed > canonicalIdentity);
|
||||||
|
assertTrue(resolvedStorage > historicalSeed);
|
||||||
assertFalse(resolver.contains("loadDimension("));
|
assertFalse(resolver.contains("loadDimension("));
|
||||||
assertFalse(resolver.contains("loadAnyDimension("));
|
assertFalse(resolver.contains("loadAnyDimension("));
|
||||||
assertFalse(resolver.contains("replaceIntoWorld("));
|
assertFalse(resolver.contains("replaceIntoWorld("));
|
||||||
assertFalse(resolver.contains("installIntoWorld("));
|
assertFalse(resolver.contains("installIntoWorld("));
|
||||||
|
assertFalse(resolver.contains(".seed(1337)"));
|
||||||
|
assertTrue(resolver.contains("GenerationHistory.openIfPresent(root, worldSeed)"));
|
||||||
|
assertTrue(resolver.contains("GenerationHistory.adoptLegacyPack("));
|
||||||
|
assertTrue(resolver.contains("GenerationRegistryContractFactory.captureRequiredDefinitions("));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+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(");
|
String baseHeight = method(source, "public int getBaseHeight(");
|
||||||
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
|
assertTrue(baseHeight.contains("currentEngine.acquireGenerationLease(\"bukkit_base_height\")"));
|
||||||
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
|
assertTrue(baseHeight.contains("IrisContext.open(currentEngine, lease.sessionId(), null)"));
|
||||||
assertTrue(baseHeight.contains("catch (GenerationSessionException e)"));
|
assertTrue(baseHeight.contains("catch (GenerationSessionException | IOException e)"));
|
||||||
|
|
||||||
String generation = method(source, "public void generateNoise(");
|
String generation = method(source, "public void generateNoise(");
|
||||||
assertTrue(generation.contains("throw new IllegalStateException"));
|
assertTrue(generation.contains("throw new IllegalStateException"));
|
||||||
|
|||||||
+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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ plugins {
|
|||||||
alias(libs.plugins.shadow)
|
alias(libs.plugins.shadow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
|
||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
|
||||||
@@ -163,6 +165,7 @@ dependencies {
|
|||||||
"art.arcane:core:${irisVersion}".toString(),
|
"art.arcane:core:${irisVersion}".toString(),
|
||||||
"art.arcane:spi:${irisVersion}".toString(),
|
"art.arcane:spi:${irisVersion}".toString(),
|
||||||
libs.paralithic,
|
libs.paralithic,
|
||||||
|
libs.asm,
|
||||||
libs.lru,
|
libs.lru,
|
||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ plugins {
|
|||||||
alias(libs.plugins.shadow)
|
alias(libs.plugins.shadow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
|
||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
|
||||||
@@ -162,6 +164,7 @@ dependencies {
|
|||||||
"art.arcane:core:${irisVersion}".toString(),
|
"art.arcane:core:${irisVersion}".toString(),
|
||||||
"art.arcane:spi:${irisVersion}".toString(),
|
"art.arcane:spi:${irisVersion}".toString(),
|
||||||
libs.paralithic,
|
libs.paralithic,
|
||||||
|
libs.asm,
|
||||||
libs.lru,
|
libs.lru,
|
||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
|
|||||||
+30
-8
@@ -1,9 +1,11 @@
|
|||||||
package art.arcane.iris.nativegen;
|
package art.arcane.iris.nativegen;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||||
@@ -23,6 +25,7 @@ import net.minecraft.world.level.levelgen.structure.Structure;
|
|||||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -48,14 +51,17 @@ public final class NativeStructureReferenceRepair {
|
|||||||
originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) {
|
originChunkX <= target.x() + REFERENCE_DISTANCE_CHUNKS; originChunkX++) {
|
||||||
for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS;
|
for (int originChunkZ = target.z() - REFERENCE_DISTANCE_CHUNKS;
|
||||||
originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) {
|
originChunkZ <= target.z() + REFERENCE_DISTANCE_CHUNKS; originChunkZ++) {
|
||||||
ChunkAccess originChunk = level.getChunk(
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS);
|
openOriginRuntimeScope(engine, originChunkX, originChunkZ)) {
|
||||||
for (Map.Entry<Structure, StructureStart> entry : originChunk.getAllStarts().entrySet()) {
|
ChunkAccess originChunk = level.getChunk(
|
||||||
ScannedStart start = scanStart(
|
originChunkX, originChunkZ, ChunkStatus.STRUCTURE_STARTS);
|
||||||
engine, serverLevel, structureManager, targetChunk, registry,
|
for (Map.Entry<Structure, StructureStart> entry : originChunk.getAllStarts().entrySet()) {
|
||||||
originChunk, entry.getKey(), entry.getValue());
|
ScannedStart start = scanStart(
|
||||||
if (start != null && isTargetRelevant(engine, targetChunk, start)) {
|
engine, serverLevel, structureManager, targetChunk, registry,
|
||||||
starts.add(start);
|
originChunk, entry.getKey(), entry.getValue());
|
||||||
|
if (start != null && isTargetRelevant(engine, targetChunk, start)) {
|
||||||
|
starts.add(start);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,6 +81,22 @@ public final class NativeStructureReferenceRepair {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GenerationHistoryRuntimeRouter.CoordinateScope openOriginRuntimeScope(
|
||||||
|
Engine engine,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Unable to resolve native structure references for origin "
|
||||||
|
+ chunkX + "," + chunkZ + " through generation history.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isTargetRelevant(
|
private static boolean isTargetRelevant(
|
||||||
Engine engine, ChunkAccess targetChunk, ScannedStart scanned) {
|
Engine engine, ChunkAccess targetChunk, ScannedStart scanned) {
|
||||||
StructureStart start = scanned.start();
|
StructureStart start = scanned.start();
|
||||||
|
|||||||
+102
-18
@@ -1,11 +1,13 @@
|
|||||||
package art.arcane.iris.nativegen;
|
package art.arcane.iris.nativegen;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||||
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
|
import art.arcane.iris.engine.framework.NativeStructurePlacementPlanner;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -28,6 +30,7 @@ import net.minecraft.world.level.levelgen.structure.StructureSet;
|
|||||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -38,6 +41,7 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.IntBinaryOperator;
|
import java.util.function.IntBinaryOperator;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,13 +64,14 @@ public final class NativeStructureVolumeIndex {
|
|||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final OriginResolver origins;
|
private final OriginResolver origins;
|
||||||
private final Map<Long, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
|
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
|
||||||
private final Map<Long, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
|
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> queryCache = lru(MAX_CACHED_QUERY_CHUNKS);
|
||||||
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
|
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
|
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
private final ReentrantLock[] originWindowStripes = createOriginWindowStripes();
|
private final ReentrantLock[] originWindowStripes = createOriginWindowStripes();
|
||||||
|
private final IntConsumer retirementListener = this::evictRuntime;
|
||||||
|
|
||||||
private NativeStructureVolumeIndex(Context context) {
|
private NativeStructureVolumeIndex(Context context) {
|
||||||
this.context = Objects.requireNonNull(context, "Native structure volume context must not be null");
|
this.context = Objects.requireNonNull(context, "Native structure volume context must not be null");
|
||||||
@@ -79,13 +84,25 @@ public final class NativeStructureVolumeIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void install(Engine engine, Context context) {
|
public static void install(Engine engine, Context context) {
|
||||||
Objects.requireNonNull(engine, "Native structure volume index requires an engine");
|
Engine requiredEngine = Objects.requireNonNull(engine, "Native structure volume index requires an engine");
|
||||||
INDEXES.put(engine, new NativeStructureVolumeIndex(context));
|
NativeStructureVolumeIndex installed = new NativeStructureVolumeIndex(context);
|
||||||
|
synchronized (INDEXES) {
|
||||||
|
NativeStructureVolumeIndex previous = INDEXES.put(requiredEngine, installed);
|
||||||
|
if (previous != null) {
|
||||||
|
previous.detachRetirementListener(requiredEngine);
|
||||||
|
}
|
||||||
|
installed.attachRetirementListener(requiredEngine);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void uninstall(Engine engine) {
|
public static void uninstall(Engine engine) {
|
||||||
if (engine != null) {
|
if (engine != null) {
|
||||||
INDEXES.remove(engine);
|
synchronized (INDEXES) {
|
||||||
|
NativeStructureVolumeIndex removed = INDEXES.remove(engine);
|
||||||
|
if (removed != null) {
|
||||||
|
removed.detachRetirementListener(engine);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +138,7 @@ public final class NativeStructureVolumeIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
KList<NativeStructureVolume> resolve(Engine engine, int minX, int minZ, int maxX, int maxZ) {
|
||||||
|
int runtimeId = runtimeId(engine);
|
||||||
int fromChunkX = Math.min(minX, maxX) >> 4;
|
int fromChunkX = Math.min(minX, maxX) >> 4;
|
||||||
int toChunkX = Math.max(minX, maxX) >> 4;
|
int toChunkX = Math.max(minX, maxX) >> 4;
|
||||||
int fromChunkZ = Math.min(minZ, maxZ) >> 4;
|
int fromChunkZ = Math.min(minZ, maxZ) >> 4;
|
||||||
@@ -128,7 +146,7 @@ public final class NativeStructureVolumeIndex {
|
|||||||
KList<NativeStructureVolume> matches = null;
|
KList<NativeStructureVolume> matches = null;
|
||||||
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
|
for (int chunkX = fromChunkX; chunkX <= toChunkX; chunkX++) {
|
||||||
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
|
for (int chunkZ = fromChunkZ; chunkZ <= toChunkZ; chunkZ++) {
|
||||||
for (NativeStructureVolume volume : chunkVolumes(engine, chunkX, chunkZ)) {
|
for (NativeStructureVolume volume : chunkVolumes(engine, runtimeId, chunkX, chunkZ)) {
|
||||||
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
if (!volume.intersectsRect(minX, minZ, maxX, maxZ)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -144,8 +162,13 @@ public final class NativeStructureVolumeIndex {
|
|||||||
return matches == null ? NativeStructureVolume.NONE : matches;
|
return matches == null ? NativeStructureVolume.NONE : matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
private KList<NativeStructureVolume> chunkVolumes(Engine engine, int chunkX, int chunkZ) {
|
private KList<NativeStructureVolume> chunkVolumes(
|
||||||
return cached(queryCache, queryBuilds, chunkKey(chunkX, chunkZ),
|
Engine engine,
|
||||||
|
int runtimeId,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
return cached(queryCache, queryBuilds, new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
|
||||||
() -> buildChunkVolumes(engine, chunkX, chunkZ));
|
() -> buildChunkVolumes(engine, chunkX, chunkZ));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,8 +219,16 @@ public final class NativeStructureVolumeIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
|
KList<NativeStructureVolume> originVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
return cached(originCache, originBuilds, chunkKey(chunkX, chunkZ),
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
() -> origins.volumesAt(engine, chunkX, chunkZ));
|
openHistoryCoordinateScope(engine, chunkX, chunkZ)) {
|
||||||
|
int runtimeId = runtimeId(engine);
|
||||||
|
return cached(
|
||||||
|
originCache,
|
||||||
|
originBuilds,
|
||||||
|
new RuntimeChunkKey(runtimeId, chunkKey(chunkX, chunkZ)),
|
||||||
|
() -> origins.volumesAt(engine, chunkX, chunkZ)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private KList<NativeStructureVolume> buildOriginVolumes(Engine engine, int chunkX, int chunkZ) {
|
private KList<NativeStructureVolume> buildOriginVolumes(Engine engine, int chunkX, int chunkZ) {
|
||||||
@@ -362,9 +393,23 @@ public final class NativeStructureVolumeIndex {
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
private KList<NativeStructureVolume> cached(Map<Long, KList<NativeStructureVolume>> cache,
|
void evictRuntime(int runtimeId) {
|
||||||
ConcurrentHashMap<Long, CompletableFuture<KList<NativeStructureVolume>>> builds,
|
synchronized (originCache) {
|
||||||
long key, Supplier<KList<NativeStructureVolume>> loader) {
|
originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
}
|
||||||
|
synchronized (queryCache) {
|
||||||
|
queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
}
|
||||||
|
originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KList<NativeStructureVolume> cached(
|
||||||
|
Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache,
|
||||||
|
ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> builds,
|
||||||
|
RuntimeChunkKey key,
|
||||||
|
Supplier<KList<NativeStructureVolume>> loader
|
||||||
|
) {
|
||||||
synchronized (cache) {
|
synchronized (cache) {
|
||||||
KList<NativeStructureVolume> hit = cache.get(key);
|
KList<NativeStructureVolume> hit = cache.get(key);
|
||||||
if (hit != null) {
|
if (hit != null) {
|
||||||
@@ -392,7 +437,9 @@ public final class NativeStructureVolumeIndex {
|
|||||||
try {
|
try {
|
||||||
KList<NativeStructureVolume> built = loader.get();
|
KList<NativeStructureVolume> built = loader.get();
|
||||||
synchronized (cache) {
|
synchronized (cache) {
|
||||||
cache.put(key, built);
|
if (builds.get(key) == future) {
|
||||||
|
cache.put(key, built);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
future.complete(built);
|
future.complete(built);
|
||||||
return built;
|
return built;
|
||||||
@@ -451,15 +498,52 @@ public final class NativeStructureVolumeIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<Long, KList<NativeStructureVolume>> lru(int capacity) {
|
private static GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||||
|
Engine engine,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ
|
||||||
|
) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return irisEngine.openGenerationHistoryCoordinateScope(chunkX << 4, chunkZ << 4);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Unable to route native structure origin "
|
||||||
|
+ chunkX + "," + chunkZ + " through generation history.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int runtimeId(Engine engine) {
|
||||||
|
return engine == null ? 0 : engine.getCacheID();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attachRetirementListener(Engine engine) {
|
||||||
|
if (engine instanceof IrisEngine irisEngine) {
|
||||||
|
irisEngine.addGenerationRuntimeRetirementListener(retirementListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void detachRetirementListener(Engine engine) {
|
||||||
|
if (engine instanceof IrisEngine irisEngine) {
|
||||||
|
irisEngine.removeGenerationRuntimeRetirementListener(retirementListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<RuntimeChunkKey, KList<NativeStructureVolume>> lru(int capacity) {
|
||||||
return new LinkedHashMap<>(16, 0.75F, true) {
|
return new LinkedHashMap<>(16, 0.75F, true) {
|
||||||
@Override
|
@Override
|
||||||
protected boolean removeEldestEntry(Map.Entry<Long, KList<NativeStructureVolume>> eldest) {
|
protected boolean removeEldestEntry(
|
||||||
|
Map.Entry<RuntimeChunkKey, KList<NativeStructureVolume>> eldest
|
||||||
|
) {
|
||||||
return size() > capacity;
|
return size() > capacity;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RuntimeChunkKey(int runtimeId, long chunkKey) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The generator, biome source and structure state arrive as suppliers so an installed index never holds the
|
* The generator, biome source and structure state arrive as suppliers so an installed index never holds the
|
||||||
* level (and through it the engine) strongly. That keeps the weakly keyed registry collectable on world unload.
|
* level (and through it the engine) strongly. That keeps the weakly keyed registry collectable on world unload.
|
||||||
|
|||||||
+201
-152
@@ -22,9 +22,11 @@ import art.arcane.iris.core.loader.IrisData;
|
|||||||
import art.arcane.iris.engine.DimensionStackContext;
|
import art.arcane.iris.engine.DimensionStackContext;
|
||||||
import art.arcane.iris.engine.DimensionStackLayout;
|
import art.arcane.iris.engine.DimensionStackLayout;
|
||||||
import art.arcane.iris.engine.DimensionTerrainContext;
|
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -51,6 +53,7 @@ import net.minecraft.world.level.biome.Climate;
|
|||||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -72,7 +75,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
private final BiomeSource serializedSource;
|
private final BiomeSource serializedSource;
|
||||||
private final Set<String> warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet();
|
private final Set<String> warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet();
|
||||||
private final Set<StructureStateBiomeSource> structureStateSources = ConcurrentHashMap.newKeySet();
|
|
||||||
// Pack generation. Every cache on this source is keyed by it, which is what keeps memoized tables from
|
// Pack generation. Every cache on this source is keyed by it, which is what keeps memoized tables from
|
||||||
// surviving a repoint with the previous pack's content.
|
// surviving a repoint with the previous pack's content.
|
||||||
private final AtomicLong packGeneration = new AtomicLong();
|
private final AtomicLong packGeneration = new AtomicLong();
|
||||||
@@ -105,9 +107,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
possibleStructureBiomeKeys = null;
|
possibleStructureBiomeKeys = null;
|
||||||
biomeKeySets = null;
|
biomeKeySets = null;
|
||||||
possibleBiomesCache = null;
|
possibleBiomesCache = null;
|
||||||
for (StructureStateBiomeSource source : structureStateSources) {
|
}
|
||||||
source.clearCache();
|
|
||||||
}
|
void evictRuntime(int runtimeIdentity) {
|
||||||
|
visibleBiomeCache.evictRuntime(runtimeIdentity);
|
||||||
|
structureBiomeCache.evictRuntime(runtimeIdentity);
|
||||||
|
surfaceStructureBiomeCache.evictRuntime(runtimeIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,7 +146,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
StructureStateBiomeSource source = new StructureStateBiomeSource(this, Set.copyOf(possible));
|
StructureStateBiomeSource source = new StructureStateBiomeSource(this, Set.copyOf(possible));
|
||||||
structureStateSources.add(source);
|
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,15 +312,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
|
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
if (lease == null) {
|
engine, quartX << 2, quartZ << 2, "modded_structure_biome");
|
||||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
try (historyScope) {
|
||||||
}
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
|
||||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
if (lease == null) {
|
||||||
if (!isReady(engine)) {
|
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
}
|
||||||
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
if (!isReady(engine)) {
|
||||||
|
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||||
|
}
|
||||||
|
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,16 +402,17 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
QuartPos.toBlock(quartZ)
|
QuartPos.toBlock(quartZ)
|
||||||
);
|
);
|
||||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||||
|
int runtimeIdentity = engine.getCacheID();
|
||||||
BiomeHolderTable cache = structureBiomeCache;
|
BiomeHolderTable cache = structureBiomeCache;
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
Holder<Biome> cached = cache.get(key);
|
Holder<Biome> cached = cache.get(runtimeIdentity, key);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
|
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
cache.put(key, resolved);
|
cache.put(runtimeIdentity, key, resolved);
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
@@ -413,16 +422,20 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
|
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
if (lease == null) {
|
engine, quartX << 2, quartZ << 2, "modded_visible_biome");
|
||||||
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
try (historyScope) {
|
||||||
}
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
|
||||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
if (lease == null) {
|
||||||
if (!isReady(engine)) {
|
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
||||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
}
|
||||||
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
if (!isReady(engine)) {
|
||||||
|
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||||
|
}
|
||||||
|
return getVisibleNoiseBiomeWithActiveGenerationLease(
|
||||||
|
engine, quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
return getVisibleNoiseBiomeWithActiveGenerationLease(
|
|
||||||
engine, quartX, quartY, quartZ, sampler);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,16 +452,18 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
QuartPos.toBlock(quartZ)
|
QuartPos.toBlock(quartZ)
|
||||||
);
|
);
|
||||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||||
|
int runtimeIdentity = engine.getCacheID();
|
||||||
BiomeHolderTable cache = visibleBiomeCache;
|
BiomeHolderTable cache = visibleBiomeCache;
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
Holder<Biome> cached = cache.get(key);
|
Holder<Biome> cached = cache.get(runtimeIdentity, key);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
Holder<Biome> resolved = resolveVisibleBiome(
|
||||||
|
engine, quartX, quartY, quartZ, sampler, null);
|
||||||
if (cacheable) {
|
if (cacheable) {
|
||||||
cache.put(key, resolved);
|
cache.put(runtimeIdentity, key, resolved);
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
@@ -458,45 +473,50 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (engine == null) {
|
if (engine == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
|
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
if (lease == null) {
|
engine, blockX, blockZ, "modded_surface_spawn_biome");
|
||||||
return null;
|
try (historyScope) {
|
||||||
}
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_surface_spawn_biome");
|
||||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
if (lease == null) {
|
||||||
if (!isReady(engine)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Registry<Biome> registry = biomeRegistry();
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
if (registry == null) {
|
if (!isReady(engine)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
int quartX = QuartPos.fromBlock(blockX);
|
|
||||||
int quartZ = QuartPos.fromBlock(blockZ);
|
|
||||||
int sampleX = QuartPos.toBlock(quartX);
|
|
||||||
int sampleZ = QuartPos.toBlock(quartZ);
|
|
||||||
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
|
|
||||||
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
|
||||||
DimensionStackLayout.Layer layer = stackContext == null
|
|
||||||
? null
|
|
||||||
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
|
|
||||||
int internalY = layer == null
|
|
||||||
? Engine.hostHeight(engine, sampleX, sampleZ, true)
|
|
||||||
: layer.clippedSurfaceY();
|
|
||||||
int quartY = QuartPos.fromBlock(internalY + engine.getMinHeight());
|
|
||||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
|
||||||
BiomeHolderTable cache = visibleBiomeCache;
|
|
||||||
if (cacheable) {
|
|
||||||
Holder<Biome> cached = cache.get(key);
|
|
||||||
if (cached != null) {
|
|
||||||
return cached;
|
|
||||||
}
|
}
|
||||||
|
Registry<Biome> registry = biomeRegistry();
|
||||||
|
if (registry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int quartX = QuartPos.fromBlock(blockX);
|
||||||
|
int quartZ = QuartPos.fromBlock(blockZ);
|
||||||
|
int sampleX = QuartPos.toBlock(quartX);
|
||||||
|
int sampleZ = QuartPos.toBlock(quartZ);
|
||||||
|
boolean cacheable = isBiomeCacheable(engine, sampleX, sampleZ);
|
||||||
|
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||||
|
DimensionStackLayout.Layer layer = stackContext == null
|
||||||
|
? null
|
||||||
|
: stackContext.getLayout(sampleX, sampleZ).surfaceLayer();
|
||||||
|
int internalY = layer == null
|
||||||
|
? Engine.hostHeight(engine, sampleX, sampleZ, true)
|
||||||
|
: layer.clippedSurfaceY();
|
||||||
|
int quartY = QuartPos.fromBlock(internalY + engine.getMinHeight());
|
||||||
|
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||||
|
int runtimeIdentity = engine.getCacheID();
|
||||||
|
BiomeHolderTable cache = visibleBiomeCache;
|
||||||
|
if (cacheable) {
|
||||||
|
Holder<Biome> cached = cache.get(runtimeIdentity, key);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Holder<Biome> resolved = resolveVisibleBiome(
|
||||||
|
engine, quartX, quartY, quartZ, null, null);
|
||||||
|
if (cacheable) {
|
||||||
|
cache.put(runtimeIdentity, key, resolved);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
}
|
}
|
||||||
Holder<Biome> resolved = resolveVisibleBiome(
|
|
||||||
engine, quartX, quartY, quartZ, null);
|
|
||||||
if (cacheable) {
|
|
||||||
cache.put(key, resolved);
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,13 +555,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (lease == null) {
|
if (lease == null) {
|
||||||
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
|
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
|
||||||
}
|
}
|
||||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
boolean surfaceQuery;
|
||||||
if (!isReady(engine)) {
|
try (lease) {
|
||||||
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
|
engine, x, z, "modded_biomes_within");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
if (!isReady(engine)) {
|
||||||
|
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||||
|
}
|
||||||
|
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
||||||
|
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
||||||
|
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight());
|
||||||
}
|
}
|
||||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
if (!surfaceQuery) {
|
||||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
|
||||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
|
|
||||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||||
}
|
}
|
||||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||||
@@ -552,7 +578,11 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||||
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
|
engine, quartX << 2, quartZ << 2, "modded_biomes_within");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
|
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return biomes;
|
return biomes;
|
||||||
@@ -562,13 +592,14 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
|
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
|
||||||
Climate.Sampler sampler) {
|
Climate.Sampler sampler) {
|
||||||
long key = packColumnKey(quartX, quartZ);
|
long key = packColumnKey(quartX, quartZ);
|
||||||
|
int runtimeIdentity = engine.getCacheID();
|
||||||
BiomeHolderTable cache = surfaceStructureBiomeCache;
|
BiomeHolderTable cache = surfaceStructureBiomeCache;
|
||||||
Holder<Biome> cached = cache.get(key);
|
Holder<Biome> cached = cache.get(runtimeIdentity, key);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
Holder<Biome> resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
Holder<Biome> resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||||
cache.put(key, resolved);
|
cache.put(runtimeIdentity, key, resolved);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,11 +649,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> resolveVisibleBiome(Engine engine, int quartX, int quartY, int quartZ,
|
|
||||||
Climate.Sampler sampler) {
|
|
||||||
return resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Holder<Biome> resolveVisibleBiome(
|
private Holder<Biome> resolveVisibleBiome(
|
||||||
Engine engine,
|
Engine engine,
|
||||||
int quartX,
|
int quartX,
|
||||||
@@ -637,20 +663,21 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (resolution == null || registry == null) {
|
if (resolution == null || registry == null) {
|
||||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
return resolveVisibleBiome(
|
String historicalBiomeKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
|
||||||
engine, registry, resolution, quartX, quartY, quartZ, sampler);
|
resolution.blockX(),
|
||||||
}
|
resolution.blockY(),
|
||||||
|
resolution.blockZ()).orElse(null);
|
||||||
private Holder<Biome> resolveVisibleBiome(
|
if (historicalBiomeKey != null) {
|
||||||
Engine engine,
|
Holder<Biome> historical = resolveHolder(registry, historicalBiomeKey);
|
||||||
Registry<Biome> registry,
|
if (historical == null) {
|
||||||
BiomeResolution resolution,
|
throw new IllegalStateException("Historical Iris biome '" + historicalBiomeKey
|
||||||
int quartX,
|
+ "' is not registered at block " + resolution.blockX() + ","
|
||||||
int quartY,
|
+ resolution.blockY() + "," + resolution.blockZ());
|
||||||
int quartZ,
|
}
|
||||||
Climate.Sampler sampler
|
return historical;
|
||||||
) {
|
}
|
||||||
String biomeKey;
|
String biomeKey;
|
||||||
|
String unresolvedBiomeKey;
|
||||||
if (resolution.irisBiome().isCustom()) {
|
if (resolution.irisBiome().isCustom()) {
|
||||||
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
||||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||||
@@ -658,18 +685,27 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return fallbackBiome(registry, "custom derivative of '"
|
return fallbackBiome(registry, "custom derivative of '"
|
||||||
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
|
+ resolution.irisBiome().getLoadKey() + "'", quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
biomeKey = customBiomeRef(
|
String logicalBiomeKey = customBiomeRef(
|
||||||
resolution.packName(), resolution.dimension(), customBiome.getId());
|
resolution.packName(), resolution.dimension(), customBiome.getId());
|
||||||
|
biomeKey = resolution.dimension() == engine.getDimension()
|
||||||
|
&& resolution.data() == engine.getData()
|
||||||
|
? ModdedWorldgenIds.biomeRef(engine, customBiome.getId())
|
||||||
|
: resolution.data().customBiomeResourceKey(resolution.dimension(), customBiome);
|
||||||
|
unresolvedBiomeKey = logicalBiomeKey.equals(biomeKey)
|
||||||
|
? biomeKey
|
||||||
|
: logicalBiomeKey + " mapped to " + biomeKey;
|
||||||
} else if (resolution.underground()) {
|
} else if (resolution.underground()) {
|
||||||
biomeKey = resolution.irisBiome().getGroundBiomeKey(
|
biomeKey = resolution.irisBiome().getGroundBiomeKey(
|
||||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||||
|
unresolvedBiomeKey = biomeKey;
|
||||||
} else {
|
} else {
|
||||||
biomeKey = resolution.irisBiome().getSkyBiomeKey(
|
biomeKey = resolution.irisBiome().getSkyBiomeKey(
|
||||||
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
resolution.rng(), engine, resolution.blockX(), resolution.blockY(), resolution.blockZ());
|
||||||
|
unresolvedBiomeKey = biomeKey;
|
||||||
}
|
}
|
||||||
Holder<Biome> resolved = resolveHolder(registry, biomeKey);
|
Holder<Biome> resolved = resolveHolder(registry, biomeKey);
|
||||||
return resolved == null
|
return resolved == null
|
||||||
? fallbackBiome(registry, biomeKey, quartX, quartY, quartZ, sampler)
|
? fallbackBiome(registry, unresolvedBiomeKey, quartX, quartY, quartZ, sampler)
|
||||||
: resolved;
|
: resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,43 +820,30 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
|
private Holder<Biome> resolveRequiredStructureBiome(
|
||||||
|
int quartX,
|
||||||
|
int quartY,
|
||||||
|
int quartZ,
|
||||||
|
Climate.Sampler sampler
|
||||||
|
) {
|
||||||
IrisModdedChunkGenerator current = generator;
|
IrisModdedChunkGenerator current = generator;
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
|
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
|
||||||
}
|
}
|
||||||
Engine engine = current.awaitStructureEngine();
|
Engine engine = current.awaitStructureEngine();
|
||||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
|
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
if (lease == null) {
|
engine, quartX << 2, quartZ << 2, "modded_structure_state_biome");
|
||||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
try (historyScope) {
|
||||||
}
|
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
|
||||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
if (lease == null) {
|
||||||
if (!isReady(engine)) {
|
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
|
||||||
}
|
}
|
||||||
Registry<Biome> registry = biomeRegistry();
|
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||||
if (registry == null) {
|
if (!isReady(engine)) {
|
||||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||||
|
}
|
||||||
|
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
||||||
}
|
}
|
||||||
IrisBiome irisBiome;
|
|
||||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
|
||||||
irisBiome = resolveSurfaceStructureBiome(engine, quartX << 2, quartZ << 2);
|
|
||||||
} else {
|
|
||||||
BiomeResolution resolution = resolveStructureBiomeResolution(
|
|
||||||
engine, quartX, quartY, quartZ);
|
|
||||||
irisBiome = resolution == null ? null : resolution.irisBiome();
|
|
||||||
}
|
|
||||||
if (irisBiome == null) {
|
|
||||||
throw new IllegalStateException("Iris returned no structure biome at quart "
|
|
||||||
+ quartX + "," + quartY + "," + quartZ);
|
|
||||||
}
|
|
||||||
String derivativeKey = irisBiome.getStructureDerivativeKey();
|
|
||||||
Holder<Biome> resolved = resolveHolder(registry, derivativeKey);
|
|
||||||
if (resolved == null) {
|
|
||||||
throw new IllegalStateException("Iris structure biome derivative '" + derivativeKey
|
|
||||||
+ "' is not registered at quart " + quartX + "," + quartY + "," + quartZ);
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -875,6 +898,30 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return current == null ? null : current.structureEngineOrNull();
|
return current == null ? null : current.structureEngineOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||||
|
Engine engine,
|
||||||
|
int blockX,
|
||||||
|
int blockZ,
|
||||||
|
String operation
|
||||||
|
) {
|
||||||
|
IrisModdedChunkGenerator current = generator;
|
||||||
|
if (current != null && current.allowsGenerationHistoryBypass(engine)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
throw new IllegalStateException("Iris " + operation + " requires an IrisEngine runtime.");
|
||||||
|
}
|
||||||
|
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElseThrow(() ->
|
||||||
|
new IllegalStateException("Iris " + operation
|
||||||
|
+ " requires an attached generation-history runtime router."));
|
||||||
|
try {
|
||||||
|
return router.openCoordinateScope(blockX, blockZ);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris " + operation + " could not route block "
|
||||||
|
+ blockX + "," + blockZ + " through generation history.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private GenerationSessionLease tryAcquireGenerationLease(Engine engine, String operation) {
|
private GenerationSessionLease tryAcquireGenerationLease(Engine engine, String operation) {
|
||||||
if (engine == null || engine.isClosed()) {
|
if (engine == null || engine.isClosed()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -950,8 +997,9 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
if (!isReady(engine)) {
|
if (!isReady(engine)) {
|
||||||
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
|
throw new IllegalStateException("Iris structure biome key lookup has no active engine runtime");
|
||||||
}
|
}
|
||||||
Set<String> structureRequired = collectStructureBiomeKeys(
|
LinkedHashSet<String> structureRequired = new LinkedHashSet<>(collectStructureBiomeKeys(
|
||||||
engine.getDimension(), engine.getData());
|
engine.getDimension(), engine.getData()));
|
||||||
|
structureRequired.addAll(IrisModdedChunkGenerator.retainedBiomeKeys(engine));
|
||||||
LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
|
LinkedHashSet<String> visibleRequired = new LinkedHashSet<>();
|
||||||
LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
|
LinkedHashSet<String> visibleEmittable = new LinkedHashSet<>();
|
||||||
boolean visibleFallbackPossible = false;
|
boolean visibleFallbackPossible = false;
|
||||||
@@ -959,11 +1007,8 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
IrisBiome irisBiome = ownedBiome.biome();
|
IrisBiome irisBiome = ownedBiome.biome();
|
||||||
if (irisBiome.isCustom()) {
|
if (irisBiome.isCustom()) {
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
String customKey = customBiomeRef(
|
String customKey = ownedBiome.data().customBiomeResourceKey(
|
||||||
ownedBiome.data().getDataFolder().getName(),
|
ownedBiome.dimension(), customBiome);
|
||||||
ownedBiome.dimension(),
|
|
||||||
customBiome.getId()
|
|
||||||
);
|
|
||||||
visibleRequired.add(customKey);
|
visibleRequired.add(customKey);
|
||||||
visibleEmittable.add(customKey);
|
visibleEmittable.add(customKey);
|
||||||
}
|
}
|
||||||
@@ -1035,6 +1080,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
irisBiome,
|
irisBiome,
|
||||||
underground,
|
underground,
|
||||||
dimension,
|
dimension,
|
||||||
|
data,
|
||||||
data.getDataFolder().getName(),
|
data.getDataFolder().getName(),
|
||||||
blockX,
|
blockX,
|
||||||
blockY,
|
blockY,
|
||||||
@@ -1181,6 +1227,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
IrisBiome irisBiome,
|
IrisBiome irisBiome,
|
||||||
boolean underground,
|
boolean underground,
|
||||||
IrisDimension dimension,
|
IrisDimension dimension,
|
||||||
|
IrisData data,
|
||||||
String packName,
|
String packName,
|
||||||
int blockX,
|
int blockX,
|
||||||
int blockY,
|
int blockY,
|
||||||
@@ -1224,35 +1271,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
private volatile AtomicReferenceArray<Entry> table;
|
private volatile AtomicReferenceArray<Entry> table;
|
||||||
|
|
||||||
Holder<Biome> get(long key) {
|
Holder<Biome> get(int runtimeIdentity, long key) {
|
||||||
AtomicReferenceArray<Entry> slots = table;
|
AtomicReferenceArray<Entry> slots = table;
|
||||||
if (slots == null) {
|
if (slots == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int home = home(key);
|
int home = home(runtimeIdentity, key);
|
||||||
for (int probe = 0; probe < PROBE_LIMIT; probe++) {
|
for (int probe = 0; probe < PROBE_LIMIT; probe++) {
|
||||||
Entry entry = slots.get((home + probe) & MASK);
|
Entry entry = slots.get((home + probe) & MASK);
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (entry.key() == key) {
|
if (entry.runtimeIdentity() == runtimeIdentity && entry.key() == key) {
|
||||||
return entry.value();
|
return entry.value();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void put(long key, Holder<Biome> value) {
|
void put(int runtimeIdentity, long key, Holder<Biome> value) {
|
||||||
AtomicReferenceArray<Entry> slots = table;
|
AtomicReferenceArray<Entry> slots = table;
|
||||||
if (slots == null) {
|
if (slots == null) {
|
||||||
slots = install();
|
slots = install();
|
||||||
}
|
}
|
||||||
int home = home(key);
|
int home = home(runtimeIdentity, key);
|
||||||
Entry entry = new Entry(key, value);
|
Entry entry = new Entry(runtimeIdentity, key, value);
|
||||||
for (int probe = 0; probe < PROBE_LIMIT; probe++) {
|
for (int probe = 0; probe < PROBE_LIMIT; probe++) {
|
||||||
int slot = (home + probe) & MASK;
|
int slot = (home + probe) & MASK;
|
||||||
Entry existing = slots.get(slot);
|
Entry existing = slots.get(slot);
|
||||||
if (existing == null || existing.key() == key) {
|
if (existing == null
|
||||||
|
|| (existing.runtimeIdentity() == runtimeIdentity && existing.key() == key)) {
|
||||||
slots.set(slot, entry);
|
slots.set(slot, entry);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1260,6 +1308,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
slots.set(home, entry);
|
slots.set(home, entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void evictRuntime(int runtimeIdentity) {
|
||||||
|
AtomicReferenceArray<Entry> slots = table;
|
||||||
|
if (slots == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < slots.length(); index++) {
|
||||||
|
Entry entry = slots.get(index);
|
||||||
|
if (entry != null && entry.runtimeIdentity() == runtimeIdentity) {
|
||||||
|
slots.compareAndSet(index, entry, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized AtomicReferenceArray<Entry> install() {
|
private synchronized AtomicReferenceArray<Entry> install() {
|
||||||
AtomicReferenceArray<Entry> existing = table;
|
AtomicReferenceArray<Entry> existing = table;
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
@@ -1270,29 +1331,25 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int home(long key) {
|
private static int home(int runtimeIdentity, long key) {
|
||||||
long mixed = key * 0x9E3779B97F4A7C15L;
|
long mixed = (key ^ ((long) runtimeIdentity * 0xC2B2AE3D27D4EB4FL))
|
||||||
|
* 0x9E3779B97F4A7C15L;
|
||||||
return (int) ((mixed ^ (mixed >>> 32)) & MASK);
|
return (int) ((mixed ^ (mixed >>> 32)) & MASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private record Entry(long key, Holder<Biome> value) {
|
private record Entry(int runtimeIdentity, long key, Holder<Biome> value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class StructureStateBiomeSource extends BiomeSource {
|
private static final class StructureStateBiomeSource extends BiomeSource {
|
||||||
private final IrisModdedBiomeSource delegate;
|
private final IrisModdedBiomeSource delegate;
|
||||||
private final Set<Holder<Biome>> possibleBiomes;
|
private final Set<Holder<Biome>> possibleBiomes;
|
||||||
private volatile BiomeHolderTable resolvedBiomes = new BiomeHolderTable();
|
|
||||||
|
|
||||||
private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set<Holder<Biome>> possibleBiomes) {
|
private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set<Holder<Biome>> possibleBiomes) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
this.possibleBiomes = possibleBiomes;
|
this.possibleBiomes = possibleBiomes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearCache() {
|
|
||||||
resolvedBiomes = new BiomeHolderTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected MapCodec<? extends BiomeSource> codec() {
|
protected MapCodec<? extends BiomeSource> codec() {
|
||||||
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
|
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
|
||||||
@@ -1305,15 +1362,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||||
long key = packNoiseKey(x, y, z);
|
return delegate.resolveRequiredStructureBiome(x, y, z, sampler);
|
||||||
BiomeHolderTable cache = resolvedBiomes;
|
|
||||||
Holder<Biome> cached = cache.get(key);
|
|
||||||
if (cached != null) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
Holder<Biome> resolved = delegate.resolveRequiredStructureBiome(x, y, z);
|
|
||||||
cache.put(key, resolved);
|
|
||||||
return resolved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+453
-64
@@ -20,11 +20,17 @@ package art.arcane.iris.modded;
|
|||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
|
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||||
import art.arcane.iris.nativegen.NativeStructureStartInjector;
|
import art.arcane.iris.nativegen.NativeStructureStartInjector;
|
||||||
@@ -73,6 +79,7 @@ import net.minecraft.world.level.chunk.ChunkAccess;
|
|||||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||||
import net.minecraft.world.level.levelgen.RandomState;
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
@@ -85,18 +92,25 @@ import net.minecraft.world.level.levelgen.structure.StructureStart;
|
|||||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.IntBinaryOperator;
|
import java.util.function.IntBinaryOperator;
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
|
||||||
public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||||
// Vanilla-shaped fallback for an unbound generator (matches IrisDimension defaults). getMinY,
|
// Vanilla-shaped fallback for an unbound generator (matches IrisDimension defaults). getMinY,
|
||||||
@@ -127,16 +141,20 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
|
private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
|
||||||
private final ModdedImportedFeatureStage importedFeatures;
|
private final ModdedImportedFeatureStage importedFeatures;
|
||||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||||
|
private final IntConsumer generationRuntimeRetirementListener = this::retireGenerationRuntimeCaches;
|
||||||
private volatile boolean unloading;
|
private volatile boolean unloading;
|
||||||
private volatile Engine engine;
|
private volatile Engine engine;
|
||||||
private volatile String activePack;
|
private volatile String activePack;
|
||||||
private volatile String activeDimensionKey;
|
private volatile String activeDimensionKey;
|
||||||
|
private volatile Path immutablePackRoot;
|
||||||
|
private volatile ModdedGenerationMode generationMode = ModdedGenerationMode.PERSISTENT_RESTORE;
|
||||||
private volatile long seedOverride = Long.MIN_VALUE;
|
private volatile long seedOverride = Long.MIN_VALUE;
|
||||||
private volatile long lastChunkGenAt = 0L;
|
private volatile long lastChunkGenAt = 0L;
|
||||||
private volatile Set<String> configuredStructureBiomeKeys;
|
private volatile Set<String> configuredStructureBiomeKeys;
|
||||||
private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack;
|
private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack;
|
||||||
private volatile ModdedDimensionMetadata.DimensionMetadata heightMetadata;
|
private volatile ModdedDimensionMetadata.DimensionMetadata heightMetadata;
|
||||||
private volatile ServerLevel boundLevel;
|
private volatile ServerLevel boundLevel;
|
||||||
|
private volatile IrisEngine retirementListenerEngine;
|
||||||
|
|
||||||
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
|
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
|
||||||
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
|
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
|
||||||
@@ -169,12 +187,39 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
|
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
|
||||||
|
repoint(
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed,
|
||||||
|
null,
|
||||||
|
ModdedGenerationMode.PERSISTENT_RESTORE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void repoint(String pack, String packDimensionKey, long seed, Path packRoot) {
|
||||||
|
repoint(
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed,
|
||||||
|
packRoot,
|
||||||
|
ModdedGenerationMode.PERSISTENT_RESTORE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void repoint(
|
||||||
|
String pack,
|
||||||
|
String packDimensionKey,
|
||||||
|
long seed,
|
||||||
|
Path packRoot,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
ServerLevel level = boundLevel();
|
ServerLevel level = boundLevel();
|
||||||
if (level != null) {
|
if (level != null) {
|
||||||
|
this.generationMode = generationMode;
|
||||||
repointAndBind(level, pack, packDimensionKey, seed);
|
repointAndBind(level, pack, packDimensionKey, seed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyUnboundConfiguration(pack, packDimensionKey, seed);
|
applyUnboundConfiguration(pack, packDimensionKey, seed, packRoot, generationMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
|
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
|
||||||
@@ -185,7 +230,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
requireGlobalStructureGeneration(
|
requireGlobalStructureGeneration(
|
||||||
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
||||||
Engine activeEngine = engineIfBound();
|
Engine activeEngine = engineIfBound();
|
||||||
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
|
Engine replacement = ModdedWorldEngines.prepareReplacement(
|
||||||
|
level,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed,
|
||||||
|
generationMode
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
if (activeEngine != null) {
|
if (activeEngine != null) {
|
||||||
requireStructureBiomeUniverseCompatible(
|
requireStructureBiomeUniverseCompatible(
|
||||||
@@ -213,13 +264,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
this.activePack = pack;
|
this.activePack = pack;
|
||||||
this.activeDimensionKey = packDimensionKey;
|
this.activeDimensionKey = packDimensionKey;
|
||||||
this.seedOverride = seed;
|
this.seedOverride = seed;
|
||||||
|
this.engine = replacement;
|
||||||
|
bindGenerationRuntimeRetirementListener(replacement);
|
||||||
|
this.immutablePackRoot = replacement.getData().getDataFolder().toPath().toAbsolutePath().normalize();
|
||||||
this.configuredStructureBiomeKeys = null;
|
this.configuredStructureBiomeKeys = null;
|
||||||
this.configuredPack = null;
|
this.configuredPack = null;
|
||||||
this.heightMetadata = engineHeights(replacement);
|
this.heightMetadata = engineHeights(replacement);
|
||||||
this.engineBinding.reset();
|
this.engineBinding.reset();
|
||||||
this.announced.set(false);
|
this.announced.set(false);
|
||||||
resetRuntimeCaches();
|
resetRuntimeCaches();
|
||||||
this.engine = replacement;
|
|
||||||
this.engineBinding.complete(replacement);
|
this.engineBinding.complete(replacement);
|
||||||
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
|
// Bind time: a feature-order cycle in the new pack is reported here, once, and degrades to features-off.
|
||||||
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
|
// Never waits on a build owned by another thread: this method owns the generator monitor and the build
|
||||||
@@ -242,10 +295,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void clearEngineBinding() {
|
private void clearEngineBinding() {
|
||||||
|
unbindGenerationRuntimeRetirementListener();
|
||||||
this.engine = null;
|
this.engine = null;
|
||||||
this.boundLevel = null;
|
this.boundLevel = null;
|
||||||
this.configuredStructureBiomeKeys = null;
|
this.configuredStructureBiomeKeys = null;
|
||||||
this.configuredPack = null;
|
this.configuredPack = null;
|
||||||
|
this.immutablePackRoot = null;
|
||||||
this.engineBinding.reset();
|
this.engineBinding.reset();
|
||||||
this.announced.set(false);
|
this.announced.set(false);
|
||||||
this.structureBiomeSource.clearCaches();
|
this.structureBiomeSource.clearCaches();
|
||||||
@@ -254,10 +309,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
this.spawnTables.resetVanillaSpawnBiomes();
|
this.spawnTables.resetVanillaSpawnBiomes();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
|
private void applyUnboundConfiguration(
|
||||||
|
String pack,
|
||||||
|
String packDimensionKey,
|
||||||
|
long seed,
|
||||||
|
Path packRoot,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
|
unbindGenerationRuntimeRetirementListener();
|
||||||
this.activePack = pack;
|
this.activePack = pack;
|
||||||
this.activeDimensionKey = packDimensionKey;
|
this.activeDimensionKey = packDimensionKey;
|
||||||
this.seedOverride = seed;
|
this.seedOverride = seed;
|
||||||
|
this.immutablePackRoot = packRoot == null ? null : packRoot.toAbsolutePath().normalize();
|
||||||
|
this.generationMode = Objects.requireNonNull(generationMode, "generationMode");
|
||||||
this.engine = null;
|
this.engine = null;
|
||||||
this.boundLevel = null;
|
this.boundLevel = null;
|
||||||
this.configuredStructureBiomeKeys = null;
|
this.configuredStructureBiomeKeys = null;
|
||||||
@@ -272,7 +336,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void resetToDefault() {
|
public synchronized void resetToDefault() {
|
||||||
repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE);
|
repoint(
|
||||||
|
defaultPack,
|
||||||
|
defaultDimensionKey,
|
||||||
|
Long.MIN_VALUE,
|
||||||
|
null,
|
||||||
|
ModdedGenerationMode.PERSISTENT_RESTORE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String activePack() {
|
public String activePack() {
|
||||||
@@ -312,7 +382,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
BlockPos pos, int radius,
|
BlockPos pos, int radius,
|
||||||
boolean findUnexplored) {
|
boolean findUnexplored) {
|
||||||
Engine current = engine();
|
Engine current = engine();
|
||||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
|
int chunkX = Math.floorDiv(pos.getX(), 16);
|
||||||
|
int chunkZ = Math.floorDiv(pos.getZ(), 16);
|
||||||
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
current, chunkX, chunkZ, "modded_structure_locate");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures(
|
HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures(
|
||||||
level, holders, current);
|
level, holders, current);
|
||||||
@@ -481,14 +556,22 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
Engine created = ModdedWorldEngines.get(
|
||||||
|
level,
|
||||||
|
activePack,
|
||||||
|
activeDimensionKey,
|
||||||
|
seedOverride,
|
||||||
|
generationMode
|
||||||
|
);
|
||||||
requireCompletedShutdown(created);
|
requireCompletedShutdown(created);
|
||||||
if (created.isClosed() || created.getComplex() == null) {
|
if (created.isClosed() || created.getComplex() == null) {
|
||||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||||
+ "' created an engine without a ready biome complex");
|
+ "' created an engine without a ready biome complex");
|
||||||
}
|
}
|
||||||
engine = created;
|
engine = created;
|
||||||
|
bindGenerationRuntimeRetirementListener(created);
|
||||||
boundLevel = level;
|
boundLevel = level;
|
||||||
|
immutablePackRoot = created.getData().getDataFolder().toPath().toAbsolutePath().normalize();
|
||||||
heightMetadata = engineHeights(created);
|
heightMetadata = engineHeights(created);
|
||||||
configuredStructureBiomeKeys = null;
|
configuredStructureBiomeKeys = null;
|
||||||
structureBiomeSource.clearCaches();
|
structureBiomeSource.clearCaches();
|
||||||
@@ -649,10 +732,14 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
Engine current = engine;
|
Engine current = engine;
|
||||||
if (current != null && !current.isClosed() && !current.isClosing()) {
|
if (current != null && !current.isClosed() && !current.isClosing()) {
|
||||||
|
Set<String> retained = retainedBiomeKeys(current);
|
||||||
|
if (!retained.isEmpty()) {
|
||||||
|
configuredStructureBiomeKeys = retained;
|
||||||
|
return retained;
|
||||||
|
}
|
||||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
|
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(current);
|
||||||
current.getAllBiomes(), current.getDimension().getLoadKey());
|
|
||||||
configuredStructureBiomeKeys = resolved;
|
configuredStructureBiomeKeys = resolved;
|
||||||
return resolved;
|
return resolved;
|
||||||
} catch (GenerationSessionException e) {
|
} catch (GenerationSessionException e) {
|
||||||
@@ -669,6 +756,27 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Set<String> retainedBiomeKeys(Engine engine) {
|
||||||
|
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||||
|
if (router == null) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||||
|
Collection<GenerationEpoch> epochs = router.history().manifest().epochs();
|
||||||
|
for (GenerationEpoch epoch : epochs) {
|
||||||
|
GenerationRegistryContract contract = epoch.registryContract();
|
||||||
|
for (GenerationRegistryContract.PhysicalResourceKey key : contract.definitions().keySet()) {
|
||||||
|
if (GenerationRegistryContractFactory.BIOME_REGISTRY.equals(key.registryKey())) {
|
||||||
|
keys.add(key.resourceKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Set.copyOf(keys);
|
||||||
|
}
|
||||||
|
|
||||||
private static ModdedDimensionMetadata.DimensionMetadata engineHeights(Engine engine) {
|
private static ModdedDimensionMetadata.DimensionMetadata engineHeights(Engine engine) {
|
||||||
IrisDimension dimension = engine.getDimension();
|
IrisDimension dimension = engine.getDimension();
|
||||||
int minY = engine.getMinHeight();
|
int minY = engine.getMinHeight();
|
||||||
@@ -713,8 +821,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
if (configuredPack != null) {
|
if (configuredPack != null) {
|
||||||
return configuredPack;
|
return configuredPack;
|
||||||
}
|
}
|
||||||
PackValidationRegistry.requireLoadable(activePack);
|
Path packRoot = immutablePackRoot;
|
||||||
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
|
File packDirectory;
|
||||||
|
if (packRoot == null) {
|
||||||
|
PackValidationRegistry.requireLoadable(activePack);
|
||||||
|
packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
|
||||||
|
} else {
|
||||||
|
PackValidationRegistry.requireLoadable(packRoot);
|
||||||
|
packDirectory = packRoot.toFile();
|
||||||
|
}
|
||||||
IrisData data = IrisData.get(packDirectory);
|
IrisData data = IrisData.get(packDirectory);
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
|
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
@@ -800,36 +915,54 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
|
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
|
||||||
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
|
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
|
||||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = biome.value().getMobSettings().getMobs(category);
|
Engine current = engine();
|
||||||
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
biome, structureManager, category, pos);
|
current, pos.getX(), pos.getZ(), "modded_mob_spawn_table");
|
||||||
if (resolvedSpawns != explicitSpawns) {
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_mob_spawn_table");
|
||||||
return resolvedSpawns;
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
}
|
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns =
|
||||||
|
biome.value().getMobSettings().getMobs(category);
|
||||||
|
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
|
||||||
|
biome, structureManager, category, pos);
|
||||||
|
if (resolvedSpawns != explicitSpawns) {
|
||||||
|
return resolvedSpawns;
|
||||||
|
}
|
||||||
|
|
||||||
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||||
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
|
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
|
||||||
if (vanillaSpawnBiome == null) {
|
if (vanillaSpawnBiome == null) {
|
||||||
return explicitSpawns;
|
return explicitSpawns;
|
||||||
}
|
}
|
||||||
|
|
||||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category);
|
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns =
|
||||||
if (explicitSpawns.isEmpty()) {
|
vanillaSpawnBiome.value().getMobSettings().getMobs(category);
|
||||||
return vanillaSpawns;
|
if (explicitSpawns.isEmpty()) {
|
||||||
}
|
return vanillaSpawns;
|
||||||
if (vanillaSpawns.isEmpty()) {
|
}
|
||||||
return explicitSpawns;
|
if (vanillaSpawns.isEmpty()) {
|
||||||
}
|
return explicitSpawns;
|
||||||
|
}
|
||||||
|
|
||||||
return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns);
|
return spawnTables.mergedSpawnTable(
|
||||||
|
current,
|
||||||
|
biome.value(),
|
||||||
|
category,
|
||||||
|
vanillaSpawns,
|
||||||
|
explicitSpawns
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
|
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
|
||||||
StructureManager structureManager, ChunkAccess chunk) {
|
StructureManager structureManager, ChunkAccess chunk) {
|
||||||
Engine current = engine();
|
Engine current = engine();
|
||||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes");
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
current, chunkPos.x(), chunkPos.z(), "modded_create_biomes");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
|
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
|
||||||
return CompletableFuture.completedFuture(chunk);
|
return CompletableFuture.completedFuture(chunk);
|
||||||
@@ -844,19 +977,35 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
ModdedIrisLog.debug("Iris generating chunk {},{}", pos.x(), pos.z());
|
ModdedIrisLog.debug("Iris generating chunk {},{}", pos.x(), pos.z());
|
||||||
|
|
||||||
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
generationEngine, pos.x(), pos.z(), "modded_chunk_pipeline");
|
||||||
|
|
||||||
if (ModdedGenPool.parallelChunkSystem()) {
|
try {
|
||||||
return CompletableFuture.completedFuture(
|
if (ModdedGenPool.parallelChunkSystem()) {
|
||||||
generateTerrain(chunk, generationEngine, pos, air));
|
try {
|
||||||
|
return CompletableFuture.completedFuture(
|
||||||
|
generateTerrain(chunk, generationEngine, pos, air, route));
|
||||||
|
} finally {
|
||||||
|
if (route != null) {
|
||||||
|
route.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CompletableFuture<ChunkAccess> pipeline = CompletableFuture.supplyAsync(
|
||||||
|
() -> generateTerrain(chunk, generationEngine, pos, air, route),
|
||||||
|
ModdedGenPool.pool());
|
||||||
|
return closeRouteOnCompletion(pipeline, route);
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
closeHistoryRoute(route, failure);
|
||||||
|
throw failure;
|
||||||
}
|
}
|
||||||
return CompletableFuture.supplyAsync(
|
|
||||||
() -> generateTerrain(chunk, generationEngine, pos, air),
|
|
||||||
ModdedGenPool.pool());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
||||||
PlatformBlockState air) {
|
PlatformBlockState air,
|
||||||
try (GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
|
GenerationHistoryRuntimeRouter.RuntimeRoute route) {
|
||||||
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
|
||||||
IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) {
|
||||||
if (announced.compareAndSet(false, true)) {
|
if (announced.compareAndSet(false, true)) {
|
||||||
ModdedIrisLog.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
|
ModdedIrisLog.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
|
||||||
@@ -875,6 +1024,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
Heightmap.Types.MOTION_BLOCKING,
|
Heightmap.Types.MOTION_BLOCKING,
|
||||||
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
||||||
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
||||||
|
if (route != null) {
|
||||||
|
route.claimGeneratedSemantics();
|
||||||
|
}
|
||||||
return chunk;
|
return chunk;
|
||||||
} catch (GenerationSessionException e) {
|
} catch (GenerationSessionException e) {
|
||||||
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
|
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
|
||||||
@@ -890,6 +1042,67 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<ChunkAccess> closeRouteOnCompletion(
|
||||||
|
CompletableFuture<ChunkAccess> pipeline,
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route
|
||||||
|
) {
|
||||||
|
if (route == null) {
|
||||||
|
return pipeline;
|
||||||
|
}
|
||||||
|
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
|
||||||
|
pipeline.whenComplete((ChunkAccess chunk, Throwable failure) -> {
|
||||||
|
boolean cancelled = isCancellationFailure(failure);
|
||||||
|
Throwable completionFailure = failure;
|
||||||
|
try {
|
||||||
|
route.close();
|
||||||
|
} catch (Throwable closeFailure) {
|
||||||
|
completionFailure = appendFailure(completionFailure, closeFailure);
|
||||||
|
}
|
||||||
|
if (completionFailure == null) {
|
||||||
|
completion.complete(chunk);
|
||||||
|
} else if (cancelled) {
|
||||||
|
completion.cancel(false);
|
||||||
|
} else {
|
||||||
|
completion.completeExceptionally(completionFailure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return completion;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void closeHistoryRoute(
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||||
|
Throwable failure
|
||||||
|
) {
|
||||||
|
if (route == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
route.close();
|
||||||
|
} catch (Throwable closeFailure) {
|
||||||
|
if (failure != closeFailure) {
|
||||||
|
failure.addSuppressed(closeFailure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Throwable appendFailure(Throwable failure, Throwable closeFailure) {
|
||||||
|
if (failure == null) {
|
||||||
|
return closeFailure;
|
||||||
|
}
|
||||||
|
if (failure != closeFailure) {
|
||||||
|
failure.addSuppressed(closeFailure);
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCancellationFailure(Throwable failure) {
|
||||||
|
Throwable current = failure;
|
||||||
|
while (current instanceof CompletionException && current.getCause() != null) {
|
||||||
|
current = current.getCause();
|
||||||
|
}
|
||||||
|
return current instanceof CancellationException;
|
||||||
|
}
|
||||||
|
|
||||||
private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
|
private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
|
||||||
int baseX = pos.getMinBlockX();
|
int baseX = pos.getMinBlockX();
|
||||||
int baseZ = pos.getMinBlockZ();
|
int baseZ = pos.getMinBlockZ();
|
||||||
@@ -961,18 +1174,47 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) {
|
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) {
|
||||||
|
Engine current = engine(region.getLevel());
|
||||||
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
current, chunkPos.x(), chunkPos.z(), "modded_apply_carvers");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_apply_carvers");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) {
|
public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) {
|
||||||
|
Engine current = engine(region.getLevel());
|
||||||
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
current, chunkPos.x(), chunkPos.z(), "modded_build_surface");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_build_surface");
|
||||||
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
||||||
Engine current = engine();
|
Engine current = engine(level.getLevel());
|
||||||
importedFeatures.prepare(current);
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
current, chunkPos.x(), chunkPos.z(), "modded_biome_decoration");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
|
if (!allowsRoutedDiscreteGeneration(
|
||||||
|
current,
|
||||||
|
route,
|
||||||
|
chunkPos,
|
||||||
|
chunk,
|
||||||
|
ChunkStatus.FEATURES
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
importedFeatures.prepare(current);
|
||||||
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
|
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
|
||||||
// Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk
|
// Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk
|
||||||
// step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the
|
// step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the
|
||||||
@@ -984,8 +1226,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||||
Engine current = engine(levelKey);
|
Engine current = engine(levelKey);
|
||||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
current, chunkPos.x(), chunkPos.z(), "modded_create_structures");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
|
if (!allowsRoutedDiscreteGeneration(
|
||||||
|
current,
|
||||||
|
route,
|
||||||
|
chunkPos,
|
||||||
|
chunk,
|
||||||
|
ChunkStatus.STRUCTURE_STARTS
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
||||||
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
||||||
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
|
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
|
||||||
@@ -1008,7 +1263,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
|
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
|
||||||
Engine current = engine(level.getLevel());
|
Engine current = engine(level.getLevel());
|
||||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
|
current, chunkPos.x(), chunkPos.z(), "modded_create_references");
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
NativeStructureReferenceRepair.createReferences(
|
NativeStructureReferenceRepair.createReferences(
|
||||||
current, level, structureManager, chunk);
|
current, level, structureManager, chunk);
|
||||||
@@ -1021,25 +1280,42 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||||
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
|
|
||||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
|
||||||
ChunkPos center = region.getCenter();
|
ChunkPos center = region.getCenter();
|
||||||
Holder<Biome> visibleBiome;
|
Engine current = engine(region.getLevel());
|
||||||
if (engine == null || engine.getDimensionStackContext() == null) {
|
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
current, center.x(), center.z(), "modded_spawn_original_mobs");
|
||||||
} else {
|
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||||
visibleBiome = structureBiomeSource.getVisibleSurfaceBiome(
|
GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_original_mobs");
|
||||||
center.getMinBlockX() + 8,
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
center.getMinBlockZ() + 8);
|
ChunkAccess centerChunk = region.getChunk(center.x(), center.z());
|
||||||
if (visibleBiome == null) {
|
if (!allowsRoutedDiscreteGeneration(
|
||||||
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
current,
|
||||||
|
route,
|
||||||
|
center,
|
||||||
|
centerChunk,
|
||||||
|
ChunkStatus.SPAWN
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
|
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||||
|
Holder<Biome> visibleBiome;
|
||||||
|
if (current.getDimensionStackContext() == null) {
|
||||||
|
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||||
|
} else {
|
||||||
|
visibleBiome = structureBiomeSource.getVisibleSurfaceBiome(
|
||||||
|
center.getMinBlockX() + 8,
|
||||||
|
center.getMinBlockZ() + 8);
|
||||||
|
if (visibleBiome == null) {
|
||||||
|
visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
|
||||||
|
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
||||||
|
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
||||||
|
NaturalSpawner.spawnMobsForChunkGeneration(
|
||||||
|
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
|
||||||
}
|
}
|
||||||
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(visibleBiome.value());
|
|
||||||
WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(RandomSupport.generateUniqueSeed()));
|
|
||||||
random.setDecorationSeed(region.getSeed(), center.getMinBlockX(), center.getMinBlockZ());
|
|
||||||
NaturalSpawner.spawnMobsForChunkGeneration(
|
|
||||||
region, vanillaBiome == null ? visibleBiome : vanillaBiome, center, random);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1075,7 +1351,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||||
Engine current = requireDataQueryEngine("base height");
|
Engine current = requireDataQueryEngine("base height");
|
||||||
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
|
current, x, z, "modded_base_height");
|
||||||
|
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
int height = current.getDimensionStackContext() == null
|
int height = current.getDimensionStackContext() == null
|
||||||
? current.getHeight(x, z, ignoreFluid)
|
? current.getHeight(x, z, ignoreFluid)
|
||||||
@@ -1091,7 +1369,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
int minY = heightAccessor.getMinY();
|
int minY = heightAccessor.getMinY();
|
||||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||||
Engine current = requireDataQueryEngine("base column");
|
Engine current = requireDataQueryEngine("base column");
|
||||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||||
|
current, x, z, "modded_base_column");
|
||||||
|
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
||||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||||
boolean dimensionStack = current.getDimensionStackContext() != null;
|
boolean dimensionStack = current.getDimensionStackContext() != null;
|
||||||
@@ -1114,6 +1394,115 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean allowsRoutedDiscreteGeneration(
|
||||||
|
Engine current,
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||||
|
ChunkPos chunkPos,
|
||||||
|
ChunkAccess chunk,
|
||||||
|
ChunkStatus stage
|
||||||
|
) {
|
||||||
|
if (chunk.getPersistedStatus().isOrAfter(stage)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (route == null) {
|
||||||
|
return allowsGenerationHistoryBypass(current);
|
||||||
|
}
|
||||||
|
return current.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindGenerationRuntimeRetirementListener(Engine current) {
|
||||||
|
if (!(current instanceof IrisEngine irisEngine)) {
|
||||||
|
unbindGenerationRuntimeRetirementListener();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IrisEngine previous = retirementListenerEngine;
|
||||||
|
if (previous == irisEngine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (previous != null) {
|
||||||
|
previous.removeGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
|
||||||
|
}
|
||||||
|
irisEngine.addGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
|
||||||
|
retirementListenerEngine = irisEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unbindGenerationRuntimeRetirementListener() {
|
||||||
|
IrisEngine previous = retirementListenerEngine;
|
||||||
|
if (previous == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
retirementListenerEngine = null;
|
||||||
|
previous.removeGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void retireGenerationRuntimeCaches(int runtimeIdentity) {
|
||||||
|
structureBiomeSource.evictRuntime(runtimeIdentity);
|
||||||
|
importedFeatures.evictRuntime(runtimeIdentity);
|
||||||
|
spawnTables.evictRuntime(runtimeIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(
|
||||||
|
Engine current,
|
||||||
|
int chunkX,
|
||||||
|
int chunkZ,
|
||||||
|
String operation
|
||||||
|
) {
|
||||||
|
if (allowsGenerationHistoryBypass(current)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GenerationHistoryRuntimeRouter router = requireHistoryRouter(current, operation);
|
||||||
|
try {
|
||||||
|
return router.openRoute(chunkX, chunkZ);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris " + operation + " could not route chunk "
|
||||||
|
+ chunkX + "," + chunkZ + " through generation history.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||||
|
Engine current,
|
||||||
|
int blockX,
|
||||||
|
int blockZ,
|
||||||
|
String operation
|
||||||
|
) {
|
||||||
|
if (allowsGenerationHistoryBypass(current)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GenerationHistoryRuntimeRouter router = requireHistoryRouter(current, operation);
|
||||||
|
try {
|
||||||
|
return router.openCoordinateScope(blockX, blockZ);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris " + operation + " could not route block "
|
||||||
|
+ blockX + "," + blockZ + " through generation history.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenerationHistoryRuntimeRouter requireHistoryRouter(Engine current, String operation) {
|
||||||
|
if (!(current instanceof IrisEngine irisEngine)) {
|
||||||
|
throw new IllegalStateException("Iris " + operation + " requires an IrisEngine runtime.");
|
||||||
|
}
|
||||||
|
return irisEngine.getGenerationHistoryRuntimeRouter().orElseThrow(() ->
|
||||||
|
new IllegalStateException("Iris " + operation
|
||||||
|
+ " requires an attached generation-history runtime router."));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean allowsGenerationHistoryBypass(Engine current) {
|
||||||
|
if (generationMode != ModdedGenerationMode.TRANSIENT_STUDIO) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (current instanceof IrisEngine irisEngine && irisEngine.isStudio()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Only an explicitly transient Iris Studio engine may bypass "
|
||||||
|
+ "generation-history routing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope(
|
||||||
|
GenerationHistoryRuntimeRouter.RuntimeRoute route
|
||||||
|
) {
|
||||||
|
return route == null ? null : route.openRuntimeScope();
|
||||||
|
}
|
||||||
|
|
||||||
GenerationSessionLease requireGenerationLease(Engine current, String operation) {
|
GenerationSessionLease requireGenerationLease(Engine current, String operation) {
|
||||||
try {
|
try {
|
||||||
return current.acquireGenerationLease(operation);
|
return current.acquireGenerationLease(operation);
|
||||||
|
|||||||
@@ -142,6 +142,19 @@ public final class MainWorldService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Path configuredWorldRootIfPresent() throws IOException {
|
||||||
|
Path instanceRoot = Path.of("").toAbsolutePath().normalize();
|
||||||
|
Path properties = instanceRoot.resolve(PROPERTIES_NAME);
|
||||||
|
if (!Files.isRegularFile(properties)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return resolveWorldRoot(instanceRoot, properties);
|
||||||
|
} catch (MissingWorldRootException missing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* net.minecraft.server.Main reads server.properties as Paths.get("server.properties"), so the authoritative
|
* net.minecraft.server.Main reads server.properties as Paths.get("server.properties"), so the authoritative
|
||||||
* instance root is the JVM working directory - not configDir().getParent(), which points somewhere else
|
* instance root is the JVM working directory - not configDir().getParent(), which points somewhere else
|
||||||
|
|||||||
+73
-16
@@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.DimensionStackContext;
|
||||||
|
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
@@ -33,6 +36,7 @@ import net.minecraft.world.level.biome.Biome;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
@@ -98,11 +102,11 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
Engine engine = context.getEngine();
|
Engine engine = context.getEngine();
|
||||||
String prefix = engine.getDimension().getCustomBiomeKeyPrefix();
|
BiomeOwner owner = findCustomBiomeOwner(engine, key);
|
||||||
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
|
if (owner != null) {
|
||||||
return key;
|
return owner.physicalKey();
|
||||||
}
|
}
|
||||||
return ModdedWorldgenIds.biomeRef(engine, key.substring(prefix.length()));
|
return engine.getData().physicalBiomeResourceKey(engine.getDimension(), key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -139,31 +143,81 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int idForDerivative(Registry<Biome> registry, String key) {
|
private int idForDerivative(Registry<Biome> registry, String key) {
|
||||||
IrisBiome owner = findCustomBiomeOwner(key);
|
BiomeOwner owner = findCustomBiomeOwner(key);
|
||||||
if (owner == null) {
|
if (owner == null) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
String derivativeKey = owner.getVanillaDerivativeKey();
|
String derivativeKey = owner.biome().getVanillaDerivativeKey();
|
||||||
if (derivativeKey == null) {
|
if (derivativeKey == null) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return idForKey(registry, derivativeKey);
|
return idForKey(registry, derivativeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IrisBiome findCustomBiomeOwner(String key) {
|
private BiomeOwner findCustomBiomeOwner(String key) {
|
||||||
for (Engine engine : ModdedWorldEngines.activeEngines()) {
|
for (Engine engine : ModdedWorldEngines.activeEngines()) {
|
||||||
if (engine == null || engine.isClosed()) {
|
if (engine == null || engine.isClosed()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
IrisDimension dimension = engine.getDimension();
|
BiomeOwner owner = findCustomBiomeOwner(engine, key);
|
||||||
for (IrisBiome biome : dimension.getAllBiomes(engine)) {
|
if (owner != null) {
|
||||||
if (!biome.isCustom()) {
|
return owner;
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
|
return null;
|
||||||
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))) {
|
}
|
||||||
return biome;
|
|
||||||
}
|
private BiomeOwner findCustomBiomeOwner(Engine engine, String key) {
|
||||||
|
IrisDimension hostDimension = engine.getDimension();
|
||||||
|
BiomeOwner owner = findCustomBiomeOwner(
|
||||||
|
hostDimension,
|
||||||
|
engine.getData(),
|
||||||
|
hostDimension.getAllBiomes(engine),
|
||||||
|
key
|
||||||
|
);
|
||||||
|
if (owner != null) {
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||||
|
if (stackContext == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
|
||||||
|
if (terrainContext.isSelfReferencing()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IrisDimension dimension = terrainContext.getDimension();
|
||||||
|
owner = findCustomBiomeOwner(
|
||||||
|
dimension,
|
||||||
|
terrainContext.getData(),
|
||||||
|
dimension.getAllBiomes(terrainContext),
|
||||||
|
key
|
||||||
|
);
|
||||||
|
if (owner != null) {
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BiomeOwner findCustomBiomeOwner(
|
||||||
|
IrisDimension dimension,
|
||||||
|
IrisData data,
|
||||||
|
Iterable<IrisBiome> biomes,
|
||||||
|
String key
|
||||||
|
) {
|
||||||
|
for (IrisBiome biome : biomes) {
|
||||||
|
if (!biome.isCustom()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
|
||||||
|
String contractLogicalKey = dimension.getLoadKey().toLowerCase(Locale.ROOT)
|
||||||
|
+ ":" + custom.getId().toLowerCase(Locale.ROOT);
|
||||||
|
String physicalKey = data.customBiomeResourceKey(dimension, custom);
|
||||||
|
if (key.equalsIgnoreCase(dimension.getCustomBiomeKey(custom.getId()))
|
||||||
|
|| key.equalsIgnoreCase(contractLogicalKey)
|
||||||
|
|| key.equalsIgnoreCase(physicalKey)) {
|
||||||
|
return new BiomeOwner(dimension, data, biome, physicalKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,4 +274,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
|||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record BiomeOwner(IrisDimension dimension, IrisData data, IrisBiome biome, String physicalKey) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+279
-53
@@ -19,7 +19,9 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.history.GenerationActivation;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
@@ -47,9 +49,12 @@ import net.minecraft.world.level.storage.LevelStorageSource;
|
|||||||
import net.minecraft.world.level.storage.ServerLevelData;
|
import net.minecraft.world.level.storage.ServerLevelData;
|
||||||
import net.minecraft.world.level.storage.WorldData;
|
import net.minecraft.world.level.storage.WorldData;
|
||||||
|
|
||||||
import java.io.File;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalLong;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -120,14 +125,76 @@ public final class ModdedDimensionManager {
|
|||||||
return generator.commandEngine();
|
return generator.commandEngine();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
public static Handle createTransientStudio(
|
||||||
ModdedStartup.requirePackForWorldCreation(pack);
|
MinecraftServer server,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
String packDimensionKey,
|
||||||
|
long seed
|
||||||
|
) {
|
||||||
|
return create(
|
||||||
|
server,
|
||||||
|
dimensionId,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed,
|
||||||
|
ModdedGenerationMode.TRANSIENT_STUDIO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Handle restorePersistent(
|
||||||
|
MinecraftServer server,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
String packDimensionKey,
|
||||||
|
long seed
|
||||||
|
) {
|
||||||
|
return create(
|
||||||
|
server,
|
||||||
|
dimensionId,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed,
|
||||||
|
ModdedGenerationMode.PERSISTENT_RESTORE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Handle create(
|
||||||
|
MinecraftServer server,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
String packDimensionKey,
|
||||||
|
long seed,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
ModdedServerAccess serverAccess = requireAccess();
|
ModdedServerAccess serverAccess = requireAccess();
|
||||||
synchronized (LOCK) {
|
synchronized (LOCK) {
|
||||||
ResourceKey<Level> key = levelKey(dimensionId);
|
ResourceKey<Level> key = levelKey(dimensionId);
|
||||||
|
RuntimePack runtimePack = switch (generationMode) {
|
||||||
|
case TRANSIENT_STUDIO -> transientStudioPack(pack, packDimensionKey);
|
||||||
|
case PERSISTENT_CREATE -> persistentRuntimePack(
|
||||||
|
ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
server,
|
||||||
|
key,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed
|
||||||
|
),
|
||||||
|
generationMode
|
||||||
|
);
|
||||||
|
case PERSISTENT_RESTORE -> persistentRuntimePack(
|
||||||
|
ModdedGenerationHistoryStorage.restoreOrAdopt(
|
||||||
|
server,
|
||||||
|
key,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed
|
||||||
|
),
|
||||||
|
generationMode
|
||||||
|
);
|
||||||
|
};
|
||||||
Handle existing = HANDLES.get(dimensionId);
|
Handle existing = HANDLES.get(dimensionId);
|
||||||
if (existing != null && serverAccess.hasLevel(server, key)) {
|
if (existing != null && serverAccess.hasLevel(server, key)) {
|
||||||
existing.generator().repointAndBind(existing.level(), pack, packDimensionKey, seed);
|
|
||||||
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
|
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
|
||||||
HANDLES.put(dimensionId, refreshed);
|
HANDLES.put(dimensionId, refreshed);
|
||||||
return refreshed;
|
return refreshed;
|
||||||
@@ -138,14 +205,22 @@ public final class ModdedDimensionManager {
|
|||||||
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
|
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
|
||||||
}
|
}
|
||||||
ModdedIrisLog.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
|
ModdedIrisLog.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
|
||||||
generator.repointAndBind(present, pack, packDimensionKey, seed);
|
|
||||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
|
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
|
||||||
HANDLES.put(dimensionId, handle);
|
HANDLES.put(dimensionId, handle);
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Handle handle = inject(server, serverAccess, dimensionId, key, pack, packDimensionKey, seed);
|
Handle handle = inject(
|
||||||
|
server,
|
||||||
|
serverAccess,
|
||||||
|
dimensionId,
|
||||||
|
key,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed,
|
||||||
|
runtimePack
|
||||||
|
);
|
||||||
HANDLES.put(dimensionId, handle);
|
HANDLES.put(dimensionId, handle);
|
||||||
ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
|
ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
|
||||||
return handle;
|
return handle;
|
||||||
@@ -157,28 +232,109 @@ public final class ModdedDimensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||||
ModdedDimensionRegistryStore.PersistentDimension previous =
|
synchronized (LOCK) {
|
||||||
ModdedDimensionRegistryStore.get(server, dimensionId);
|
ModdedDimensionRegistryStore.PersistentDimension previous =
|
||||||
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed));
|
ModdedDimensionRegistryStore.get(server, dimensionId);
|
||||||
try {
|
if (previous != null) {
|
||||||
return create(server, dimensionId, pack, packDimensionKey, seed);
|
throw new IllegalStateException("Iris dimension '" + dimensionId
|
||||||
} catch (Throwable e) {
|
+ "' already exists. Use the Iris world update command to stage a pack update.");
|
||||||
|
}
|
||||||
|
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(
|
||||||
|
dimensionId,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed
|
||||||
|
));
|
||||||
try {
|
try {
|
||||||
if (previous == null) {
|
return create(
|
||||||
|
server,
|
||||||
|
dimensionId,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
seed,
|
||||||
|
ModdedGenerationMode.PERSISTENT_CREATE
|
||||||
|
);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
try {
|
||||||
ModdedDimensionRegistryStore.remove(server, dimensionId);
|
ModdedDimensionRegistryStore.remove(server, dimensionId);
|
||||||
} else {
|
} catch (Throwable rollbackFailure) {
|
||||||
ModdedDimensionRegistryStore.put(server, previous);
|
e.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
} catch (Throwable rollbackFailure) {
|
if (e instanceof RuntimeException runtimeException) {
|
||||||
e.addSuppressed(rollbackFailure);
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
if (e instanceof Error fatalError) {
|
||||||
|
throw fatalError;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
|
||||||
}
|
}
|
||||||
if (e instanceof RuntimeException runtimeException) {
|
}
|
||||||
throw runtimeException;
|
}
|
||||||
|
|
||||||
|
public static UpdateResult stagePersistentUpdate(
|
||||||
|
MinecraftServer server,
|
||||||
|
String dimensionId,
|
||||||
|
String pack,
|
||||||
|
String packDimensionKey
|
||||||
|
) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
ModdedDimensionRegistryStore.PersistentDimension previous =
|
||||||
|
ModdedDimensionRegistryStore.get(server, dimensionId);
|
||||||
|
if (previous == null) {
|
||||||
|
throw new IllegalStateException("Iris dimension '" + dimensionId
|
||||||
|
+ "' is not registered. Create it before staging an update.");
|
||||||
}
|
}
|
||||||
if (e instanceof Error fatalError) {
|
ModdedDimensionRegistryStore.PersistentDimension updated =
|
||||||
throw fatalError;
|
new ModdedDimensionRegistryStore.PersistentDimension(
|
||||||
|
dimensionId,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
previous.seed()
|
||||||
|
);
|
||||||
|
ModdedDimensionRegistryStore.put(server, updated);
|
||||||
|
try {
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack active =
|
||||||
|
ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
server,
|
||||||
|
levelKey(dimensionId),
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
previous.seed()
|
||||||
|
);
|
||||||
|
Optional<GenerationActivation> pending = active.history().pendingActivation();
|
||||||
|
long activeActivationId = active.history().activeActivation().activationId();
|
||||||
|
OptionalLong pendingActivationId = pending.isPresent()
|
||||||
|
? OptionalLong.of(pending.orElseThrow().activationId())
|
||||||
|
: OptionalLong.empty();
|
||||||
|
UpdateResult result = new UpdateResult(
|
||||||
|
pending.isPresent(),
|
||||||
|
pending.isPresent(),
|
||||||
|
activeActivationId,
|
||||||
|
pendingActivationId
|
||||||
|
);
|
||||||
|
if (result.restartRequired()) {
|
||||||
|
ModdedIrisLog.warn("Iris dimension '{}' staged generation activation {} from pack={}:{}. Restart the server to activate it.",
|
||||||
|
dimensionId, pendingActivationId.orElseThrow(), pack, packDimensionKey);
|
||||||
|
} else {
|
||||||
|
ModdedIrisLog.info("Iris dimension '{}' already uses pack={}:{}; no generation update was staged.",
|
||||||
|
dimensionId, pack, packDimensionKey);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
try {
|
||||||
|
ModdedDimensionRegistryStore.put(server, previous);
|
||||||
|
} catch (Throwable rollbackFailure) {
|
||||||
|
failure.addSuppressed(rollbackFailure);
|
||||||
|
}
|
||||||
|
if (failure instanceof RuntimeException runtimeException) {
|
||||||
|
throw runtimeException;
|
||||||
|
}
|
||||||
|
if (failure instanceof Error fatalError) {
|
||||||
|
throw fatalError;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Iris generation update staging failed for "
|
||||||
|
+ dimensionId + ".", failure);
|
||||||
}
|
}
|
||||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,48 +680,47 @@ public final class ModdedDimensionManager {
|
|||||||
server.execute(task);
|
server.execute(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
|
private static Holder<DimensionType> resolveDimensionType(
|
||||||
|
RegistryAccess registryAccess,
|
||||||
|
String pack,
|
||||||
|
String packDimensionKey,
|
||||||
|
RuntimePack runtimePack
|
||||||
|
) {
|
||||||
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
|
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
|
||||||
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
|
String typeRef = runtimePack.dimensionTypeKey();
|
||||||
String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey);
|
|
||||||
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
|
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
|
||||||
ModdedRuntimeRegistry.ensureCustomBiomes(registryAccess, dimension, pack);
|
|
||||||
ModdedRuntimeRegistry.ensureDimensionType(registry, typeKey, typeRef);
|
|
||||||
return ModdedForcedDatapack.requireRegisteredDimensionType(
|
return ModdedForcedDatapack.requireRegisteredDimensionType(
|
||||||
typeRef, registry.get(typeKey), pack, packDimensionKey);
|
typeRef, registry.get(typeKey), pack, packDimensionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
|
private static Handle inject(
|
||||||
try {
|
MinecraftServer server,
|
||||||
File packFolder = ModdedWorldEngines.packFolder(pack);
|
ModdedServerAccess serverAccess,
|
||||||
if (!packFolder.isDirectory()) {
|
String dimensionId,
|
||||||
throw new IllegalStateException("Iris pack directory is missing: " + packFolder.getAbsolutePath());
|
ResourceKey<Level> key,
|
||||||
}
|
String pack,
|
||||||
IrisDimension dimension = IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
|
String packDimensionKey,
|
||||||
if (dimension == null) {
|
long seed,
|
||||||
throw new IllegalStateException("Iris pack '" + pack
|
RuntimePack runtimePack
|
||||||
+ "' does not contain dimension '" + packDimensionKey + "'");
|
) {
|
||||||
}
|
|
||||||
return dimension;
|
|
||||||
} catch (Throwable e) {
|
|
||||||
ModdedIrisLog.error("Iris could not load pack '{}' dimension '{}' for dimension type resolution",
|
|
||||||
pack, packDimensionKey, e);
|
|
||||||
if (e instanceof Error fatalError) {
|
|
||||||
throw fatalError;
|
|
||||||
}
|
|
||||||
throw new IllegalStateException("Iris could not load pack '" + pack + "' dimension '"
|
|
||||||
+ packDimensionKey + "' for dimension type resolution", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String pack, String packDimensionKey, long seed) {
|
|
||||||
RegistryAccess registryAccess = server.registryAccess();
|
RegistryAccess registryAccess = server.registryAccess();
|
||||||
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess, pack, packDimensionKey);
|
Holder<DimensionType> dimensionType = resolveDimensionType(
|
||||||
|
registryAccess,
|
||||||
|
pack,
|
||||||
|
packDimensionKey,
|
||||||
|
runtimePack
|
||||||
|
);
|
||||||
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
|
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
|
||||||
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
|
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
|
||||||
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
|
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
|
||||||
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef);
|
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef);
|
||||||
generator.repoint(pack, packDimensionKey, seed);
|
generator.repoint(
|
||||||
|
pack,
|
||||||
|
runtimePack.dimensionKey(),
|
||||||
|
seed,
|
||||||
|
runtimePack.packRoot(),
|
||||||
|
runtimePack.generationMode()
|
||||||
|
);
|
||||||
LevelStem stem = new LevelStem(dimensionType, generator);
|
LevelStem stem = new LevelStem(dimensionType, generator);
|
||||||
|
|
||||||
WorldData worldData = server.getWorldData();
|
WorldData worldData = server.getWorldData();
|
||||||
@@ -681,6 +836,77 @@ public final class ModdedDimensionManager {
|
|||||||
return bound;
|
return bound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RuntimePack persistentRuntimePack(
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack active,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
|
return new RuntimePack(
|
||||||
|
active.packRoot(),
|
||||||
|
active.dimensionKey(),
|
||||||
|
active.dimensionContract().dimensionTypeKey(),
|
||||||
|
generationMode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimePack transientStudioPack(String pack, String dimensionKey) {
|
||||||
|
ModdedStartup.requirePackForWorldCreation(pack);
|
||||||
|
Path packRoot = ModdedWorldEngines.resolvePack(pack, dimensionKey)
|
||||||
|
.toPath()
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
PackValidationRegistry.requireLoadable(packRoot);
|
||||||
|
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
|
||||||
|
try {
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IllegalStateException("Transient Iris Studio pack does not contain dimension '"
|
||||||
|
+ dimensionKey + "'.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
return new RuntimePack(
|
||||||
|
packRoot,
|
||||||
|
dimensionKey,
|
||||||
|
ModdedWorldgenIds.dimensionTypeRef(pack, dimensionKey),
|
||||||
|
ModdedGenerationMode.TRANSIENT_STUDIO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RuntimePack(
|
||||||
|
Path packRoot,
|
||||||
|
String dimensionKey,
|
||||||
|
String dimensionTypeKey,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
|
private RuntimePack {
|
||||||
|
packRoot = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
|
||||||
|
dimensionKey = Objects.requireNonNull(dimensionKey, "dimensionKey");
|
||||||
|
dimensionTypeKey = Objects.requireNonNull(dimensionTypeKey, "dimensionTypeKey");
|
||||||
|
generationMode = Objects.requireNonNull(generationMode, "generationMode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
|
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record UpdateResult(
|
||||||
|
boolean pending,
|
||||||
|
boolean restartRequired,
|
||||||
|
long activeActivationId,
|
||||||
|
OptionalLong pendingActivationId
|
||||||
|
) {
|
||||||
|
public UpdateResult {
|
||||||
|
pendingActivationId = Objects.requireNonNull(
|
||||||
|
pendingActivationId,
|
||||||
|
"pendingActivationId"
|
||||||
|
);
|
||||||
|
if (pending != pendingActivationId.isPresent()) {
|
||||||
|
throw new IllegalArgumentException("Pending update status must match the pending activation ID.");
|
||||||
|
}
|
||||||
|
if (restartRequired != pending) {
|
||||||
|
throw new IllegalArgumentException("A pending generation activation requires a restart.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+37
-3
@@ -19,6 +19,9 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.DimensionStackContext;
|
||||||
|
import art.arcane.iris.engine.DimensionTerrainContext;
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
@@ -28,6 +31,7 @@ import net.minecraft.resources.Identifier;
|
|||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
final class ModdedDimensionMetadata {
|
final class ModdedDimensionMetadata {
|
||||||
private ModdedDimensionMetadata() {
|
private ModdedDimensionMetadata() {
|
||||||
@@ -45,7 +49,10 @@ final class ModdedDimensionMetadata {
|
|||||||
|
|
||||||
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
|
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
|
||||||
LinkedHashSet<String> keys = new LinkedHashSet<>(
|
LinkedHashSet<String> keys = new LinkedHashSet<>(
|
||||||
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
|
collectConfiguredBiomeKeys(
|
||||||
|
dimension.getReachableBiomes(() -> data),
|
||||||
|
customBiome -> data.customBiomeResourceKey(dimension, customBiome)
|
||||||
|
));
|
||||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||||
if (region == null) {
|
if (region == null) {
|
||||||
continue;
|
continue;
|
||||||
@@ -60,7 +67,27 @@ final class ModdedDimensionMetadata {
|
|||||||
return Set.copyOf(keys);
|
return Set.copyOf(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
|
static Set<String> collectConfiguredBiomeKeys(Engine engine) {
|
||||||
|
LinkedHashSet<String> keys = new LinkedHashSet<>(collectConfiguredBiomeKeys(
|
||||||
|
engine.getDimension(), engine.getData()));
|
||||||
|
DimensionStackContext stackContext = engine.getDimensionStackContext();
|
||||||
|
if (stackContext == null) {
|
||||||
|
return Set.copyOf(keys);
|
||||||
|
}
|
||||||
|
for (DimensionTerrainContext terrainContext : stackContext.getLayersBottomToTop()) {
|
||||||
|
if (terrainContext.isSelfReferencing()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
keys.addAll(collectConfiguredBiomeKeys(
|
||||||
|
terrainContext.getDimension(), terrainContext.getData()));
|
||||||
|
}
|
||||||
|
return Set.copyOf(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<String> collectConfiguredBiomeKeys(
|
||||||
|
Iterable<IrisBiome> biomes,
|
||||||
|
Function<IrisBiomeCustom, String> customBiomeKey
|
||||||
|
) {
|
||||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||||
for (IrisBiome irisBiome : biomes) {
|
for (IrisBiome irisBiome : biomes) {
|
||||||
if (irisBiome == null) {
|
if (irisBiome == null) {
|
||||||
@@ -74,12 +101,19 @@ final class ModdedDimensionMetadata {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
keys.add(IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId()));
|
keys.add(customBiomeKey.apply(customBiome).toLowerCase(Locale.ROOT));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Set.copyOf(keys);
|
return Set.copyOf(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
|
||||||
|
return collectConfiguredBiomeKeys(
|
||||||
|
biomes,
|
||||||
|
customBiome -> IrisDimension.customBiomeKey(dimensionLoadKey, customBiome.getId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static int clampSpawnHeight(int minY, int height) {
|
static int clampSpawnHeight(int minY, int height) {
|
||||||
int minimum = minY + 1;
|
int minimum = minY + 1;
|
||||||
int maximum = minY + height - 2;
|
int maximum = minY + height - 2;
|
||||||
|
|||||||
+9
-1
@@ -53,6 +53,10 @@ public final class ModdedDimensionRegistryStore {
|
|||||||
return contents(file).dimensions();
|
return contents(file).dimensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<PersistentDimension> loadWorldRoot(Path worldRoot) {
|
||||||
|
return load(storeFile(worldRoot));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boot-safe load: a corrupt registry must not abort server start. The broken file is renamed aside so the
|
* Boot-safe load: a corrupt registry must not abort server start. The broken file is renamed aside so the
|
||||||
* next write starts clean, and every id we can still recognise in the raw text is reported as lost.
|
* next write starts clean, and every id we can still recognise in the raw text is reported as lost.
|
||||||
@@ -232,7 +236,11 @@ public final class ModdedDimensionRegistryStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Path storeFile(MinecraftServer server) {
|
private static Path storeFile(MinecraftServer server) {
|
||||||
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
|
return storeFile(server.getWorldPath(LevelResource.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path storeFile(Path worldRoot) {
|
||||||
|
return worldRoot.resolve("iris").resolve(FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PersistentDimension(String id, String pack, String dimension, long seed) {
|
public record PersistentDimension(String id, String pack, String dimension, long seed) {
|
||||||
|
|||||||
+3
-1
@@ -19,6 +19,7 @@
|
|||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.iris.engine.IrisEngineMantle;
|
import art.arcane.iris.engine.IrisEngineMantle;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryPaths;
|
||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
@@ -38,7 +39,8 @@ public final class ModdedDimensionStorage {
|
|||||||
"region",
|
"region",
|
||||||
"entities",
|
"entities",
|
||||||
"poi",
|
"poi",
|
||||||
IrisEngineMantle.STORAGE_FOLDER_NAME
|
IrisEngineMantle.STORAGE_FOLDER_NAME,
|
||||||
|
GenerationHistoryPaths.IRIS_DIRECTORY_NAME
|
||||||
);
|
);
|
||||||
|
|
||||||
private ModdedDimensionStorage() {
|
private ModdedDimensionStorage() {
|
||||||
|
|||||||
+366
-95
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.core.IrisDatapackCompiler;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||||
@@ -27,11 +27,25 @@ import art.arcane.iris.core.nms.datapack.IDataFixer;
|
|||||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
import art.arcane.iris.core.pack.PackValidationResult;
|
import art.arcane.iris.core.pack.PackValidationResult;
|
||||||
import art.arcane.iris.core.pack.PackValidator;
|
import art.arcane.iris.core.pack.PackValidator;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryPaths;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract.PhysicalResourceKey;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.engine.object.IrisCustomBiomeAliasResolver;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionType;
|
import art.arcane.iris.engine.object.IrisDimensionType;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import art.arcane.volmlib.util.collection.KSet;
|
import art.arcane.volmlib.util.collection.KSet;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.packs.PackLocationInfo;
|
import net.minecraft.server.packs.PackLocationInfo;
|
||||||
import net.minecraft.server.packs.PackSelectionConfig;
|
import net.minecraft.server.packs.PackSelectionConfig;
|
||||||
import net.minecraft.server.packs.PackType;
|
import net.minecraft.server.packs.PackType;
|
||||||
@@ -39,25 +53,28 @@ import net.minecraft.server.packs.PathPackResources;
|
|||||||
import net.minecraft.server.packs.repository.Pack;
|
import net.minecraft.server.packs.repository.Pack;
|
||||||
import net.minecraft.server.packs.repository.PackSource;
|
import net.minecraft.server.packs.repository.PackSource;
|
||||||
import net.minecraft.server.packs.repository.RepositorySource;
|
import net.minecraft.server.packs.repository.RepositorySource;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileVisitResult;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.SimpleFileVisitor;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HexFormat;
|
import java.util.HexFormat;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -67,14 +84,10 @@ public final class ModdedForcedDatapack {
|
|||||||
private static final String PACK_ID = "iris_worldgen";
|
private static final String PACK_ID = "iris_worldgen";
|
||||||
private static final String PACK_FOLDER = "iris";
|
private static final String PACK_FOLDER = "iris";
|
||||||
private static final String HASH_FILE_NAME = "packs.hash";
|
private static final String HASH_FILE_NAME = "packs.hash";
|
||||||
// Bump this whenever the emitted datapack content changes for reasons the pack-directory hash cannot see.
|
private static final String HASH_SALT = "iris-forced-datapack-v3-content-addressed-biomes";
|
||||||
// v2: custom biomes now inherit their vanilla derivative's biome tags, so every already-published pack has
|
|
||||||
// to regenerate once.
|
|
||||||
private static final String HASH_SALT = "iris-forced-datapack-v2";
|
|
||||||
private static final long PACKS_HASH_TTL_NANOS = 2_000_000_000L;
|
private static final long PACKS_HASH_TTL_NANOS = 2_000_000_000L;
|
||||||
private static final Object LOCK = new Object();
|
private static final Object LOCK = new Object();
|
||||||
private static final AtomicBoolean LOADED = new AtomicBoolean(false);
|
private static final AtomicBoolean LOADED = new AtomicBoolean(false);
|
||||||
private static final AtomicBoolean STALE_SERVE_LOGGED = new AtomicBoolean(false);
|
|
||||||
private static volatile PublishedState published;
|
private static volatile PublishedState published;
|
||||||
private static volatile HashMemo packsHashMemo;
|
private static volatile HashMemo packsHashMemo;
|
||||||
|
|
||||||
@@ -119,11 +132,9 @@ public final class ModdedForcedDatapack {
|
|||||||
reason = "no published pack";
|
reason = "no published pack";
|
||||||
} else if (!hash.isEmpty() && !state.packsHash().equals(hash)) {
|
} else if (!hash.isEmpty() && !state.packsHash().equals(hash)) {
|
||||||
reason = "stale cache (hash changed)";
|
reason = "stale cache (hash changed)";
|
||||||
|
} else if (hash.isEmpty()) {
|
||||||
|
reason = "current registry inputs could not be verified";
|
||||||
} else {
|
} else {
|
||||||
if (hash.isEmpty() && STALE_SERVE_LOGGED.compareAndSet(false, true)) {
|
|
||||||
ModdedIrisLog.warn("Iris cannot hash the installed packs; serving the last generated forced datapack from {} unverified",
|
|
||||||
state.directory());
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return requireReadablePack(state.directory());
|
return requireReadablePack(state.directory());
|
||||||
} catch (RuntimeException unreadable) {
|
} catch (RuntimeException unreadable) {
|
||||||
@@ -163,17 +174,7 @@ public final class ModdedForcedDatapack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Pack buildPack() {
|
private static Pack buildPack() {
|
||||||
try {
|
return requireReadablePack(regenerate());
|
||||||
return requireReadablePack(regenerate());
|
|
||||||
} catch (RuntimeException | Error generationFailure) {
|
|
||||||
Path lastKnownGood = packDirectory();
|
|
||||||
if (Files.isRegularFile(lastKnownGood.resolve("pack.mcmeta"))) {
|
|
||||||
ModdedIrisLog.error("Iris kept the last known-good generated datapack after regeneration failed",
|
|
||||||
generationFailure);
|
|
||||||
return requireReadablePack(lastKnownGood);
|
|
||||||
}
|
|
||||||
throw generationFailure;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Pack requireReadablePack(Path directory) {
|
private static Pack requireReadablePack(Path directory) {
|
||||||
@@ -263,7 +264,6 @@ public final class ModdedForcedDatapack {
|
|||||||
// Publish the hash this run was built from as the memo too: a memo captured before staging would
|
// Publish the hash this run was built from as the memo too: a memo captured before staging would
|
||||||
// otherwise mismatch what was just published and send the next serve straight back into buildPack.
|
// otherwise mismatch what was just published and send the next serve straight back into buildPack.
|
||||||
packsHashMemo = new HashMemo(packsHash, System.nanoTime());
|
packsHashMemo = new HashMemo(packsHash, System.nanoTime());
|
||||||
STALE_SERVE_LOGGED.set(false);
|
|
||||||
return packDirectory();
|
return packDirectory();
|
||||||
} catch (IOException | RuntimeException | Error failure) {
|
} catch (IOException | RuntimeException | Error failure) {
|
||||||
try {
|
try {
|
||||||
@@ -335,10 +335,6 @@ public final class ModdedForcedDatapack {
|
|||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Content hash over the installed packs: relative path, size and mtime of every regular file, plus the
|
|
||||||
* pack format and loader the generated datapack is shaped for.
|
|
||||||
*/
|
|
||||||
private static String packsHash() throws IOException {
|
private static String packsHash() throws IOException {
|
||||||
MessageDigest digest;
|
MessageDigest digest;
|
||||||
try {
|
try {
|
||||||
@@ -348,39 +344,13 @@ public final class ModdedForcedDatapack {
|
|||||||
}
|
}
|
||||||
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
|
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
|
||||||
+ '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
|
+ '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
|
||||||
Path root = packsRoot();
|
String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
||||||
if (Files.isDirectory(root)) {
|
compilationPackRoots(),
|
||||||
List<String> entries = new ArrayList<>();
|
List.of(),
|
||||||
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
|
DataVersion.getLatest().get(),
|
||||||
@Override
|
false
|
||||||
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) {
|
);
|
||||||
return !directory.equals(root)
|
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
|
||||||
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())
|
|
||||||
? FileVisitResult.SKIP_SUBTREE
|
|
||||||
: FileVisitResult.CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
|
|
||||||
if (attributes.isRegularFile()) {
|
|
||||||
entries.add(root.relativize(file).toString().replace('\\', '/')
|
|
||||||
+ '|' + attributes.size() + '|' + attributes.lastModifiedTime().toMillis());
|
|
||||||
}
|
|
||||||
return FileVisitResult.CONTINUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileVisitResult visitFileFailed(Path file, IOException failure) {
|
|
||||||
entries.add(root.relativize(file).toString().replace('\\', '/') + "|unreadable");
|
|
||||||
return FileVisitResult.CONTINUE;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
entries.sort(Comparator.naturalOrder());
|
|
||||||
for (String entry : entries) {
|
|
||||||
digest.update(entry.getBytes(StandardCharsets.UTF_8));
|
|
||||||
digest.update((byte) '\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return HexFormat.of().formatHex(digest.digest());
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,10 +360,11 @@ public final class ModdedForcedDatapack {
|
|||||||
|
|
||||||
int packCount = 0;
|
int packCount = 0;
|
||||||
KList<String> presetIds = new KList<>();
|
KList<String> presetIds = new KList<>();
|
||||||
File root = packsRoot().toFile();
|
Path worldRoot = worldRootOrNull();
|
||||||
List<File> packs = PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(root);
|
Set<PackSelection> legacySelections = unadoptedSelections(worldRoot);
|
||||||
for (File pack : packs) {
|
for (File pack : compilationPackRoots(worldRoot)) {
|
||||||
if (stagePack(pack, fixer, stagingDirectory, seenBiomes, presetIds)) {
|
PackStageContext context = packStageContext(pack.toPath(), legacySelections);
|
||||||
|
if (stagePack(context, fixer, stagingDirectory, seenBiomes, presetIds)) {
|
||||||
packCount++;
|
packCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,19 +385,28 @@ public final class ModdedForcedDatapack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean stagePack(File sourcePack, IDataFixer fixer, Path stagingDirectory,
|
private static boolean stagePack(PackStageContext context, IDataFixer fixer, Path stagingDirectory,
|
||||||
Map<String, KSet<String>> seenBiomes,
|
Map<String, KSet<String>> seenBiomes,
|
||||||
KList<String> presetIds) throws IOException {
|
KList<String> presetIds) throws IOException {
|
||||||
|
File sourcePack = context.packRoot().toFile();
|
||||||
PackValidationResult validation;
|
PackValidationResult validation;
|
||||||
try {
|
try {
|
||||||
validation = PackValidator.validateForDatapackBootstrap(sourcePack);
|
validation = PackValidator.validateForDatapackBootstrap(sourcePack);
|
||||||
} catch (Throwable validationFailure) {
|
} catch (Throwable validationFailure) {
|
||||||
|
if (context.required()) {
|
||||||
|
throw requiredPackFailure(context, "validation", validationFailure);
|
||||||
|
}
|
||||||
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because validation failed",
|
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because validation failed",
|
||||||
sourcePack.getName(), validationFailure);
|
sourcePack.getName(), validationFailure);
|
||||||
rethrowIfUnrecoverable(validationFailure);
|
rethrowIfUnrecoverable(validationFailure);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!validation.isLoadable()) {
|
if (!validation.isLoadable()) {
|
||||||
|
if (context.required()) {
|
||||||
|
throw new IOException("Retained Iris generation pack at " + context.packRoot()
|
||||||
|
+ " has " + validation.getBlockingErrors().size()
|
||||||
|
+ " blocking validation error(s): " + validation.getBlockingErrors().getFirst());
|
||||||
|
}
|
||||||
ModdedIrisLog.error("Iris excluded pack '{}' from Create World: {} blocking validation error(s); first error: {}",
|
ModdedIrisLog.error("Iris excluded pack '{}' from Create World: {} blocking validation error(s); first error: {}",
|
||||||
sourcePack.getName(), validation.getBlockingErrors().size(),
|
sourcePack.getName(), validation.getBlockingErrors().size(),
|
||||||
validation.getBlockingErrors().getFirst());
|
validation.getBlockingErrors().getFirst());
|
||||||
@@ -441,8 +421,11 @@ public final class ModdedForcedDatapack {
|
|||||||
packFolders.add(packStagingDirectory.toFile());
|
packFolders.add(packStagingDirectory.toFile());
|
||||||
boolean installed;
|
boolean installed;
|
||||||
try {
|
try {
|
||||||
installed = installPack(sourcePack, fixer, packFolders, packBiomes, packPresetIds);
|
installed = installPack(context, fixer, packFolders, packBiomes, packPresetIds);
|
||||||
} catch (Throwable installationFailure) {
|
} catch (Throwable installationFailure) {
|
||||||
|
if (context.required()) {
|
||||||
|
throw requiredPackFailure(context, "datapack serialization", installationFailure);
|
||||||
|
}
|
||||||
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because datapack serialization failed",
|
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because datapack serialization failed",
|
||||||
sourcePack.getName(), installationFailure);
|
sourcePack.getName(), installationFailure);
|
||||||
rethrowIfUnrecoverable(installationFailure);
|
rethrowIfUnrecoverable(installationFailure);
|
||||||
@@ -481,7 +464,7 @@ public final class ModdedForcedDatapack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void mergeDirectory(Path sourceDirectory, Path destinationDirectory) throws IOException {
|
static void mergeDirectory(Path sourceDirectory, Path destinationDirectory) throws IOException {
|
||||||
List<Path> entries = new ArrayList<>();
|
List<Path> entries = new ArrayList<>();
|
||||||
try (Stream<Path> walk = Files.walk(sourceDirectory)) {
|
try (Stream<Path> walk = Files.walk(sourceDirectory)) {
|
||||||
walk.sorted(Comparator.comparingInt(Path::getNameCount)).forEach(entries::add);
|
walk.sorted(Comparator.comparingInt(Path::getNameCount)).forEach(entries::add);
|
||||||
@@ -490,9 +473,14 @@ public final class ModdedForcedDatapack {
|
|||||||
Path relative = sourceDirectory.relativize(source);
|
Path relative = sourceDirectory.relativize(source);
|
||||||
Path destination = destinationDirectory.resolve(relative);
|
Path destination = destinationDirectory.resolve(relative);
|
||||||
if (Files.isDirectory(source)) {
|
if (Files.isDirectory(source)) {
|
||||||
|
if (Files.exists(destination) && !Files.isDirectory(destination)) {
|
||||||
|
throw new IOException("Conflicting Iris datapack output at " + destination + ".");
|
||||||
|
}
|
||||||
Files.createDirectories(destination);
|
Files.createDirectories(destination);
|
||||||
} else if (!Files.exists(destination)) {
|
} else if (!Files.exists(destination)) {
|
||||||
Files.copy(source, destination);
|
Files.copy(source, destination);
|
||||||
|
} else if (!Files.isRegularFile(destination) || Files.mismatch(source, destination) != -1L) {
|
||||||
|
throw new IOException("Conflicting Iris datapack output at " + destination + ".");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,37 +493,65 @@ public final class ModdedForcedDatapack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders,
|
private static boolean installPack(PackStageContext context, IDataFixer fixer, KList<File> folders,
|
||||||
Map<String, KSet<String>> seenBiomes,
|
Map<String, KSet<String>> seenBiomes,
|
||||||
KList<String> presetIds) throws IOException {
|
KList<String> presetIds) throws IOException {
|
||||||
String packName = packFolder.getName();
|
File packFolder = context.packRoot().toFile();
|
||||||
|
String packName = context.packName();
|
||||||
File dimensionsDirectory = new File(packFolder, "dimensions");
|
File dimensionsDirectory = new File(packFolder, "dimensions");
|
||||||
if (!dimensionsDirectory.isDirectory()) {
|
if (!dimensionsDirectory.isDirectory()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
IrisData data = IrisData.get(packFolder);
|
IrisData data = IrisData.openDatapackCompiler(packFolder);
|
||||||
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
|
try {
|
||||||
if (dimensionKeys == null || dimensionKeys.length == 0) {
|
if (context.retainedContract() != null) {
|
||||||
return false;
|
data.bindGenerationRegistryContract(context.retainedContract());
|
||||||
}
|
|
||||||
List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
|
|
||||||
for (String dimensionKey : sortedDimensionKeys) {
|
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
|
||||||
if (dimension == null) {
|
|
||||||
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
|
|
||||||
+ dimensionKey + "' did not load while building the forced datapack");
|
|
||||||
}
|
}
|
||||||
String biomePathPrefix = ModdedWorldgenIds.biomePathPrefix(packName, dimensionKey);
|
String[] dimensionKeys = data.getDimensionLoader().getPossibleKeys();
|
||||||
dimension.installBiomes(fixer, () -> data, folders, "irisworldgen", biomePathPrefix,
|
if (dimensionKeys == null || dimensionKeys.length == 0) {
|
||||||
biomesForNamespace(seenBiomes, biomePathPrefix));
|
return false;
|
||||||
dimension.installBiomes(fixer, () -> data, folders,
|
}
|
||||||
biomesForNamespace(seenBiomes, dimension.getLoadKey()));
|
int installedDimensions = 0;
|
||||||
writeDimensionType(folders, fixer, dimension, packName, dimensionKey);
|
List<String> sortedDimensionKeys = Stream.of(dimensionKeys).sorted().toList();
|
||||||
String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey);
|
for (String dimensionKey : sortedDimensionKeys) {
|
||||||
writeWorldPreset(folders, packName, dimensionKey, presetRef);
|
if (context.dimensionKey() != null && !context.dimensionKey().equals(dimensionKey)) {
|
||||||
presetIds.add(presetRef);
|
continue;
|
||||||
|
}
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
|
||||||
|
+ dimensionKey + "' did not load while building the forced datapack");
|
||||||
|
}
|
||||||
|
dimension.installBiomes(
|
||||||
|
fixer,
|
||||||
|
() -> data,
|
||||||
|
folders,
|
||||||
|
biomesForNamespace(seenBiomes, "iris"),
|
||||||
|
context
|
||||||
|
);
|
||||||
|
writeDimensionType(
|
||||||
|
folders,
|
||||||
|
fixer,
|
||||||
|
dimension,
|
||||||
|
context.dimensionTypeRef(dimensionKey),
|
||||||
|
context.emitLegacyDimensionTypeAlias(),
|
||||||
|
context
|
||||||
|
);
|
||||||
|
if (context.emitPresets()) {
|
||||||
|
String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey);
|
||||||
|
writeWorldPreset(folders, packName, dimensionKey, presetRef);
|
||||||
|
presetIds.add(presetRef);
|
||||||
|
}
|
||||||
|
installedDimensions++;
|
||||||
|
}
|
||||||
|
if (context.required() && installedDimensions == 0) {
|
||||||
|
throw new IOException("Retained Iris generation pack at " + context.packRoot()
|
||||||
|
+ " does not contain dimension '" + context.dimensionKey() + "'.");
|
||||||
|
}
|
||||||
|
return installedDimensions > 0;
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
|
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
|
||||||
@@ -625,15 +641,41 @@ public final class ModdedForcedDatapack {
|
|||||||
|
|
||||||
static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension,
|
static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension,
|
||||||
String pack, String packDimensionKey) throws IOException {
|
String pack, String packDimensionKey) throws IOException {
|
||||||
|
writeDimensionType(
|
||||||
|
folders,
|
||||||
|
fixer,
|
||||||
|
dimension,
|
||||||
|
ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey),
|
||||||
|
true,
|
||||||
|
IrisCustomBiomeAliasResolver.none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeDimensionType(
|
||||||
|
KList<File> folders,
|
||||||
|
IDataFixer fixer,
|
||||||
|
IrisDimension dimension,
|
||||||
|
String typeRef,
|
||||||
|
boolean emitLegacyAlias,
|
||||||
|
IrisCustomBiomeAliasResolver sourceResolver
|
||||||
|
) throws IOException {
|
||||||
IrisDimensionType type = dimension.getDimensionType();
|
IrisDimensionType type = dimension.getDimensionType();
|
||||||
String json = type.toJson(fixer);
|
String json = Objects.requireNonNull(sourceResolver, "sourceResolver").generatedSource(
|
||||||
String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey);
|
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
|
||||||
|
typeRef,
|
||||||
|
type.toJson(fixer),
|
||||||
|
fixer
|
||||||
|
);
|
||||||
String typePath = typeRef.substring(typeRef.indexOf(':') + 1);
|
String typePath = typeRef.substring(typeRef.indexOf(':') + 1);
|
||||||
|
String typeNamespace = typeRef.substring(0, typeRef.indexOf(':'));
|
||||||
for (File datapackRoot : folders) {
|
for (File datapackRoot : folders) {
|
||||||
Path output = datapackRoot.toPath().resolve("data").resolve("irisworldgen")
|
Path output = datapackRoot.toPath().resolve("data").resolve(typeNamespace)
|
||||||
.resolve("dimension_type").resolve(typePath + ".json");
|
.resolve("dimension_type").resolve(typePath + ".json");
|
||||||
Files.createDirectories(output.getParent());
|
Files.createDirectories(output.getParent());
|
||||||
Files.writeString(output, json, StandardCharsets.UTF_8);
|
Files.writeString(output, json, StandardCharsets.UTF_8);
|
||||||
|
if (!emitLegacyAlias) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Load-bearing, not dead: worlds created before the scoped pack path reference
|
// Load-bearing, not dead: worlds created before the scoped pack path reference
|
||||||
// irisworldgen:<dimensionTypeKey> in their level.dat, and ModdedWorldEngines accepts that legacy
|
// irisworldgen:<dimensionTypeKey> in their level.dat, and ModdedWorldEngines accepts that legacy
|
||||||
// key when validating the runtime dimension contract. Removing this emission unloads those worlds.
|
// key when validating the runtime dimension contract. Removing this emission unloads those worlds.
|
||||||
@@ -725,10 +767,239 @@ public final class ModdedForcedDatapack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<File> compilationPackRoots() throws IOException {
|
||||||
|
return compilationPackRoots(worldRootOrNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<File> compilationPackRoots(Path worldRoot) throws IOException {
|
||||||
|
Path dataDirectory = packsRoot().getParent();
|
||||||
|
Path historyRoot = worldRoot == null
|
||||||
|
? dataDirectory.resolve(".iris-world-unavailable")
|
||||||
|
: worldRoot;
|
||||||
|
return IrisDatapackCompiler.collectPackRoots(dataDirectory, historyRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path worldRootOrNull() throws IOException {
|
||||||
|
MinecraftServer server = ModdedEngineBootstrap.currentServer();
|
||||||
|
if (server != null) {
|
||||||
|
return server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
if (ModdedEngineBootstrap.loader().clientEnvironment()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return MainWorldService.configuredWorldRootIfPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<PackSelection> unadoptedSelections(Path worldRoot) throws IOException {
|
||||||
|
if (worldRoot == null) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
Set<PackSelection> selections = new LinkedHashSet<>();
|
||||||
|
for (ModdedDimensionRegistryStore.PersistentDimension dimension
|
||||||
|
: ModdedDimensionRegistryStore.loadWorldRoot(worldRoot)) {
|
||||||
|
Identifier identifier = Identifier.tryParse(dimension.id());
|
||||||
|
if (identifier == null) {
|
||||||
|
throw new IOException("Invalid persistent Iris dimension ID '" + dimension.id() + "'.");
|
||||||
|
}
|
||||||
|
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, identifier);
|
||||||
|
Path dimensionRoot = DimensionType.getStorageFolder(key, worldRoot).toAbsolutePath().normalize();
|
||||||
|
Path generationRoot = GenerationHistoryPaths.forDimension(dimensionRoot).generationRoot();
|
||||||
|
if (!Files.exists(generationRoot)) {
|
||||||
|
selections.add(new PackSelection(dimension.pack(), dimension.dimension()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Set.copyOf(selections);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PackStageContext packStageContext(
|
||||||
|
Path packRoot,
|
||||||
|
Set<PackSelection> unadoptedSelections
|
||||||
|
) throws IOException {
|
||||||
|
Path normalizedRoot = packRoot.toAbsolutePath().normalize();
|
||||||
|
RetainedPack retained = retainedPack(normalizedRoot);
|
||||||
|
if (retained != null) {
|
||||||
|
ModdedWorldgenIds.ScopedDimension identity = ModdedWorldgenIds
|
||||||
|
.scopedDimensionType(retained.epoch().dimensionContract().dimensionTypeKey())
|
||||||
|
.orElseThrow(() -> new IOException("Retained Iris dimension type key is not scoped: "
|
||||||
|
+ retained.epoch().dimensionContract().dimensionTypeKey()));
|
||||||
|
if (!identity.dimension().equals(retained.epoch().dimensionContract().dimensionKey())) {
|
||||||
|
throw new IOException("Retained Iris dimension type identity does not match its epoch contract.");
|
||||||
|
}
|
||||||
|
return PackStageContext.retained(normalizedRoot, identity.pack(), retained.epoch());
|
||||||
|
}
|
||||||
|
|
||||||
|
String packName = normalizedRoot.getFileName().toString();
|
||||||
|
Set<String> legacyDimensions = new LinkedHashSet<>();
|
||||||
|
for (PackSelection selection : unadoptedSelections) {
|
||||||
|
if (selection.pack().equals(packName)) {
|
||||||
|
legacyDimensions.add(selection.dimension());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PackStageContext.installed(normalizedRoot, packName, legacyDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RetainedPack retainedPack(Path packRoot) throws IOException {
|
||||||
|
Path epochRoot = packRoot.getParent();
|
||||||
|
Path epochsRoot = epochRoot == null ? null : epochRoot.getParent();
|
||||||
|
Path generationRoot = epochsRoot == null ? null : epochsRoot.getParent();
|
||||||
|
Path irisRoot = generationRoot == null ? null : generationRoot.getParent();
|
||||||
|
Path dimensionRoot = irisRoot == null ? null : irisRoot.getParent();
|
||||||
|
if (epochRoot == null
|
||||||
|
|| epochsRoot == null
|
||||||
|
|| generationRoot == null
|
||||||
|
|| irisRoot == null
|
||||||
|
|| dimensionRoot == null
|
||||||
|
|| !"pack".equals(packRoot.getFileName().toString())
|
||||||
|
|| !GenerationHistoryPaths.EPOCHS_DIRECTORY_NAME.equals(epochsRoot.getFileName().toString())
|
||||||
|
|| !GenerationHistoryPaths.GENERATION_DIRECTORY_NAME.equals(generationRoot.getFileName().toString())
|
||||||
|
|| !GenerationHistoryPaths.IRIS_DIRECTORY_NAME.equals(irisRoot.getFileName().toString())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GenerationHistory history = GenerationHistory.open(dimensionRoot);
|
||||||
|
String epochId = epochRoot.getFileName().toString();
|
||||||
|
GenerationEpoch epoch = history.manifest().epoch(epochId).orElseThrow(
|
||||||
|
() -> new IOException("Retained Iris pack is not referenced by generation history: " + packRoot)
|
||||||
|
);
|
||||||
|
if (!history.paths().packRoot(epochId).toAbsolutePath().normalize().equals(packRoot)) {
|
||||||
|
throw new IOException("Retained Iris pack path does not match generation history: " + packRoot);
|
||||||
|
}
|
||||||
|
return new RetainedPack(history, epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IOException requiredPackFailure(
|
||||||
|
PackStageContext context,
|
||||||
|
String operation,
|
||||||
|
Throwable failure
|
||||||
|
) {
|
||||||
|
if (failure instanceof IOException ioFailure) {
|
||||||
|
return ioFailure;
|
||||||
|
}
|
||||||
|
return new IOException("Retained Iris generation pack at " + context.packRoot()
|
||||||
|
+ " failed " + operation + ".", failure);
|
||||||
|
}
|
||||||
|
|
||||||
private static Path packsRoot() {
|
private static Path packsRoot() {
|
||||||
return IrisPlatforms.get().packsFolderNoCreate().toPath();
|
return IrisPlatforms.get().packsFolderNoCreate().toPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record PackSelection(String pack, String dimension) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RetainedPack(GenerationHistory history, GenerationEpoch epoch) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PackStageContext(
|
||||||
|
Path packRoot,
|
||||||
|
String packName,
|
||||||
|
String dimensionKey,
|
||||||
|
String retainedDimensionTypeRef,
|
||||||
|
boolean emitPresets,
|
||||||
|
boolean emitLegacyDimensionTypeAlias,
|
||||||
|
boolean required,
|
||||||
|
Set<String> legacyDimensions,
|
||||||
|
GenerationRegistryContract retainedContract
|
||||||
|
) implements IrisCustomBiomeAliasResolver {
|
||||||
|
private static PackStageContext installed(
|
||||||
|
Path packRoot,
|
||||||
|
String packName,
|
||||||
|
Set<String> legacyDimensions
|
||||||
|
) {
|
||||||
|
return new PackStageContext(
|
||||||
|
packRoot,
|
||||||
|
packName,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Set.copyOf(legacyDimensions),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PackStageContext retained(
|
||||||
|
Path packRoot,
|
||||||
|
String packName,
|
||||||
|
GenerationEpoch epoch
|
||||||
|
) {
|
||||||
|
return new PackStageContext(
|
||||||
|
packRoot,
|
||||||
|
packName,
|
||||||
|
epoch.dimensionContract().dimensionKey(),
|
||||||
|
epoch.dimensionContract().dimensionTypeKey(),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
Set.of(),
|
||||||
|
epoch.registryContract()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String dimensionTypeRef(String installedDimensionKey) {
|
||||||
|
return retainedDimensionTypeRef == null
|
||||||
|
? ModdedWorldgenIds.dimensionTypeRef(packName, installedDimensionKey)
|
||||||
|
: retainedDimensionTypeRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String physicalResourceKey(
|
||||||
|
IrisDimension dimension,
|
||||||
|
IrisBiome biome,
|
||||||
|
IrisBiomeCustom customBiome,
|
||||||
|
String currentPhysicalResourceKey
|
||||||
|
) throws IOException {
|
||||||
|
if (retainedContract == null) {
|
||||||
|
return currentPhysicalResourceKey;
|
||||||
|
}
|
||||||
|
return dimension.getLoader().customBiomeResourceKey(dimension, customBiome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> aliases(
|
||||||
|
IrisDimension dimension,
|
||||||
|
IrisBiome biome,
|
||||||
|
IrisBiomeCustom customBiome,
|
||||||
|
String physicalResourceKey
|
||||||
|
) {
|
||||||
|
List<String> candidates = ModdedWorldgenIds.legacyBiomeRefs(
|
||||||
|
packName,
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
customBiome.getId()
|
||||||
|
);
|
||||||
|
if (legacyDimensions.contains(dimension.getLoadKey())) {
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
if (retainedContract == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> aliases = new ArrayList<>(candidates.size());
|
||||||
|
for (String candidate : candidates) {
|
||||||
|
PhysicalResourceKey key = new PhysicalResourceKey(
|
||||||
|
GenerationRegistryContractFactory.BIOME_REGISTRY,
|
||||||
|
candidate
|
||||||
|
);
|
||||||
|
if (retainedContract.definitions().containsKey(key)) {
|
||||||
|
aliases.add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generatedSource(
|
||||||
|
String registryKey,
|
||||||
|
String resourceKey,
|
||||||
|
String currentSource,
|
||||||
|
IDataFixer fixer
|
||||||
|
) throws IOException {
|
||||||
|
if (retainedContract == null) {
|
||||||
|
return currentSource;
|
||||||
|
}
|
||||||
|
PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey);
|
||||||
|
return GenerationRegistryContractFactory.requireGeneratedSource(retainedContract, key, fixer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record PublishedState(Path directory, String packsHash) {
|
private record PublishedState(Path directory, String packsHash) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.IrisSettings;
|
||||||
|
import art.arcane.iris.core.pack.BrokenPackException;
|
||||||
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
|
import art.arcane.iris.core.pack.PackValidationResult;
|
||||||
|
import art.arcane.iris.core.pack.PackValidator;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationPackFingerprint;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
final class ModdedGenerationHistoryStorage {
|
||||||
|
private ModdedGenerationHistoryStorage() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static ActivePack createOrStage(
|
||||||
|
MinecraftServer server,
|
||||||
|
ResourceKey<Level> levelKey,
|
||||||
|
String selectedPack,
|
||||||
|
String selectedDimensionKey,
|
||||||
|
long worldSeed
|
||||||
|
) {
|
||||||
|
Path dimensionRoot = dimensionRoot(server, levelKey);
|
||||||
|
try {
|
||||||
|
Path source = selectedPackSource(selectedPack, selectedDimensionKey);
|
||||||
|
Candidate candidate = captureCandidate(source, selectedDimensionKey);
|
||||||
|
GenerationHistory history = createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
source,
|
||||||
|
worldSeed,
|
||||||
|
candidate,
|
||||||
|
ModdedGenerationHistoryStorage::requireRegistryDefinitions,
|
||||||
|
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
||||||
|
);
|
||||||
|
return resolveActive(history);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw historyFailure(levelKey, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ActivePack restoreOrAdopt(
|
||||||
|
MinecraftServer server,
|
||||||
|
ResourceKey<Level> levelKey,
|
||||||
|
String selectedPack,
|
||||||
|
String selectedDimensionKey,
|
||||||
|
long worldSeed
|
||||||
|
) {
|
||||||
|
Path dimensionRoot = dimensionRoot(server, levelKey);
|
||||||
|
try {
|
||||||
|
GenerationHistory history = restoreOrAdopt(
|
||||||
|
dimensionRoot,
|
||||||
|
worldSeed,
|
||||||
|
() -> {
|
||||||
|
Path source = selectedPackSource(selectedPack, selectedDimensionKey);
|
||||||
|
return new AdoptionSource(source, captureCandidate(
|
||||||
|
source,
|
||||||
|
selectedDimensionKey,
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
|
||||||
|
));
|
||||||
|
},
|
||||||
|
ModdedGenerationHistoryStorage::requireRegistryDefinitions
|
||||||
|
);
|
||||||
|
return resolveActive(history);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw historyFailure(levelKey, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ActivePack openOrAdopt(
|
||||||
|
ServerLevel level,
|
||||||
|
String selectedPack,
|
||||||
|
String selectedDimensionKey,
|
||||||
|
long worldSeed
|
||||||
|
) {
|
||||||
|
return restoreOrAdopt(
|
||||||
|
level.getServer(),
|
||||||
|
level.dimension(),
|
||||||
|
selectedPack,
|
||||||
|
selectedDimensionKey,
|
||||||
|
worldSeed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GenerationHistory createOrStage(
|
||||||
|
Path dimensionRoot,
|
||||||
|
Path source,
|
||||||
|
long worldSeed,
|
||||||
|
Candidate candidate,
|
||||||
|
RegistryContractValidator validator,
|
||||||
|
int transitionWidthBlocks
|
||||||
|
) throws IOException {
|
||||||
|
Path requiredRoot = normalize(dimensionRoot);
|
||||||
|
Path requiredSource = normalize(source);
|
||||||
|
Candidate requiredCandidate = Objects.requireNonNull(candidate, "candidate");
|
||||||
|
RegistryContractValidator requiredValidator = Objects.requireNonNull(validator, "validator");
|
||||||
|
Optional<GenerationHistory> existing = GenerationHistory.openIfPresent(requiredRoot, worldSeed);
|
||||||
|
if (existing.isEmpty()) {
|
||||||
|
captureAndRequireRegistryDefinitions(
|
||||||
|
List.of(requiredCandidate.registryContract()),
|
||||||
|
requiredValidator
|
||||||
|
);
|
||||||
|
Files.createDirectories(requiredRoot);
|
||||||
|
return GenerationHistory.create(
|
||||||
|
requiredRoot,
|
||||||
|
requiredSource,
|
||||||
|
requiredCandidate.packFingerprint(),
|
||||||
|
worldSeed,
|
||||||
|
requiredCandidate.dimensionContract(),
|
||||||
|
requiredCandidate.registryContract()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerationHistory history = existing.get();
|
||||||
|
captureAndRequireRegistryDefinitions(registryContracts(history), requiredValidator);
|
||||||
|
history.stageUpdate(
|
||||||
|
requiredSource,
|
||||||
|
requiredCandidate.packFingerprint(),
|
||||||
|
requiredCandidate.dimensionContract(),
|
||||||
|
requiredCandidate.registryContract(),
|
||||||
|
transitionWidthBlocks
|
||||||
|
);
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GenerationHistory restoreOrAdopt(
|
||||||
|
Path dimensionRoot,
|
||||||
|
long worldSeed,
|
||||||
|
AdoptionSourceSupplier sourceSupplier,
|
||||||
|
RegistryContractValidator validator
|
||||||
|
) throws IOException {
|
||||||
|
Path requiredRoot = normalize(dimensionRoot);
|
||||||
|
RegistryContractValidator requiredValidator = Objects.requireNonNull(validator, "validator");
|
||||||
|
Optional<GenerationHistory> existing = GenerationHistory.openIfPresent(requiredRoot, worldSeed);
|
||||||
|
if (existing.isPresent()) {
|
||||||
|
requireRegistryDefinitions(existing.get(), requiredValidator);
|
||||||
|
return existing.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
AdoptionSource adoption = Objects.requireNonNull(sourceSupplier, "sourceSupplier").get();
|
||||||
|
Candidate candidate = Objects.requireNonNull(adoption.candidate(), "candidate");
|
||||||
|
captureAndRequireRegistryDefinitions(List.of(candidate.registryContract()), requiredValidator);
|
||||||
|
Files.createDirectories(requiredRoot);
|
||||||
|
return GenerationHistory.create(
|
||||||
|
requiredRoot,
|
||||||
|
normalize(adoption.packSource()),
|
||||||
|
candidate.packFingerprint(),
|
||||||
|
worldSeed,
|
||||||
|
candidate.dimensionContract(),
|
||||||
|
candidate.registryContract()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ActivePack resolveActive(GenerationHistory history) throws IOException {
|
||||||
|
GenerationEpoch epoch = history.activeEpoch();
|
||||||
|
Path packRoot = normalize(history.activePackRoot());
|
||||||
|
validatePack(packRoot, epoch.packFingerprint());
|
||||||
|
validateDimension(packRoot, epoch.dimensionContract());
|
||||||
|
return new ActivePack(
|
||||||
|
packRoot,
|
||||||
|
epoch.dimensionContract().dimensionKey(),
|
||||||
|
epoch.dimensionContract(),
|
||||||
|
history
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validatePack(Path packRoot, String fingerprint) {
|
||||||
|
PackValidationResult validation = PackValidator.validate(packRoot.toFile());
|
||||||
|
PackValidationRegistry.publish(packRoot, validation, fingerprint);
|
||||||
|
if (!validation.isLoadable()) {
|
||||||
|
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateDimension(
|
||||||
|
Path packRoot,
|
||||||
|
GenerationEpoch.DimensionContract recordedContract
|
||||||
|
) throws IOException {
|
||||||
|
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
|
||||||
|
try {
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(recordedContract.dimensionKey(), false);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IOException("Immutable generation pack does not contain dimension '"
|
||||||
|
+ recordedContract.dimensionKey() + "'.");
|
||||||
|
}
|
||||||
|
GenerationEpoch.DimensionContract actualContract = GenerationEpochContractFactory.create(
|
||||||
|
dimension,
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
recordedContract.dimensionTypeKey()
|
||||||
|
);
|
||||||
|
if (!recordedContract.equals(actualContract)) {
|
||||||
|
throw new IOException("Immutable generation pack no longer matches its recorded dimension contract.");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Candidate captureCandidate(Path source, String dimensionKey) throws IOException {
|
||||||
|
return captureCandidate(
|
||||||
|
source,
|
||||||
|
dimensionKey,
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.CONTENT_ADDRESSED_ONLY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Candidate captureCandidate(
|
||||||
|
Path source,
|
||||||
|
String dimensionKey,
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeAliasPolicy aliasPolicy
|
||||||
|
) throws IOException {
|
||||||
|
String fingerprint = GenerationPackFingerprint.compute(
|
||||||
|
source,
|
||||||
|
GenerationPackFingerprint.CURRENT_VERSION
|
||||||
|
);
|
||||||
|
IrisData data = IrisData.openDatapackCompiler(source.toFile());
|
||||||
|
try {
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IOException("Selected Iris pack does not contain dimension '" + dimensionKey + "'.");
|
||||||
|
}
|
||||||
|
PlatformGenerationRegistry generationRegistry = IrisPlatforms.get()
|
||||||
|
.registries()
|
||||||
|
.generationRegistry();
|
||||||
|
String dimensionTypeKey = generationRegistry.dimensionTypeResourceKey(
|
||||||
|
source.getFileName().toString(),
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
dimension.getDimensionTypeKey()
|
||||||
|
);
|
||||||
|
GenerationEpoch.DimensionContract dimensionContract = GenerationEpochContractFactory.create(
|
||||||
|
dimension,
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
dimensionTypeKey
|
||||||
|
);
|
||||||
|
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
|
||||||
|
data,
|
||||||
|
dimension,
|
||||||
|
fingerprint,
|
||||||
|
aliasPolicy
|
||||||
|
);
|
||||||
|
return new Candidate(fingerprint, dimensionContract, registryContract);
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path selectedPackSource(String pack, String dimensionKey) {
|
||||||
|
ModdedStartup.requirePackForWorldCreation(pack);
|
||||||
|
return normalize(ModdedWorldEngines.resolvePack(pack, dimensionKey).toPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationRegistryContract requireRegistryDefinitions(
|
||||||
|
Collection<GenerationRegistryContract> contracts
|
||||||
|
)
|
||||||
|
throws IOException {
|
||||||
|
return GenerationRegistryContractFactory.captureRequiredDefinitions(contracts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireRegistryDefinitions(
|
||||||
|
GenerationHistory history,
|
||||||
|
RegistryContractValidator validator
|
||||||
|
) throws IOException {
|
||||||
|
captureAndRequireRegistryDefinitions(registryContracts(history), validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void captureAndRequireRegistryDefinitions(
|
||||||
|
Collection<GenerationRegistryContract> contracts,
|
||||||
|
RegistryContractValidator validator
|
||||||
|
) throws IOException {
|
||||||
|
GenerationRegistryContract available = validator.capture(contracts);
|
||||||
|
for (GenerationRegistryContract contract : contracts) {
|
||||||
|
contract.requireDefinitionsAvailableIn(available);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<GenerationRegistryContract> registryContracts(GenerationHistory history) {
|
||||||
|
Collection<GenerationEpoch> epochs = history.manifest().epochs();
|
||||||
|
List<GenerationRegistryContract> contracts = new ArrayList<>(epochs.size());
|
||||||
|
for (GenerationEpoch epoch : epochs) {
|
||||||
|
contracts.add(epoch.registryContract());
|
||||||
|
}
|
||||||
|
return contracts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path dimensionRoot(MinecraftServer server, ResourceKey<Level> levelKey) {
|
||||||
|
return normalize(ModdedDimensionStorage.storageFolder(server, levelKey).toPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path normalize(Path path) {
|
||||||
|
return Objects.requireNonNull(path, "path").toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IllegalStateException historyFailure(ResourceKey<Level> levelKey, IOException failure) {
|
||||||
|
return new IllegalStateException("Iris generation history is unusable for '"
|
||||||
|
+ levelKey.identifier() + "'.", failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
record ActivePack(
|
||||||
|
Path packRoot,
|
||||||
|
String dimensionKey,
|
||||||
|
GenerationEpoch.DimensionContract dimensionContract,
|
||||||
|
GenerationHistory history
|
||||||
|
) {
|
||||||
|
ActivePack {
|
||||||
|
packRoot = normalize(packRoot);
|
||||||
|
dimensionKey = Objects.requireNonNull(dimensionKey, "dimensionKey");
|
||||||
|
dimensionContract = Objects.requireNonNull(dimensionContract, "dimensionContract");
|
||||||
|
history = Objects.requireNonNull(history, "history");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Candidate(
|
||||||
|
String packFingerprint,
|
||||||
|
GenerationEpoch.DimensionContract dimensionContract,
|
||||||
|
GenerationRegistryContract registryContract
|
||||||
|
) {
|
||||||
|
Candidate {
|
||||||
|
packFingerprint = Objects.requireNonNull(packFingerprint, "packFingerprint");
|
||||||
|
dimensionContract = Objects.requireNonNull(dimensionContract, "dimensionContract");
|
||||||
|
registryContract = Objects.requireNonNull(registryContract, "registryContract");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record AdoptionSource(Path packSource, Candidate candidate) {
|
||||||
|
AdoptionSource {
|
||||||
|
packSource = normalize(packSource);
|
||||||
|
candidate = Objects.requireNonNull(candidate, "candidate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface AdoptionSourceSupplier {
|
||||||
|
AdoptionSource get() throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface RegistryContractValidator {
|
||||||
|
GenerationRegistryContract capture(Collection<GenerationRegistryContract> contracts) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
enum ModdedGenerationMode {
|
||||||
|
PERSISTENT_CREATE(true),
|
||||||
|
PERSISTENT_RESTORE(true),
|
||||||
|
TRANSIENT_STUDIO(false);
|
||||||
|
|
||||||
|
private final boolean historyRequired;
|
||||||
|
|
||||||
|
ModdedGenerationMode(boolean historyRequired) {
|
||||||
|
this.historyRequired = historyRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean historyRequired() {
|
||||||
|
return historyRequired;
|
||||||
|
}
|
||||||
|
}
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
import net.minecraft.core.Registry;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.RegistryOps;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
final class ModdedGenerationRegistry implements PlatformGenerationRegistry {
|
||||||
|
private static final String BIOME_REGISTRY = "minecraft:worldgen/biome";
|
||||||
|
private static final String STRUCTURE_REGISTRY = "minecraft:worldgen/structure";
|
||||||
|
private static final String DIMENSION_TYPE_REGISTRY = "minecraft:dimension_type";
|
||||||
|
private static final String BLOCK_REGISTRY = "minecraft:block";
|
||||||
|
private static final String ENTITY_TYPE_REGISTRY = "minecraft:entity_type";
|
||||||
|
|
||||||
|
private final Supplier<MinecraftServer> server;
|
||||||
|
private final String runtimeIdentity;
|
||||||
|
private final String rendererIdentity;
|
||||||
|
|
||||||
|
ModdedGenerationRegistry(
|
||||||
|
Supplier<MinecraftServer> server,
|
||||||
|
String runtimeIdentity,
|
||||||
|
String rendererIdentity
|
||||||
|
) {
|
||||||
|
this.server = Objects.requireNonNull(server, "server");
|
||||||
|
this.runtimeIdentity = requireText(runtimeIdentity, "runtimeIdentity");
|
||||||
|
this.rendererIdentity = requireText(rendererIdentity, "rendererIdentity");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String runtimeIdentity() {
|
||||||
|
return runtimeIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generatedDefinitionRendererIdentity() {
|
||||||
|
return rendererIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String customBiomeResourceKey(String identitySha256) {
|
||||||
|
return PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(identitySha256);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> legacyCustomBiomeResourceKeys(
|
||||||
|
String packName,
|
||||||
|
String dimensionKey,
|
||||||
|
String customBiomeId
|
||||||
|
) {
|
||||||
|
return ModdedWorldgenIds.legacyBiomeRefs(
|
||||||
|
requireText(packName, "packName"),
|
||||||
|
requireText(dimensionKey, "dimensionKey"),
|
||||||
|
requireText(customBiomeId, "customBiomeId")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
|
||||||
|
return ModdedWorldgenIds.dimensionTypeRef(
|
||||||
|
requireText(packName, "packName"),
|
||||||
|
requireText(dimensionKey, "dimensionKey")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Definition definition(String registryKey, String resourceKey) {
|
||||||
|
MinecraftServer instance = Objects.requireNonNull(server.get(), "Minecraft server");
|
||||||
|
RegistryAccess access = instance.registryAccess();
|
||||||
|
return switch (registryKey) {
|
||||||
|
case BIOME_REGISTRY -> presence(access, Registries.BIOME, registryKey, resourceKey);
|
||||||
|
case STRUCTURE_REGISTRY -> presence(access, Registries.STRUCTURE, registryKey, resourceKey);
|
||||||
|
case DIMENSION_TYPE_REGISTRY -> presence(
|
||||||
|
access,
|
||||||
|
Registries.DIMENSION_TYPE,
|
||||||
|
registryKey,
|
||||||
|
resourceKey
|
||||||
|
);
|
||||||
|
case BLOCK_REGISTRY -> presence(access, Registries.BLOCK, registryKey, resourceKey);
|
||||||
|
case ENTITY_TYPE_REGISTRY -> presence(access, Registries.ENTITY_TYPE, registryKey, resourceKey);
|
||||||
|
default -> throw new IllegalArgumentException("Unsupported generation registry: " + registryKey);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Definition canonicalDefinition(String registryKey, String resourceKey, String sourceJson) {
|
||||||
|
MinecraftServer instance = Objects.requireNonNull(server.get(), "Minecraft server");
|
||||||
|
RegistryAccess access = instance.registryAccess();
|
||||||
|
return switch (registryKey) {
|
||||||
|
case BIOME_REGISTRY -> canonicalize(access, Biome.DIRECT_CODEC, sourceJson);
|
||||||
|
case DIMENSION_TYPE_REGISTRY -> canonicalize(access, DimensionType.DIRECT_CODEC, sourceJson);
|
||||||
|
default -> throw new IllegalArgumentException(
|
||||||
|
"Registry does not accept generated JSON definitions: " + registryKey
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Definition generatedDefinition(String registryKey, String resourceKey) {
|
||||||
|
MinecraftServer instance = Objects.requireNonNull(server.get(), "Minecraft server");
|
||||||
|
RegistryAccess access = instance.registryAccess();
|
||||||
|
return switch (registryKey) {
|
||||||
|
case BIOME_REGISTRY -> exactRegistered(
|
||||||
|
access,
|
||||||
|
Registries.BIOME,
|
||||||
|
Biome.DIRECT_CODEC,
|
||||||
|
resourceKey
|
||||||
|
);
|
||||||
|
case DIMENSION_TYPE_REGISTRY -> exactRegistered(
|
||||||
|
access,
|
||||||
|
Registries.DIMENSION_TYPE,
|
||||||
|
DimensionType.DIRECT_CODEC,
|
||||||
|
resourceKey
|
||||||
|
);
|
||||||
|
default -> throw new IllegalArgumentException(
|
||||||
|
"Registry does not contain Iris-generated definitions: " + registryKey
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Definition presence(
|
||||||
|
RegistryAccess access,
|
||||||
|
ResourceKey<? extends Registry<T>> registryKey,
|
||||||
|
String registryResourceKey,
|
||||||
|
String resourceKey
|
||||||
|
) {
|
||||||
|
Registry<T> registry = access.lookupOrThrow(registryKey);
|
||||||
|
Identifier identifier = requireIdentifier(resourceKey);
|
||||||
|
T value = registry.getValue(identifier);
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalStateException("Missing registry definition " + registryKey.identifier()
|
||||||
|
+ " / " + resourceKey + ".");
|
||||||
|
}
|
||||||
|
return Definition.resourceIdentity(registryResourceKey, resourceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Definition canonicalize(RegistryAccess access, Codec<T> codec, String sourceJson) {
|
||||||
|
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||||
|
JsonElement source = JsonParser.parseString(Objects.requireNonNull(sourceJson, "sourceJson"));
|
||||||
|
T decoded = codec.parse(ops, source).getOrThrow(IllegalArgumentException::new);
|
||||||
|
JsonElement encoded = codec.encodeStart(ops, decoded).getOrThrow(IllegalStateException::new);
|
||||||
|
return Definition.exactJson(encoded.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Definition exactRegistered(
|
||||||
|
RegistryAccess access,
|
||||||
|
ResourceKey<? extends Registry<T>> registryKey,
|
||||||
|
Codec<T> codec,
|
||||||
|
String resourceKey
|
||||||
|
) {
|
||||||
|
Registry<T> registry = access.lookupOrThrow(registryKey);
|
||||||
|
Identifier identifier = requireIdentifier(resourceKey);
|
||||||
|
T value = registry.getValue(identifier);
|
||||||
|
if (value == null) {
|
||||||
|
throw new IllegalStateException("Missing generated registry definition "
|
||||||
|
+ registryKey.identifier() + " / " + resourceKey + ".");
|
||||||
|
}
|
||||||
|
RegistryOps<JsonElement> ops = RegistryOps.create(JsonOps.INSTANCE, access);
|
||||||
|
JsonElement encoded = codec.encodeStart(ops, value).getOrThrow(IllegalStateException::new);
|
||||||
|
return Definition.exactJson(encoded.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Identifier requireIdentifier(String resourceKey) {
|
||||||
|
Identifier identifier = Identifier.tryParse(resourceKey);
|
||||||
|
if (identifier == null) {
|
||||||
|
throw new IllegalArgumentException("Invalid registry resource key: " + resourceKey);
|
||||||
|
}
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String label) {
|
||||||
|
String required = Objects.requireNonNull(value, label).trim();
|
||||||
|
if (required.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(label + " must not be blank.");
|
||||||
|
}
|
||||||
|
return required;
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-26
@@ -82,13 +82,11 @@ import java.util.function.Predicate;
|
|||||||
*/
|
*/
|
||||||
final class ModdedImportedFeatureStage {
|
final class ModdedImportedFeatureStage {
|
||||||
private static final String CYCLE_MARKER = "Feature order cycle found";
|
private static final String CYCLE_MARKER = "Feature order cycle found";
|
||||||
private static final long NO_GENERATION = Long.MIN_VALUE;
|
|
||||||
|
|
||||||
private final IrisModdedBiomeSource biomeSource;
|
private final IrisModdedBiomeSource biomeSource;
|
||||||
private final ReentrantLock buildLock = new ReentrantLock();
|
private final ReentrantLock buildLock = new ReentrantLock();
|
||||||
|
private final ConcurrentHashMap<Integer, FeatureTable> featureTables = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<Integer, Long> inertGenerations = new ConcurrentHashMap<>();
|
||||||
private volatile IrisModdedChunkGenerator generator;
|
private volatile IrisModdedChunkGenerator generator;
|
||||||
private volatile FeatureTable featureTable;
|
|
||||||
private volatile long inertGeneration = NO_GENERATION;
|
|
||||||
|
|
||||||
ModdedImportedFeatureStage(IrisModdedBiomeSource biomeSource) {
|
ModdedImportedFeatureStage(IrisModdedBiomeSource biomeSource) {
|
||||||
this.biomeSource = biomeSource;
|
this.biomeSource = biomeSource;
|
||||||
@@ -103,12 +101,12 @@ final class ModdedImportedFeatureStage {
|
|||||||
* pack can never serve another.
|
* pack can never serve another.
|
||||||
*/
|
*/
|
||||||
void invalidate() {
|
void invalidate() {
|
||||||
featureTable = null;
|
featureTables.clear();
|
||||||
inertGeneration = NO_GENERATION;
|
inertGenerations.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean active() {
|
boolean active() {
|
||||||
return featureTable != null;
|
return !featureTables.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,7 +118,8 @@ final class ModdedImportedFeatureStage {
|
|||||||
* <p>With {@code importedFeatures} disabled there is no table and this is vanilla's default getter.
|
* <p>With {@code importedFeatures} disabled there is no table and this is vanilla's default getter.
|
||||||
*/
|
*/
|
||||||
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
||||||
FeatureTable table = featureTable;
|
Engine engine = generator == null ? null : generator.engineOrNull();
|
||||||
|
FeatureTable table = engine == null ? null : featureTables.get(engine.getCacheID());
|
||||||
if (table == null) {
|
if (table == null) {
|
||||||
return biome.value().getGenerationSettings();
|
return biome.value().getGenerationSettings();
|
||||||
}
|
}
|
||||||
@@ -151,8 +150,9 @@ final class ModdedImportedFeatureStage {
|
|||||||
if (engine == null || engine.isClosed() || engine.isClosing()) {
|
if (engine == null || engine.isClosed() || engine.isClosing()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
int runtimeIdentity = engine.getCacheID();
|
||||||
long generation = biomeSource.packGeneration();
|
long generation = biomeSource.packGeneration();
|
||||||
if (settled(generation)) {
|
if (settled(runtimeIdentity, generation)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (waitForBuild) {
|
if (waitForBuild) {
|
||||||
@@ -161,35 +161,35 @@ final class ModdedImportedFeatureStage {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (settled(generation)) {
|
if (settled(runtimeIdentity, generation)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
build(engine, generation);
|
build(engine, runtimeIdentity, generation);
|
||||||
} finally {
|
} finally {
|
||||||
buildLock.unlock();
|
buildLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean settled(long generation) {
|
private boolean settled(int runtimeIdentity, long generation) {
|
||||||
FeatureTable current = featureTable;
|
FeatureTable current = featureTables.get(runtimeIdentity);
|
||||||
if (current != null && current.generation() == generation) {
|
if (current != null && current.generation() == generation) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return inertGeneration == generation;
|
return Long.valueOf(generation).equals(inertGenerations.get(runtimeIdentity));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void build(Engine engine, long generation) {
|
private void build(Engine engine, int runtimeIdentity, long generation) {
|
||||||
IrisImportedFeatureControl control;
|
IrisImportedFeatureControl control;
|
||||||
try {
|
try {
|
||||||
control = NativeFeatureGenerationPolicy.control(engine);
|
control = NativeFeatureGenerationPolicy.control(engine);
|
||||||
} catch (RuntimeException error) {
|
} catch (RuntimeException error) {
|
||||||
ModdedIrisLog.error("Iris could not read importedFeatures for this dimension; features off: {}",
|
ModdedIrisLog.error("Iris could not read importedFeatures for this dimension; features off: {}",
|
||||||
error.toString());
|
error.toString());
|
||||||
markInert(generation);
|
markInert(runtimeIdentity, generation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!control.shouldGenerateFeatures()) {
|
if (!control.shouldGenerateFeatures()) {
|
||||||
markInert(generation);
|
markInert(runtimeIdentity, generation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
FeatureTable built;
|
FeatureTable built;
|
||||||
@@ -199,15 +199,15 @@ final class ModdedImportedFeatureStage {
|
|||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
ModdedIrisLog.error("Iris importedFeatures is off for {}: feature table construction failed: {}",
|
ModdedIrisLog.error("Iris importedFeatures is off for {}: feature table construction failed: {}",
|
||||||
dimensionKey(engine), error.toString());
|
dimensionKey(engine), error.toString());
|
||||||
markInert(generation);
|
markInert(runtimeIdentity, generation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (built == null) {
|
if (built == null) {
|
||||||
markInert(generation);
|
markInert(runtimeIdentity, generation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
featureTable = built;
|
featureTables.put(runtimeIdentity, built);
|
||||||
inertGeneration = NO_GENERATION;
|
inertGenerations.remove(runtimeIdentity);
|
||||||
// Arm the worldcheck log watch here, before any chunk decorates: arming from the first pass instead
|
// Arm the worldcheck log watch here, before any chunk decorates: arming from the first pass instead
|
||||||
// missed every far-chunk write the first chunk made. No-op unless -Diris.worldcheck is set.
|
// missed every far-chunk write the first chunk made. No-op unless -Diris.worldcheck is set.
|
||||||
WorldCheckFeaturePlacement.arm();
|
WorldCheckFeaturePlacement.arm();
|
||||||
@@ -216,9 +216,9 @@ final class ModdedImportedFeatureStage {
|
|||||||
built.derivatives().size());
|
built.derivatives().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markInert(long generation) {
|
private void markInert(int runtimeIdentity, long generation) {
|
||||||
featureTable = null;
|
featureTables.remove(runtimeIdentity);
|
||||||
inertGeneration = generation;
|
inertGenerations.put(runtimeIdentity, generation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
|
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
|
||||||
@@ -312,7 +312,8 @@ final class ModdedImportedFeatureStage {
|
|||||||
* control is disabled or the table degraded.
|
* control is disabled or the table degraded.
|
||||||
*/
|
*/
|
||||||
void run(WorldGenLevel level, ChunkAccess chunk, Engine engine) {
|
void run(WorldGenLevel level, ChunkAccess chunk, Engine engine) {
|
||||||
FeatureTable table = featureTable;
|
int runtimeIdentity = engine.getCacheID();
|
||||||
|
FeatureTable table = featureTables.get(runtimeIdentity);
|
||||||
if (table == null) {
|
if (table == null) {
|
||||||
WorldCheckFeaturePlacement.recordFeaturesOff();
|
WorldCheckFeaturePlacement.recordFeaturesOff();
|
||||||
return;
|
return;
|
||||||
@@ -320,7 +321,8 @@ final class ModdedImportedFeatureStage {
|
|||||||
if (table.generation() != biomeSource.packGeneration()) {
|
if (table.generation() != biomeSource.packGeneration()) {
|
||||||
// A repoint landed between the prepare above and this chunk. Refuse stale content outright; the
|
// A repoint landed between the prepare above and this chunk. Refuse stale content outright; the
|
||||||
// next chunk's prepare rebuilds against the new pack.
|
// next chunk's prepare rebuilds against the new pack.
|
||||||
invalidate();
|
featureTables.remove(runtimeIdentity, table);
|
||||||
|
inertGenerations.remove(runtimeIdentity);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
IrisModdedChunkGenerator owner = generator;
|
IrisModdedChunkGenerator owner = generator;
|
||||||
@@ -423,6 +425,11 @@ final class ModdedImportedFeatureStage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void evictRuntime(int runtimeIdentity) {
|
||||||
|
featureTables.remove(runtimeIdentity);
|
||||||
|
inertGenerations.remove(runtimeIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
||||||
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
||||||
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
||||||
|
|||||||
+27
-5
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.framework.Engine;
|
|||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||||
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
@@ -233,17 +234,30 @@ final class ModdedNativeStructureStage {
|
|||||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (configuredStarts.containsKey(structure)) {
|
|
||||||
recordWorldCheckStructureShift(
|
|
||||||
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Identifier id = registry.getKey(structure);
|
Identifier id = registry.getKey(structure);
|
||||||
String structureId = id == null ? null : id.toString();
|
String structureId = id == null ? null : id.toString();
|
||||||
if (structureId == null) {
|
if (structureId == null) {
|
||||||
throw NativeStructureGenerationException.failure(
|
throw NativeStructureGenerationException.failure(
|
||||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||||
}
|
}
|
||||||
|
BoundingBox footprint = start.getBoundingBox();
|
||||||
|
if (!current.getComplex().allowsNewGenerationFootprint(
|
||||||
|
footprint.minX(),
|
||||||
|
footprint.minZ(),
|
||||||
|
footprint.maxX(),
|
||||||
|
footprint.maxZ())) {
|
||||||
|
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||||
|
if (configuredStarts.containsKey(structure)) {
|
||||||
|
NativeStructureOwnershipStore.discard(
|
||||||
|
current, structureId, chunkPos.x(), chunkPos.z());
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (configuredStarts.containsKey(structure)) {
|
||||||
|
recordWorldCheckStructureShift(
|
||||||
|
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
||||||
IrisNativeStructureDecision decision;
|
IrisNativeStructureDecision decision;
|
||||||
try {
|
try {
|
||||||
@@ -326,6 +340,14 @@ final class ModdedNativeStructureStage {
|
|||||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||||
for (StructureStart start : starts) {
|
for (StructureStart start : starts) {
|
||||||
|
BoundingBox footprint = start.getBoundingBox();
|
||||||
|
if (!current.getComplex().allowsNewGenerationFootprint(
|
||||||
|
footprint.minX(),
|
||||||
|
footprint.minZ(),
|
||||||
|
footprint.maxX(),
|
||||||
|
footprint.maxZ())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
NativeStructureOwnershipRecord ownership =
|
NativeStructureOwnershipRecord ownership =
|
||||||
NativeStructureOwnershipRecovery.resolve(
|
NativeStructureOwnershipRecovery.resolve(
|
||||||
current, world.getLevel(), structureId, structure, start);
|
current, world.getLevel(), structureId, structure, start);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.BuildConstants;
|
||||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||||
import art.arcane.iris.modded.api.ModdedDataType;
|
import art.arcane.iris.modded.api.ModdedDataType;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
@@ -25,6 +26,7 @@ import art.arcane.iris.spi.PlatformBiome;
|
|||||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
import art.arcane.iris.spi.PlatformEntityType;
|
import art.arcane.iris.spi.PlatformEntityType;
|
||||||
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
import art.arcane.iris.spi.PlatformItem;
|
import art.arcane.iris.spi.PlatformItem;
|
||||||
import art.arcane.iris.spi.PlatformRegistries;
|
import art.arcane.iris.spi.PlatformRegistries;
|
||||||
import art.arcane.volmlib.util.data.UnresolvedKeyLog;
|
import art.arcane.volmlib.util.data.UnresolvedKeyLog;
|
||||||
@@ -59,6 +61,19 @@ public final class ModdedRegistries implements PlatformRegistries {
|
|||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlatformGenerationRegistry generationRegistry() {
|
||||||
|
MinecraftServer instance = server.get();
|
||||||
|
if (instance == null) {
|
||||||
|
throw new IllegalStateException("Minecraft server is not ready for generation registry capture.");
|
||||||
|
}
|
||||||
|
return new ModdedGenerationRegistry(
|
||||||
|
server,
|
||||||
|
"modded-generation-registry-v1",
|
||||||
|
"modded-generated-registry-json-v1|minecraft:" + BuildConstants.MINECRAFT_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PlatformBlockState block(String key) {
|
public PlatformBlockState block(String key) {
|
||||||
return ModdedBlockResolution.get(key);
|
return ModdedBlockResolution.get(key);
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@ public final class ModdedRuntimeRegistry {
|
|||||||
if (!seen.add(biomeId)) {
|
if (!seen.add(biomeId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String biomeRef = ModdedWorldgenIds.biomeRef(pack, dimension.getLoadKey(), biomeId);
|
String biomeRef = data.customBiomeResourceKey(dimension, customBiome);
|
||||||
ResourceKey<Biome> biomeKey = ResourceKey.create(Registries.BIOME, Identifier.parse(biomeRef));
|
ResourceKey<Biome> biomeKey = ResourceKey.create(Registries.BIOME, Identifier.parse(biomeRef));
|
||||||
if (registry.get(biomeKey).isEmpty()) {
|
if (registry.get(biomeKey).isEmpty()) {
|
||||||
missing.add(biomeRef);
|
missing.add(biomeRef);
|
||||||
|
|||||||
+47
-31
@@ -36,27 +36,31 @@ import net.minecraft.world.level.biome.Biome;
|
|||||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
final class ModdedSpawnTableMerger {
|
final class ModdedSpawnTableMerger {
|
||||||
private final IrisModdedChunkGenerator generator;
|
private final IrisModdedChunkGenerator generator;
|
||||||
private volatile SpawnTableState state = new SpawnTableState();
|
private final ConcurrentHashMap<SpawnBiomeKey, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final Set<Integer> initializedRuntimeIdentities = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
|
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
|
||||||
this.generator = generator;
|
this.generator = generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||||
SpawnTableState active = state;
|
Engine current = generator.engineOrNull();
|
||||||
if (active.initialized) {
|
if (current == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
synchronized (active) {
|
int runtimeIdentity = current.getCacheID();
|
||||||
if (active != state || active.initialized) {
|
if (initializedRuntimeIdentities.contains(runtimeIdentity)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Engine current = generator.engineOrNull();
|
synchronized (generator) {
|
||||||
if (current == null) {
|
if (initializedRuntimeIdentities.contains(runtimeIdentity)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +72,7 @@ final class ModdedSpawnTableMerger {
|
|||||||
hostDimension,
|
hostDimension,
|
||||||
current.getData(),
|
current.getData(),
|
||||||
hostDimension.getReachableBiomes(current),
|
hostDimension.getReachableBiomes(current),
|
||||||
active
|
runtimeIdentity
|
||||||
);
|
);
|
||||||
DimensionStackContext stackContext = current.getDimensionStackContext();
|
DimensionStackContext stackContext = current.getDimensionStackContext();
|
||||||
if (stackContext != null) {
|
if (stackContext != null) {
|
||||||
@@ -82,11 +86,11 @@ final class ModdedSpawnTableMerger {
|
|||||||
dimension,
|
dimension,
|
||||||
terrainContext.getData(),
|
terrainContext.getData(),
|
||||||
dimension.getReachableBiomes(terrainContext),
|
dimension.getReachableBiomes(terrainContext),
|
||||||
active
|
runtimeIdentity
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
active.initialized = true;
|
initializedRuntimeIdentities.add(runtimeIdentity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +100,7 @@ final class ModdedSpawnTableMerger {
|
|||||||
IrisDimension dimension,
|
IrisDimension dimension,
|
||||||
IrisData data,
|
IrisData data,
|
||||||
Iterable<IrisBiome> biomes,
|
Iterable<IrisBiome> biomes,
|
||||||
SpawnTableState state
|
int runtimeIdentity
|
||||||
) {
|
) {
|
||||||
for (IrisBiome irisBiome : biomes) {
|
for (IrisBiome irisBiome : biomes) {
|
||||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||||
@@ -110,31 +114,43 @@ final class ModdedSpawnTableMerger {
|
|||||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||||
Holder<Biome> customHolder = resolveBiomeHolder(
|
Holder<Biome> customHolder = resolveBiomeHolder(
|
||||||
registry,
|
registry,
|
||||||
ModdedWorldgenIds.biomeRef(
|
data.customBiomeResourceKey(dimension, customBiome)
|
||||||
data.getDataFolder().getName(),
|
|
||||||
dimension.getLoadKey(),
|
|
||||||
customBiome.getId()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
if (customHolder == null) {
|
if (customHolder == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
state.vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
vanillaSpawnBiomes.putIfAbsent(
|
||||||
|
new SpawnBiomeKey(runtimeIdentity, customHolder.value()),
|
||||||
|
vanillaHolder
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Holder<Biome> vanillaSpawnBiome(Biome biome) {
|
Holder<Biome> vanillaSpawnBiome(Biome biome) {
|
||||||
return state.vanillaSpawnBiomes.get(biome);
|
Engine current = generator.engineOrNull();
|
||||||
|
return current == null
|
||||||
|
? null
|
||||||
|
: vanillaSpawnBiomes.get(new SpawnBiomeKey(current.getCacheID(), biome));
|
||||||
}
|
}
|
||||||
|
|
||||||
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
|
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
|
||||||
Biome biome, MobCategory category,
|
Engine engine,
|
||||||
|
Biome biome,
|
||||||
|
MobCategory category,
|
||||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||||
SpawnTableKey key = new SpawnTableKey(biome, category);
|
SpawnTableKey key = new SpawnTableKey(engine.getCacheID(), biome, category);
|
||||||
return state.mergedSpawnTables.computeIfAbsent(
|
return mergedSpawnTables.computeIfAbsent(
|
||||||
key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
key,
|
||||||
|
ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void evictRuntime(int runtimeIdentity) {
|
||||||
|
vanillaSpawnBiomes.keySet().removeIf(key -> key.runtimeIdentity() == runtimeIdentity);
|
||||||
|
mergedSpawnTables.keySet().removeIf(key -> key.runtimeIdentity() == runtimeIdentity);
|
||||||
|
initializedRuntimeIdentities.remove(runtimeIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||||
@@ -150,16 +166,16 @@ final class ModdedSpawnTableMerger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetVanillaSpawnBiomes() {
|
void resetVanillaSpawnBiomes() {
|
||||||
state = new SpawnTableState();
|
synchronized (generator) {
|
||||||
|
vanillaSpawnBiomes.clear();
|
||||||
|
mergedSpawnTables.clear();
|
||||||
|
initializedRuntimeIdentities.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
private record SpawnBiomeKey(int runtimeIdentity, Biome biome) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class SpawnTableState {
|
private record SpawnTableKey(int runtimeIdentity, Biome biome, MobCategory category) {
|
||||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
|
||||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables =
|
|
||||||
new ConcurrentHashMap<>();
|
|
||||||
private volatile boolean initialized;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,13 @@ public final class ModdedStartup {
|
|||||||
index++;
|
index++;
|
||||||
long dimensionStartedAt = System.currentTimeMillis();
|
long dimensionStartedAt = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
ModdedDimensionManager.create(server, dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
|
ModdedDimensionManager.restorePersistent(
|
||||||
|
server,
|
||||||
|
dimension.id(),
|
||||||
|
dimension.pack(),
|
||||||
|
dimension.dimension(),
|
||||||
|
dimension.seed()
|
||||||
|
);
|
||||||
injected++;
|
injected++;
|
||||||
ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms",
|
ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms",
|
||||||
index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(),
|
index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(),
|
||||||
|
|||||||
+249
-49
@@ -18,21 +18,30 @@
|
|||||||
|
|
||||||
package art.arcane.iris.modded;
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.engine.IrisEngine;
|
import art.arcane.iris.engine.IrisEngine;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
|
import art.arcane.iris.engine.history.GenerationActivation;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
||||||
|
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
import art.arcane.iris.modded.command.ModdedGuiHost;
|
import art.arcane.iris.modded.command.ModdedGuiHost;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.world.level.dimension.DimensionType;
|
import net.minecraft.world.level.dimension.DimensionType;
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -45,13 +54,25 @@ public final class ModdedWorldEngines {
|
|||||||
private ModdedWorldEngines() {
|
private ModdedWorldEngines() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Engine get(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
public static Engine get(
|
||||||
|
ServerLevel level,
|
||||||
|
String pack,
|
||||||
|
String dimensionKey,
|
||||||
|
long seedOverride,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
Engine existing = ENGINES.get(level);
|
Engine existing = ENGINES.get(level);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
return ENGINES.computeIfAbsent(level,
|
return ENGINES.computeIfAbsent(level,
|
||||||
(ServerLevel l) -> createAndApplyWorldBoundary(l, pack, dimensionKey, seedOverride));
|
(ServerLevel l) -> createAndApplyWorldBoundary(
|
||||||
|
l,
|
||||||
|
pack,
|
||||||
|
dimensionKey,
|
||||||
|
seedOverride,
|
||||||
|
generationMode
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Collection<Engine> activeEngines() {
|
public static Collection<Engine> activeEngines() {
|
||||||
@@ -81,8 +102,14 @@ public final class ModdedWorldEngines {
|
|||||||
ModdedIrisLog.info("Iris engine evicted for {}", level.dimension().identifier());
|
ModdedIrisLog.info("Iris engine evicted for {}", level.dimension().identifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
static Engine prepareReplacement(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
static Engine prepareReplacement(
|
||||||
return create(level, pack, dimensionKey, seedOverride);
|
ServerLevel level,
|
||||||
|
String pack,
|
||||||
|
String dimensionKey,
|
||||||
|
long seedOverride,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
|
return create(level, pack, dimensionKey, seedOverride, generationMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void installReplacement(ServerLevel level, Engine replacement) {
|
static void installReplacement(ServerLevel level, Engine replacement) {
|
||||||
@@ -103,9 +130,14 @@ public final class ModdedWorldEngines {
|
|||||||
ModdedGuiHost.unbind(engine);
|
ModdedGuiHost.unbind(engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Engine createAndApplyWorldBoundary(ServerLevel level, String pack,
|
private static Engine createAndApplyWorldBoundary(
|
||||||
String dimensionKey, long seedOverride) {
|
ServerLevel level,
|
||||||
Engine engine = create(level, pack, dimensionKey, seedOverride);
|
String pack,
|
||||||
|
String dimensionKey,
|
||||||
|
long seedOverride,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
|
Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode);
|
||||||
try {
|
try {
|
||||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||||
return engine;
|
return engine;
|
||||||
@@ -126,22 +158,178 @@ public final class ModdedWorldEngines {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
private static Engine create(
|
||||||
|
ServerLevel level,
|
||||||
|
String pack,
|
||||||
|
String dimensionKey,
|
||||||
|
long seedOverride,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
ModdedEngineBootstrap.bind();
|
ModdedEngineBootstrap.bind();
|
||||||
File packDir = resolvePack(pack, dimensionKey);
|
|
||||||
PackValidationRegistry.requireLoadable(pack);
|
|
||||||
IrisData data = IrisData.openRuntime(packDir);
|
|
||||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
|
||||||
if (dimension == null) {
|
|
||||||
ModdedIrisLog.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
|
|
||||||
pack, packDir.getAbsolutePath(), dimensionKey, dimensionKey);
|
|
||||||
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + packDir.getAbsolutePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
|
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
|
||||||
validateDimensionContract(pack, dimensionKey, dimension, level);
|
ModdedGenerationMode requiredMode = Objects.requireNonNull(generationMode, "generationMode");
|
||||||
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
|
if (requiredMode == ModdedGenerationMode.TRANSIENT_STUDIO) {
|
||||||
IrisWorld world = IrisWorld.builder()
|
return createTransientStudioEngine(level, pack, dimensionKey, seed);
|
||||||
|
}
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack active = ModdedGenerationHistoryStorage.openOrAdopt(
|
||||||
|
level,
|
||||||
|
pack,
|
||||||
|
dimensionKey,
|
||||||
|
seed
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
return createHistoryEngine(level, seed, active);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris generation history could not initialize runtime routing for '"
|
||||||
|
+ level.dimension().identifier() + "'.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine createHistoryEngine(
|
||||||
|
ServerLevel level,
|
||||||
|
long seed,
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack opened
|
||||||
|
) throws IOException {
|
||||||
|
GenerationHistory history = opened.history();
|
||||||
|
long openedActivationId = history.activeActivation().activationId();
|
||||||
|
IrisEngine candidate = buildEngine(level, seed, opened);
|
||||||
|
boolean ready = false;
|
||||||
|
try {
|
||||||
|
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
|
||||||
|
history,
|
||||||
|
IrisBoundarySignatureSampler.INSTANCE,
|
||||||
|
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
||||||
|
);
|
||||||
|
router.preloadActiveRuntimes();
|
||||||
|
if (history.activeActivation().activationId() != openedActivationId) {
|
||||||
|
close(candidate);
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack promoted =
|
||||||
|
ModdedGenerationHistoryStorage.resolveActive(history);
|
||||||
|
candidate = buildEngine(level, seed, promoted);
|
||||||
|
router = GenerationHistoryRuntimeRouter.attach(
|
||||||
|
candidate,
|
||||||
|
history,
|
||||||
|
IrisBoundarySignatureSampler.INSTANCE
|
||||||
|
);
|
||||||
|
router.preloadActiveRuntimes();
|
||||||
|
}
|
||||||
|
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
|
||||||
|
ready = true;
|
||||||
|
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
|
||||||
|
level.dimension().identifier(),
|
||||||
|
candidate.getData().getDataFolder().getAbsolutePath(),
|
||||||
|
candidate.getDimension().getLoadKey(),
|
||||||
|
seed,
|
||||||
|
candidate.getDimension().getMinHeight(),
|
||||||
|
candidate.getDimension().getMaxHeight(),
|
||||||
|
history.activeActivation().activationId());
|
||||||
|
return candidate;
|
||||||
|
} finally {
|
||||||
|
if (!ready) {
|
||||||
|
close(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisEngine buildEngine(
|
||||||
|
ServerLevel level,
|
||||||
|
long seed,
|
||||||
|
ModdedGenerationHistoryStorage.ActivePack active
|
||||||
|
) throws IOException {
|
||||||
|
File packDir = active.packRoot().toFile();
|
||||||
|
PackValidationRegistry.requireLoadable(active.packRoot());
|
||||||
|
IrisData data = IrisData.openRuntime(packDir);
|
||||||
|
boolean runtimeOwnsData = false;
|
||||||
|
try {
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(active.dimensionKey());
|
||||||
|
if (dimension == null) {
|
||||||
|
ModdedIrisLog.error("Iris immutable generation pack at {} does not contain dimension '{}' (expected dimensions/{}.json).",
|
||||||
|
packDir.getAbsolutePath(), active.dimensionKey(), active.dimensionKey());
|
||||||
|
throw new IllegalStateException("Iris dimension '" + active.dimensionKey()
|
||||||
|
+ "' missing from immutable pack " + packDir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
validateDimensionContract(active.dimensionContract(), dimension, level);
|
||||||
|
File worldFolder = DimensionType.getStorageFolder(
|
||||||
|
level.dimension(),
|
||||||
|
level.getServer().getWorldPath(LevelResource.ROOT)
|
||||||
|
).toFile();
|
||||||
|
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
|
||||||
|
GenerationHistory history = active.history();
|
||||||
|
GenerationActivation activation = history.activeActivation();
|
||||||
|
GenerationEpoch epoch = history.activeEpoch();
|
||||||
|
TransitionGenerationPlan transitionPlan = activation.isInitial()
|
||||||
|
? null
|
||||||
|
: history.transitionPlan(activation.activationId());
|
||||||
|
IrisEngine engine = new IrisEngine(
|
||||||
|
new EngineTarget(world, dimension, data),
|
||||||
|
IrisEngine.InitializationMode.RUNTIME,
|
||||||
|
history.paths().activationMantleRoot(activation.activationId()),
|
||||||
|
epoch.kernelVersion(),
|
||||||
|
transitionPlan);
|
||||||
|
runtimeOwnsData = true;
|
||||||
|
return engine;
|
||||||
|
} finally {
|
||||||
|
if (!runtimeOwnsData) {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisEngine createTransientStudioEngine(
|
||||||
|
ServerLevel level,
|
||||||
|
String pack,
|
||||||
|
String dimensionKey,
|
||||||
|
long seed
|
||||||
|
) {
|
||||||
|
File packDir = resolvePack(pack, dimensionKey);
|
||||||
|
PackValidationRegistry.requireLoadable(packDir.toPath());
|
||||||
|
IrisData data = IrisData.openRuntime(packDir);
|
||||||
|
boolean runtimeOwnsData = false;
|
||||||
|
boolean ready = false;
|
||||||
|
IrisEngine engine = null;
|
||||||
|
try {
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IllegalStateException("Transient Iris Studio pack does not contain dimension '"
|
||||||
|
+ dimensionKey + "'.");
|
||||||
|
}
|
||||||
|
File worldFolder = DimensionType.getStorageFolder(
|
||||||
|
level.dimension(),
|
||||||
|
level.getServer().getWorldPath(LevelResource.ROOT)
|
||||||
|
).toFile();
|
||||||
|
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
|
||||||
|
engine = new IrisEngine(
|
||||||
|
new EngineTarget(world, dimension, data),
|
||||||
|
IrisEngine.InitializationMode.STUDIO
|
||||||
|
);
|
||||||
|
runtimeOwnsData = true;
|
||||||
|
requireReady(engine, level, ModdedGenerationMode.TRANSIENT_STUDIO);
|
||||||
|
ready = true;
|
||||||
|
ModdedIrisLog.info("Iris transient Studio engine up for {}: pack={} dim={} seed={} height={}..{}",
|
||||||
|
level.dimension().identifier(),
|
||||||
|
packDir.getAbsolutePath(),
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
seed,
|
||||||
|
dimension.getMinHeight(),
|
||||||
|
dimension.getMaxHeight());
|
||||||
|
return engine;
|
||||||
|
} finally {
|
||||||
|
if (!ready && engine != null) {
|
||||||
|
close(engine);
|
||||||
|
} else if (!runtimeOwnsData) {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisWorld buildWorld(
|
||||||
|
ServerLevel level,
|
||||||
|
long seed,
|
||||||
|
IrisDimension dimension,
|
||||||
|
File worldFolder
|
||||||
|
) {
|
||||||
|
return IrisWorld.builder()
|
||||||
.platformIdentity(level.dimension().identifier().toString())
|
.platformIdentity(level.dimension().identifier().toString())
|
||||||
.name(level.dimension().identifier().toString().replace(':', '_'))
|
.name(level.dimension().identifier().toString().replace(':', '_'))
|
||||||
.seed(seed)
|
.seed(seed)
|
||||||
@@ -150,40 +338,52 @@ public final class ModdedWorldEngines {
|
|||||||
.maxHeight(dimension.getMaxHeight())
|
.maxHeight(dimension.getMaxHeight())
|
||||||
.platformWorld(new ModdedPlatformWorld(level))
|
.platformWorld(new ModdedPlatformWorld(level))
|
||||||
.build();
|
.build();
|
||||||
Engine engine = new IrisEngine(
|
|
||||||
new EngineTarget(world, dimension, data),
|
|
||||||
IrisEngine.InitializationMode.RUNTIME);
|
|
||||||
if (engine.isClosed() || engine.getComplex() == null) {
|
|
||||||
IllegalStateException failure = new IllegalStateException("Iris engine for "
|
|
||||||
+ level.dimension().identifier() + " did not initialize a ready biome complex");
|
|
||||||
try {
|
|
||||||
close(engine);
|
|
||||||
} catch (Throwable cleanupError) {
|
|
||||||
failure.addSuppressed(cleanupError);
|
|
||||||
}
|
|
||||||
throw failure;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
|
|
||||||
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
|
|
||||||
return engine;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateDimensionContract(String pack, String dimensionKey,
|
private static void requireReady(
|
||||||
IrisDimension dimension, ServerLevel level) {
|
IrisEngine engine,
|
||||||
|
ServerLevel level,
|
||||||
|
ModdedGenerationMode generationMode
|
||||||
|
) {
|
||||||
|
if (engine.isClosed() || engine.getComplex() == null) {
|
||||||
|
throw new IllegalStateException("Iris engine for " + level.dimension().identifier()
|
||||||
|
+ " did not initialize a ready runtime");
|
||||||
|
}
|
||||||
|
if (generationMode.historyRequired()
|
||||||
|
&& engine.getGenerationHistoryRuntimeRouter().isEmpty()) {
|
||||||
|
throw new IllegalStateException("Persistent Iris engine for " + level.dimension().identifier()
|
||||||
|
+ " did not initialize a generation-history runtime router");
|
||||||
|
}
|
||||||
|
if (!generationMode.historyRequired()
|
||||||
|
&& (!engine.isStudio() || engine.getGenerationHistoryRuntimeRouter().isPresent())) {
|
||||||
|
throw new IllegalStateException("Transient Iris Studio engine for "
|
||||||
|
+ level.dimension().identifier() + " has an invalid history mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateDimensionContract(
|
||||||
|
GenerationEpoch.DimensionContract recorded,
|
||||||
|
IrisDimension dimension,
|
||||||
|
ServerLevel level
|
||||||
|
) {
|
||||||
|
GenerationEpoch.DimensionContract loaded = GenerationEpochContractFactory.create(
|
||||||
|
dimension,
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
recorded.dimensionTypeKey()
|
||||||
|
);
|
||||||
|
if (!recorded.equals(loaded)) {
|
||||||
|
throw new IllegalStateException("Immutable Iris dimension contract changed for '"
|
||||||
|
+ level.dimension().identifier() + "'.");
|
||||||
|
}
|
||||||
DimensionType actualType = level.dimensionType();
|
DimensionType actualType = level.dimensionType();
|
||||||
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
|
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
|
||||||
.map(key -> key.identifier().toString())
|
.map(key -> key.identifier().toString())
|
||||||
.orElse("<unregistered>");
|
.orElse("<unregistered>");
|
||||||
String legacyTypeKey = "irisworldgen:" + dimension.getDimensionTypeKey();
|
|
||||||
String expectedTypeKey = legacyTypeKey.equals(actualTypeKey)
|
|
||||||
? legacyTypeKey
|
|
||||||
: ModdedWorldgenIds.dimensionTypeRef(pack, dimensionKey);
|
|
||||||
IrisDimensionRuntimeContract expected = new IrisDimensionRuntimeContract(
|
IrisDimensionRuntimeContract expected = new IrisDimensionRuntimeContract(
|
||||||
expectedTypeKey,
|
recorded.dimensionTypeKey(),
|
||||||
dimension.getMinHeight(),
|
recorded.minHeight(),
|
||||||
dimension.getMaxHeight() - dimension.getMinHeight(),
|
recorded.height(),
|
||||||
dimension.getLogicalHeight());
|
recorded.logicalHeight());
|
||||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
|
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
|
||||||
actualTypeKey,
|
actualTypeKey,
|
||||||
actualType.minY(),
|
actualType.minY(),
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package art.arcane.iris.modded;
|
|||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public final class ModdedWorldgenIds {
|
public final class ModdedWorldgenIds {
|
||||||
private static final String NAMESPACE = "irisworldgen";
|
private static final String NAMESPACE = "irisworldgen";
|
||||||
@@ -29,8 +31,33 @@ public final class ModdedWorldgenIds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String biomeRef(Engine engine, String biome) {
|
public static String biomeRef(Engine engine, String biome) {
|
||||||
return biomeRef(engine.getData().getDataFolder().getName(),
|
return engine.getData().customBiomeResourceKey(engine.getDimension(), biome);
|
||||||
engine.getDimension().getLoadKey(), biome);
|
}
|
||||||
|
|
||||||
|
public static List<String> legacyBiomeRefs(String pack, String dimension, String biome) {
|
||||||
|
String scoped = biomeRef(pack, dimension, biome);
|
||||||
|
String unscoped = dimension.toLowerCase(Locale.ROOT) + ":" + biome.toLowerCase(Locale.ROOT);
|
||||||
|
return scoped.equals(unscoped) ? List.of(scoped) : List.of(scoped, unscoped);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<ScopedDimension> scopedDimensionType(String dimensionTypeRef) {
|
||||||
|
String prefix = NAMESPACE + ":";
|
||||||
|
if (dimensionTypeRef == null || !dimensionTypeRef.startsWith(prefix)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String[] parts = dimensionTypeRef.substring(prefix.length()).split("/");
|
||||||
|
if (parts.length != 5
|
||||||
|
|| !"packs".equals(parts[0])
|
||||||
|
|| !"dimensions".equals(parts[2])
|
||||||
|
|| !"dimension_type".equals(parts[4])) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
String pack = decode(parts[1]);
|
||||||
|
String dimension = decode(parts[3]);
|
||||||
|
if (pack == null || dimension == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(new ScopedDimension(pack, dimension));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String displayName(String presetPath) {
|
public static String displayName(String presetPath) {
|
||||||
@@ -100,4 +127,7 @@ public final class ModdedWorldgenIds {
|
|||||||
}
|
}
|
||||||
return Character.toUpperCase(normalized.charAt(0)) + normalized.substring(1);
|
return Character.toUpperCase(normalized.charAt(0)) + normalized.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record ScopedDimension(String pack, String dimension) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+7
-4
@@ -26,6 +26,7 @@ import art.arcane.iris.engine.framework.IrisStructureLocator;
|
|||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.StructureReachability;
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||||
|
import art.arcane.iris.engine.history.GenerationFindCatalog;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||||
@@ -109,7 +110,7 @@ final class ModdedCommandSuggestions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Set<String> reachableBiomeKeys(Engine engine) {
|
static Set<String> reachableBiomeKeys(Engine engine) {
|
||||||
return reachableBiomeKeys(engine.getAllBiomes());
|
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) {
|
static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) {
|
||||||
@@ -123,7 +124,7 @@ final class ModdedCommandSuggestions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static boolean isReachableBiome(Engine engine, String biomeKey) {
|
static boolean isReachableBiome(Engine engine, String biomeKey) {
|
||||||
return isReachableBiome(engine.getAllBiomes(), biomeKey);
|
return GenerationFindCatalog.biome(engine, biomeKey) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isReachableBiome(Iterable<IrisBiome> biomes, String biomeKey) {
|
static boolean isReachableBiome(Iterable<IrisBiome> biomes, String biomeKey) {
|
||||||
@@ -135,7 +136,8 @@ final class ModdedCommandSuggestions {
|
|||||||
try {
|
try {
|
||||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||||
if (engine != null) {
|
if (engine != null) {
|
||||||
return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder);
|
return SharedSuggestionProvider.suggest(GenerationFindCatalog.regions(engine).stream()
|
||||||
|
.map(region -> region.getLoadKey()), builder);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
warnTabFailure("region keys", context.getSource(), e);
|
warnTabFailure("region keys", context.getSource(), e);
|
||||||
@@ -148,7 +150,7 @@ final class ModdedCommandSuggestions {
|
|||||||
try {
|
try {
|
||||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||||
if (engine != null) {
|
if (engine != null) {
|
||||||
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
|
return SharedSuggestionProvider.suggest(GenerationFindCatalog.objectKeys(engine), builder);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
warnTabFailure("object keys", context.getSource(), e);
|
warnTabFailure("object keys", context.getSource(), e);
|
||||||
@@ -191,6 +193,7 @@ final class ModdedCommandSuggestions {
|
|||||||
Collection<String> unregisteredIrisKeys = eligibleUnregisteredEditableKeys(
|
Collection<String> unregisteredIrisKeys = eligibleUnregisteredEditableKeys(
|
||||||
irisKeys, registeredKeys,
|
irisKeys, registeredKeys,
|
||||||
(String candidate) -> IrisStructureLocator.hasNativePlacement(engine, candidate));
|
(String candidate) -> IrisStructureLocator.hasNativePlacement(engine, candidate));
|
||||||
|
nativeKeys.addAll(GenerationFindCatalog.retainedStructureKeys(engine));
|
||||||
return SharedSuggestionProvider.suggest(
|
return SharedSuggestionProvider.suggest(
|
||||||
combineStructureKeys(unregisteredIrisKeys, nativeKeys), builder);
|
combineStructureKeys(unregisteredIrisKeys, nativeKeys), builder);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
|
|||||||
+23
-26
@@ -29,8 +29,9 @@ import art.arcane.iris.engine.framework.Locator;
|
|||||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||||
import art.arcane.iris.engine.framework.StructureReachability;
|
import art.arcane.iris.engine.framework.StructureReachability;
|
||||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||||
|
import art.arcane.iris.engine.history.GenerationSemanticQueries;
|
||||||
|
import art.arcane.iris.engine.history.GenerationFindCatalog;
|
||||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
|
||||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
@@ -86,15 +87,11 @@ final class ModdedLocateCommands {
|
|||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim());
|
IrisBiome biome = GenerationFindCatalog.biome(engine, key);
|
||||||
if (biome == null) {
|
if (biome == null) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!ModdedCommandSuggestions.isReachableBiome(engine, biome.getLoadKey())) {
|
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", biome.getLoadKey())));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
|
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -111,15 +108,11 @@ final class ModdedLocateCommands {
|
|||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
IrisRegion region = engine.getData().getRegionLoader().load(key.trim());
|
IrisRegion region = GenerationFindCatalog.region(engine, key);
|
||||||
if (region == null) {
|
if (region == null) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key)));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key)));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!engine.getDimension().getRegions().contains(region.getLoadKey())) {
|
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey())));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
|
locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -132,7 +125,7 @@ final class ModdedLocateCommands {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
String key = keyRaw.trim();
|
String key = keyRaw.trim();
|
||||||
if (!engine.hasObjectPlacement(key)) {
|
if (!GenerationFindCatalog.hasObjectPlacement(engine, key)) {
|
||||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
|
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -158,10 +151,6 @@ final class ModdedLocateCommands {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
||||||
if (runtime == null) {
|
|
||||||
IrisModdedCommands.fail(source, "Hydrology is not active in this dimension.");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
HydrologyFeatureQuery query;
|
HydrologyFeatureQuery query;
|
||||||
try {
|
try {
|
||||||
query = HydrologyFeatureQuery.parse(type);
|
query = HydrologyFeatureQuery.parse(type);
|
||||||
@@ -171,15 +160,19 @@ final class ModdedLocateCommands {
|
|||||||
}
|
}
|
||||||
int originX = player.blockPosition().getX();
|
int originX = player.blockPosition().getX();
|
||||||
int originZ = player.blockPosition().getZ();
|
int originZ = player.blockPosition().getZ();
|
||||||
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
int requestedDistance = runtime == null
|
||||||
int maximumDistance = runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
? 8192
|
||||||
|
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||||
|
int maximumDistance = runtime == null
|
||||||
|
? requestedDistance
|
||||||
|
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
||||||
MinecraftServer server = source.getServer();
|
MinecraftServer server = source.getServer();
|
||||||
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
||||||
MultiBurst.burst.completeValueAsync(() -> runtime.nearestFeature(
|
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
|
||||||
query.types(), query.profileKey(), originX, originZ, maximumDistance,
|
engine, query, originX, originZ, maximumDistance,
|
||||||
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
|
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
|
||||||
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
|
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
|
||||||
.whenComplete((HydrologyFeatureRef feature, Throwable error) -> server.execute(() -> {
|
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
|
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
|
||||||
IrisModdedCommands.fail(source,
|
IrisModdedCommands.fail(source,
|
||||||
@@ -192,13 +185,12 @@ final class ModdedLocateCommands {
|
|||||||
+ maximumDistance + " blocks.");
|
+ maximumDistance + " blocks.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int worldY = feature.y() + engine.getDimension().getMinHeight() + 2;
|
|
||||||
teleportToStructure(
|
teleportToStructure(
|
||||||
source,
|
source,
|
||||||
level,
|
level,
|
||||||
player,
|
player,
|
||||||
feature.x(),
|
feature.x(),
|
||||||
worldY,
|
feature.y() + 2,
|
||||||
feature.z(),
|
feature.z(),
|
||||||
type + " hydrology feature"
|
type + " hydrology feature"
|
||||||
);
|
);
|
||||||
@@ -225,8 +217,9 @@ final class ModdedLocateCommands {
|
|||||||
}
|
}
|
||||||
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
|
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
|
||||||
if (resolved.isEmpty()) {
|
if (resolved.isEmpty()) {
|
||||||
if (!IrisStructureLocator.hasNativePlacement(engine, key)
|
if ((!IrisStructureLocator.hasNativePlacement(engine, key)
|
||||||
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key)) {
|
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key))
|
||||||
|
|| GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)) {
|
||||||
locateIrisStructure(source, level, engine, player, key);
|
locateIrisStructure(source, level, engine, player, key);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -247,6 +240,10 @@ final class ModdedLocateCommands {
|
|||||||
if (!ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
if (!ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||||
decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement,
|
decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement,
|
||||||
reachable, nativeGenerationEnabled)) {
|
reachable, nativeGenerationEnabled)) {
|
||||||
|
if (GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)) {
|
||||||
|
locateIrisStructure(source, level, engine, player, key);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
IrisModdedCommands.fail(source, registeredStructureUnavailableMessage(
|
IrisModdedCommands.fail(source, registeredStructureUnavailableMessage(
|
||||||
target.key(), target.availability(), decision,
|
target.key(), target.availability(), decision,
|
||||||
nativePlacement, locatableNativePlacement,
|
nativePlacement, locatableNativePlacement,
|
||||||
@@ -312,7 +309,7 @@ final class ModdedLocateCommands {
|
|||||||
Thread thread = new Thread(() -> {
|
Thread thread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
IrisStructureLocator.LocateResult result =
|
IrisStructureLocator.LocateResult result =
|
||||||
IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
|
GenerationSemanticQueries.nearestStructure(engine, key, blockX, blockZ, 1024);
|
||||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key))));
|
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key))));
|
||||||
return;
|
return;
|
||||||
|
|||||||
+1
-1
@@ -491,7 +491,7 @@ public final class ModdedStudioCommands {
|
|||||||
ModdedDimensionManager.Handle handle = null;
|
ModdedDimensionManager.Handle handle = null;
|
||||||
try {
|
try {
|
||||||
replaceExistingStudio(server, owner, dimensionId);
|
replaceExistingStudio(server, owner, dimensionId);
|
||||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
|
handle = ModdedDimensionManager.createTransientStudio(server, dimensionId, pack, pack, seed);
|
||||||
STUDIOS.put(owner, dimensionId);
|
STUDIOS.put(owner, dimensionId);
|
||||||
if (owner.equals(CONSOLE_OWNER)) {
|
if (owner.equals(CONSOLE_OWNER)) {
|
||||||
completeConsoleOpen(source, dimensionId, pack, seed, handle);
|
completeConsoleOpen(source, dimensionId, pack, seed, handle);
|
||||||
|
|||||||
+56
@@ -78,6 +78,7 @@ public final class ModdedWorldCommands {
|
|||||||
|
|
||||||
root.then(enableTree("enable"));
|
root.then(enableTree("enable"));
|
||||||
root.then(enableTree("create"));
|
root.then(enableTree("create"));
|
||||||
|
root.then(updateTree());
|
||||||
root.then(replaceOverworldTree());
|
root.then(replaceOverworldTree());
|
||||||
root.then(mainWorldTree());
|
root.then(mainWorldTree());
|
||||||
|
|
||||||
@@ -110,6 +111,20 @@ public final class ModdedWorldCommands {
|
|||||||
.executes(ModdedCommandTree.localized((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), false))));
|
.executes(ModdedCommandTree.localized((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), false))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LiteralArgumentBuilder<CommandSourceStack> updateTree() {
|
||||||
|
return Commands.literal("update")
|
||||||
|
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
|
||||||
|
.then(Commands.argument("pack", StringArgumentType.string())
|
||||||
|
.suggests(IrisModdedCommands.PACK_NAMES)
|
||||||
|
.executes(ModdedCommandTree.localized(
|
||||||
|
(CommandContext<CommandSourceStack> context) -> update(
|
||||||
|
context.getSource(),
|
||||||
|
dimensionArgument(context),
|
||||||
|
StringArgumentType.getString(context, "pack")
|
||||||
|
)
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
private static LiteralArgumentBuilder<CommandSourceStack> deleteTree(String name) {
|
private static LiteralArgumentBuilder<CommandSourceStack> deleteTree(String name) {
|
||||||
return Commands.literal(name)
|
return Commands.literal(name)
|
||||||
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
|
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
|
||||||
@@ -197,6 +212,47 @@ public final class ModdedWorldCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int update(CommandSourceStack source, String targetDimension, String packRaw) {
|
||||||
|
String dimensionId;
|
||||||
|
try {
|
||||||
|
dimensionId = normalizeDimensionId(targetDimension);
|
||||||
|
} catch (IllegalArgumentException failure) {
|
||||||
|
IrisModdedCommands.fail(source, failure.getMessage());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
String[] packRef = parsePackRef(packRaw);
|
||||||
|
String pack = packRef[0];
|
||||||
|
String packDimension = packRef[1];
|
||||||
|
if (!validPackRef(source, pack, packDimension)
|
||||||
|
|| blockIfPackBroken(source, dimensionId, pack)
|
||||||
|
|| !loadPackDimension(source, pack, packDimension)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ModdedDimensionManager.UpdateResult result = ModdedDimensionManager.stagePersistentUpdate(
|
||||||
|
source.getServer(),
|
||||||
|
dimensionId,
|
||||||
|
pack,
|
||||||
|
packDimension
|
||||||
|
);
|
||||||
|
if (result.restartRequired()) {
|
||||||
|
IrisModdedCommands.ok(source, "Iris world update staged for " + dimensionId
|
||||||
|
+ " from " + pack + ":" + packDimension
|
||||||
|
+ ". Restart the server to activate it; the running world remains on its current pack.");
|
||||||
|
} else {
|
||||||
|
IrisModdedCommands.ok(source, "Iris world " + dimensionId
|
||||||
|
+ " already uses this immutable pack; no restart is required.");
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
ModdedIrisLog.error("Iris world update staging failed for {} (pack={} dim={})",
|
||||||
|
dimensionId, pack, packDimension, failure);
|
||||||
|
IrisModdedCommands.fail(source, "Iris world update failed for " + dimensionId
|
||||||
|
+ ": " + IrisLanguage.errorDetail(failure));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int replaceOverworld(CommandSourceStack source, String packRaw, String seedRaw) {
|
private static int replaceOverworld(CommandSourceStack source, String packRaw, String seedRaw) {
|
||||||
MinecraftServer server = source.getServer();
|
MinecraftServer server = source.getServer();
|
||||||
Long seed = parseSeed(source, seedRaw);
|
Long seed = parseSeed(source, seedRaw);
|
||||||
|
|||||||
+4
-2
@@ -18,6 +18,7 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@@ -267,10 +268,11 @@ public class IrisModdedStructureParityTest {
|
|||||||
.setInferredType(InferredType.SEA);
|
.setInferredType(InferredType.SEA);
|
||||||
|
|
||||||
Set<String> keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
Set<String> keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
||||||
List.of(ocean, custom, shore, unsafeSea), "OverWorld");
|
List.of(ocean, custom, shore, unsafeSea),
|
||||||
|
customBiome -> "iris:biomes/" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||||
|
|
||||||
assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach",
|
assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach",
|
||||||
"minecraft:the_void", "overworld:aurora"), keys);
|
"minecraft:the_void", "iris:biomes/aurora"), keys);
|
||||||
assertFalse(keys.contains("minecraft:desert"));
|
assertFalse(keys.contains("minecraft:desert"));
|
||||||
assertFalse(keys.contains("minecraft:plains"));
|
assertFalse(keys.contains("minecraft:plains"));
|
||||||
|
|
||||||
|
|||||||
+31
@@ -52,6 +52,19 @@ public class ModdedForcedDatapackTest {
|
|||||||
assertEquals("iris:overworld", ModdedWorldgenIds.generatorIdentity("overworld"));
|
assertEquals("iris:overworld", ModdedWorldgenIds.generatorIdentity("overworld"));
|
||||||
assertEquals("iris:other/overworld",
|
assertEquals("iris:other/overworld",
|
||||||
ModdedWorldgenIds.generatorIdentity("other:overworld"));
|
ModdedWorldgenIds.generatorIdentity("other:overworld"));
|
||||||
|
assertEquals(
|
||||||
|
new ModdedWorldgenIds.ScopedDimension("overworld", "overworld"),
|
||||||
|
ModdedWorldgenIds.scopedDimensionType(
|
||||||
|
ModdedWorldgenIds.dimensionTypeRef("overworld", "overworld")
|
||||||
|
).orElseThrow()
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
List.of(
|
||||||
|
ModdedWorldgenIds.biomeRef("overworld", "overworld", "mist"),
|
||||||
|
"overworld:mist"
|
||||||
|
),
|
||||||
|
ModdedWorldgenIds.legacyBiomeRefs("overworld", "overworld", "mist")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -148,6 +161,24 @@ public class ModdedForcedDatapackTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsConflictingRegistryDefinitionsDuringMerge() throws IOException {
|
||||||
|
Path root = Files.createTempDirectory("iris-forced-pack-conflict");
|
||||||
|
try {
|
||||||
|
Path source = Files.createDirectories(root.resolve("source/data/iris/worldgen/biome"));
|
||||||
|
Path destination = Files.createDirectories(root.resolve("destination/data/iris/worldgen/biome"));
|
||||||
|
Files.writeString(source.resolve("shared.json"), "{\"temperature\":1}", StandardCharsets.UTF_8);
|
||||||
|
Files.writeString(destination.resolve("shared.json"), "{\"temperature\":2}", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
assertThrows(
|
||||||
|
IOException.class,
|
||||||
|
() -> ModdedForcedDatapack.mergeDirectory(root.resolve("source"), root.resolve("destination"))
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> directoryEntries(Path root) throws IOException {
|
private List<String> directoryEntries(Path root) throws IOException {
|
||||||
try (Stream<Path> entries = Files.list(root)) {
|
try (Stream<Path> entries = Files.list(root)) {
|
||||||
return entries.map((Path path) -> path.getFileName().toString())
|
return entries.map((Path path) -> path.getFileName().toString())
|
||||||
|
|||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedGenerationHistoryRoutingContractTest {
|
||||||
|
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startupPublishesOnlyAnImmutableHistoryBoundRuntime() throws IOException {
|
||||||
|
String source = source("ModdedWorldEngines.java");
|
||||||
|
String create = method(source, "private static Engine create(");
|
||||||
|
String createHistoryEngine = method(source, "private static Engine createHistoryEngine(");
|
||||||
|
String buildEngine = method(source, "private static IrisEngine buildEngine(");
|
||||||
|
|
||||||
|
assertTrue(create.contains("ModdedGenerationHistoryStorage.openOrAdopt("));
|
||||||
|
assertFalse(create.contains("resolvePack("));
|
||||||
|
assertTrue(createHistoryEngine.contains("candidate.attachGenerationHistory("));
|
||||||
|
assertTrue(createHistoryEngine.contains("IrisBoundarySignatureSampler.INSTANCE"));
|
||||||
|
assertTrue(createHistoryEngine.contains("getGenerationTransitionWidthBlocks()"));
|
||||||
|
assertTrue(createHistoryEngine.contains("router.preloadActiveRuntimes();"));
|
||||||
|
assertBefore(createHistoryEngine, "close(candidate);", "ModdedGenerationHistoryStorage.resolveActive(history)");
|
||||||
|
assertTrue(createHistoryEngine.contains("GenerationHistoryRuntimeRouter.attach("));
|
||||||
|
assertTrue(buildEngine.contains("active.packRoot().toFile()"));
|
||||||
|
assertTrue(buildEngine.contains("history.paths().activationMantleRoot(activation.activationId())"));
|
||||||
|
assertTrue(buildEngine.contains("epoch.kernelVersion()"));
|
||||||
|
assertTrue(buildEngine.contains("transitionPlan"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void onlyExplicitTransientStudioEnginesBypassHistory() throws IOException {
|
||||||
|
String manager = source("ModdedDimensionManager.java");
|
||||||
|
String transientCreate = method(manager, "public static Handle createTransientStudio(");
|
||||||
|
String persistentCreate = method(manager, "public static Handle createPersistent(");
|
||||||
|
String engines = source("ModdedWorldEngines.java");
|
||||||
|
String studio = method(engines, "private static IrisEngine createTransientStudioEngine(");
|
||||||
|
String generator = source("IrisModdedChunkGenerator.java");
|
||||||
|
String bypass = method(generator, "boolean allowsGenerationHistoryBypass(");
|
||||||
|
String route = method(generator, "private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(");
|
||||||
|
|
||||||
|
assertTrue(transientCreate.contains("ModdedGenerationMode.TRANSIENT_STUDIO"));
|
||||||
|
assertTrue(persistentCreate.contains("ModdedGenerationMode.PERSISTENT_CREATE"));
|
||||||
|
assertTrue(studio.contains("IrisData.openRuntime(packDir)"));
|
||||||
|
assertTrue(studio.contains("IrisEngine.InitializationMode.STUDIO"));
|
||||||
|
assertFalse(studio.contains("ModdedGenerationHistoryStorage"));
|
||||||
|
assertFalse(studio.contains("attachGenerationHistory("));
|
||||||
|
assertTrue(bypass.contains("generationMode != ModdedGenerationMode.TRANSIENT_STUDIO"));
|
||||||
|
assertTrue(bypass.contains("irisEngine.isStudio()"));
|
||||||
|
assertTrue(bypass.contains("Only an explicitly transient Iris Studio engine"));
|
||||||
|
assertBefore(route, "allowsGenerationHistoryBypass(current)", "requireHistoryRouter(current, operation)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyCoordinateBearingChunkStageOpensItsRoutedRuntime() throws IOException {
|
||||||
|
String source = source("IrisModdedChunkGenerator.java");
|
||||||
|
|
||||||
|
assertRuntimeRoute(source, "public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(");
|
||||||
|
assertCoordinateRoute(source, "public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(");
|
||||||
|
assertRuntimeRoute(source, "public CompletableFuture<ChunkAccess> createBiomes(");
|
||||||
|
assertRuntimeRoute(source, "public void applyCarvers(");
|
||||||
|
assertRuntimeRoute(source, "public void buildSurface(");
|
||||||
|
assertRuntimeRoute(source, "public void applyBiomeDecoration(");
|
||||||
|
assertRuntimeRoute(source, "public void createStructures(");
|
||||||
|
assertRuntimeRoute(source, "public void createReferences(");
|
||||||
|
assertRuntimeRoute(source, "public void spawnOriginalMobs(");
|
||||||
|
assertCoordinateRoute(source, "public int getBaseHeight(");
|
||||||
|
assertCoordinateRoute(source, "public NoiseColumn getBaseColumn(");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void asyncTerrainKeepsTheRouteUntilDurableSemanticClaim() throws IOException {
|
||||||
|
String source = source("IrisModdedChunkGenerator.java");
|
||||||
|
String fill = method(source, "public CompletableFuture<ChunkAccess> fillFromNoise(");
|
||||||
|
String terrain = method(source, "private ChunkAccess generateTerrain(");
|
||||||
|
String completion = method(source, "private static CompletableFuture<ChunkAccess> closeRouteOnCompletion(");
|
||||||
|
|
||||||
|
assertBefore(fill, "openHistoryRoute(", "CompletableFuture.supplyAsync(");
|
||||||
|
assertTrue(fill.contains("generateTerrain(chunk, generationEngine, pos, air, route)"));
|
||||||
|
assertTrue(fill.contains("return closeRouteOnCompletion(pipeline, route);"));
|
||||||
|
assertTrue(terrain.contains("openHistoryRuntimeScope(route)"));
|
||||||
|
assertBefore(terrain, "generationEngine.generate(", "route.claimGeneratedSemantics();");
|
||||||
|
assertBefore(terrain, "route.claimGeneratedSemantics();", "return chunk;");
|
||||||
|
assertTrue(completion.contains("pipeline.whenComplete("));
|
||||||
|
assertTrue(completion.contains("route.close();"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void biomeQueriesRouteEachCoordinateAndKeepRuntimeSpecificCaches() throws IOException {
|
||||||
|
String source = source("IrisModdedBiomeSource.java");
|
||||||
|
String structure = method(source, "public Holder<Biome> getNoiseBiome(");
|
||||||
|
String visible = method(source, "Holder<Biome> getVisibleNoiseBiome(");
|
||||||
|
String batch = method(source, "public Set<Holder<Biome>> getBiomesWithin(");
|
||||||
|
String resolution = method(source, "private Holder<Biome> resolveVisibleBiome(");
|
||||||
|
|
||||||
|
assertTrue(structure.contains("openHistoryCoordinateScope("));
|
||||||
|
assertTrue(visible.contains("openHistoryCoordinateScope("));
|
||||||
|
assertTrue(batch.contains("quartX << 2, quartZ << 2"));
|
||||||
|
assertTrue(source.contains("int runtimeIdentity = engine.getCacheID();"));
|
||||||
|
assertTrue(source.contains("cache.get(runtimeIdentity, key)"));
|
||||||
|
assertTrue(source.contains("cache.put(runtimeIdentity, key, resolved)"));
|
||||||
|
assertBefore(resolution, "historicalPhysicalBiomeKeyAt(", "ModdedWorldgenIds.biomeRef(engine");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void discreteStagesResumeIncompleteHistoricalChunksAndRejectTransitionOwnership() throws IOException {
|
||||||
|
String generator = source("IrisModdedChunkGenerator.java");
|
||||||
|
String discrete = method(generator, "private boolean allowsRoutedDiscreteGeneration(");
|
||||||
|
String decoration = method(generator, "public void applyBiomeDecoration(");
|
||||||
|
String structures = method(generator, "public void createStructures(");
|
||||||
|
String mobs = method(generator, "public void spawnOriginalMobs(");
|
||||||
|
String nativeStage = source("ModdedNativeStructureStage.java");
|
||||||
|
String adjust = method(nativeStage, "void adjustGeneratedStructures(");
|
||||||
|
String place = method(nativeStage, "void placeVanillaStructures(");
|
||||||
|
|
||||||
|
assertTrue(discrete.contains("chunk.getPersistedStatus().isOrAfter(stage)"));
|
||||||
|
assertFalse(discrete.contains("router.history().activeActivation().activationId()"));
|
||||||
|
assertFalse(discrete.contains("return true;"));
|
||||||
|
assertTrue(discrete.contains("allowsNewGenerationChunk("));
|
||||||
|
assertTrue(decoration.contains("ChunkStatus.FEATURES"));
|
||||||
|
assertTrue(structures.contains("ChunkStatus.STRUCTURE_STARTS"));
|
||||||
|
assertTrue(mobs.contains("ChunkStatus.SPAWN"));
|
||||||
|
assertBefore(decoration, "allowsRoutedDiscreteGeneration(", "nativeStructures.placeVanillaStructures(");
|
||||||
|
assertBefore(structures, "allowsRoutedDiscreteGeneration(", "super.createStructures(");
|
||||||
|
assertBefore(mobs, "allowsRoutedDiscreteGeneration(", "NaturalSpawner.spawnMobsForChunkGeneration(");
|
||||||
|
assertBefore(adjust, "allowsNewGenerationFootprint(", "recordWorldCheckStructureShift(");
|
||||||
|
assertTrue(place.contains("allowsNewGenerationFootprint("));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertRuntimeRoute(String source, String signature) {
|
||||||
|
String body = method(source, signature);
|
||||||
|
assertTrue(signature + " must open a generation-history route", body.contains("openHistoryRoute("));
|
||||||
|
assertTrue(signature + " must open its routed runtime", body.contains("openHistoryRuntimeScope(route)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertCoordinateRoute(String source, String signature) {
|
||||||
|
String body = method(source, signature);
|
||||||
|
assertTrue(signature + " must route its coordinate", body.contains("openHistoryCoordinateScope("));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertBefore(String source, String first, String second) {
|
||||||
|
int firstIndex = source.indexOf(first);
|
||||||
|
int secondIndex = source.indexOf(second);
|
||||||
|
assertTrue("Missing source contract token: " + first, firstIndex >= 0);
|
||||||
|
assertTrue("Missing source contract token: " + second, secondIndex >= 0);
|
||||||
|
assertTrue(first + " must occur before " + second, firstIndex < secondIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String source(String fileName) throws IOException {
|
||||||
|
String sourceRoot = System.getProperty(SOURCE_ROOT_PROPERTY);
|
||||||
|
assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY,
|
||||||
|
sourceRoot != null && !sourceRoot.isBlank());
|
||||||
|
return Files.readString(Path.of(sourceRoot, "art", "arcane", "iris", "modded", fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String method(String source, String signature) {
|
||||||
|
int start = source.indexOf(signature);
|
||||||
|
assertTrue("Missing source contract signature: " + signature, start >= 0);
|
||||||
|
int openBrace = source.indexOf('{', start);
|
||||||
|
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
|
||||||
|
int depth = 0;
|
||||||
|
for (int index = openBrace; index < source.length(); index++) {
|
||||||
|
char current = source.charAt(index);
|
||||||
|
if (current == '{') {
|
||||||
|
depth++;
|
||||||
|
} else if (current == '}') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) {
|
||||||
|
return source.substring(start, index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
+362
@@ -0,0 +1,362 @@
|
|||||||
|
package art.arcane.iris.modded;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationPackFingerprint;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ModdedGenerationHistoryStorageTest {
|
||||||
|
private static final String DIMENSION_FINGERPRINT = "11".repeat(32);
|
||||||
|
private static final String DEFINITION_FINGERPRINT = "22".repeat(32);
|
||||||
|
private static final int TRANSITION_WIDTH_BLOCKS = 256;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void newDimensionPublishesAnImmutableWorldLocalPack() throws IOException {
|
||||||
|
Path root = Files.createTempDirectory("iris-modded-history-create");
|
||||||
|
try {
|
||||||
|
Path source = pack(root.resolve("global-pack"), "first");
|
||||||
|
ModdedGenerationHistoryStorage.Candidate candidate = candidate(source, registryContract());
|
||||||
|
GenerationHistory history = ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
root.resolve("world-dimension"),
|
||||||
|
source,
|
||||||
|
41L,
|
||||||
|
candidate,
|
||||||
|
ModdedGenerationHistoryStorageTest::union,
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
);
|
||||||
|
|
||||||
|
Path activePack = history.activePackRoot();
|
||||||
|
assertEquals("first", Files.readString(activePack.resolve("content.txt"), StandardCharsets.UTF_8));
|
||||||
|
Files.writeString(source.resolve("content.txt"), "mutated", StandardCharsets.UTF_8);
|
||||||
|
assertEquals("first", Files.readString(activePack.resolve("content.txt"), StandardCharsets.UTF_8));
|
||||||
|
assertEquals(candidate.packFingerprint(), history.activeEpoch().packFingerprint());
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void existingDimensionAdoptsItsSelectedPackOnlyOnce() throws IOException {
|
||||||
|
Path root = Files.createTempDirectory("iris-modded-history-adopt");
|
||||||
|
try {
|
||||||
|
Path source = pack(root.resolve("global-pack"), "adopted");
|
||||||
|
Path dimensionRoot = root.resolve("world-dimension");
|
||||||
|
AtomicInteger sourceReads = new AtomicInteger();
|
||||||
|
ModdedGenerationHistoryStorage.AdoptionSourceSupplier supplier = () -> {
|
||||||
|
sourceReads.incrementAndGet();
|
||||||
|
return new ModdedGenerationHistoryStorage.AdoptionSource(
|
||||||
|
source,
|
||||||
|
candidate(source, registryContract())
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GenerationHistory adopted = ModdedGenerationHistoryStorage.restoreOrAdopt(
|
||||||
|
dimensionRoot,
|
||||||
|
91L,
|
||||||
|
supplier,
|
||||||
|
ModdedGenerationHistoryStorageTest::union
|
||||||
|
);
|
||||||
|
String epochId = adopted.activeEpoch().epochId();
|
||||||
|
Files.writeString(source.resolve("content.txt"), "changed", StandardCharsets.UTF_8);
|
||||||
|
GenerationHistory restored = ModdedGenerationHistoryStorage.restoreOrAdopt(
|
||||||
|
dimensionRoot,
|
||||||
|
91L,
|
||||||
|
supplier,
|
||||||
|
ModdedGenerationHistoryStorageTest::union
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(1, sourceReads.get());
|
||||||
|
assertEquals(epochId, restored.activeEpoch().epochId());
|
||||||
|
assertEquals("adopted", Files.readString(
|
||||||
|
restored.activePackRoot().resolve("content.txt"),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void selectedUpdateStagesWithoutReplacingTheActivePack() throws IOException {
|
||||||
|
Path root = Files.createTempDirectory("iris-modded-history-stage");
|
||||||
|
try {
|
||||||
|
Path dimensionRoot = root.resolve("world-dimension");
|
||||||
|
Path first = pack(root.resolve("first-pack"), "first");
|
||||||
|
GenerationHistory created = ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
first,
|
||||||
|
5L,
|
||||||
|
candidate(first, registryContract()),
|
||||||
|
ModdedGenerationHistoryStorageTest::union,
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
);
|
||||||
|
String activeEpoch = created.activeEpoch().epochId();
|
||||||
|
Path second = pack(root.resolve("second-pack"), "second");
|
||||||
|
GenerationHistory staged = ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
second,
|
||||||
|
5L,
|
||||||
|
candidate(second, registryContract()),
|
||||||
|
ModdedGenerationHistoryStorageTest::union,
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(activeEpoch, staged.activeEpoch().epochId());
|
||||||
|
assertTrue(staged.pendingActivation().isPresent());
|
||||||
|
assertNotEquals(activeEpoch, staged.pendingActivation().orElseThrow().epochId());
|
||||||
|
assertEquals("first", Files.readString(
|
||||||
|
staged.activePackRoot().resolve("content.txt"),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
));
|
||||||
|
assertEquals("second", Files.readString(
|
||||||
|
staged.packRoot(staged.pendingActivation().orElseThrow().activationId()).resolve("content.txt"),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unavailableUpdateRegistryDoesNotCreatePendingActivation() throws IOException {
|
||||||
|
Path root = Files.createTempDirectory("iris-modded-history-stage-registry");
|
||||||
|
try {
|
||||||
|
Path dimensionRoot = root.resolve("world-dimension");
|
||||||
|
Path first = pack(root.resolve("first-pack"), "first");
|
||||||
|
GenerationHistory history = ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
first,
|
||||||
|
63L,
|
||||||
|
candidate(first, registryContract()),
|
||||||
|
ModdedGenerationHistoryStorageTest::union,
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
);
|
||||||
|
Path second = pack(root.resolve("second-pack"), "second");
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
second,
|
||||||
|
63L,
|
||||||
|
candidate(second, registryContract()),
|
||||||
|
contracts -> GenerationRegistryContract.empty(),
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
));
|
||||||
|
|
||||||
|
assertTrue(history.pendingActivation().isEmpty());
|
||||||
|
assertEquals("first", Files.readString(
|
||||||
|
history.activePackRoot().resolve("content.txt"),
|
||||||
|
StandardCharsets.UTF_8
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void updateWithANewCustomBiomeKeyStagesBeforeThatKeyIsRegistered() throws IOException {
|
||||||
|
Path root = Files.createTempDirectory("iris-modded-history-new-biome");
|
||||||
|
try {
|
||||||
|
Path dimensionRoot = root.resolve("world-dimension");
|
||||||
|
Path first = pack(root.resolve("first-pack"), "first");
|
||||||
|
GenerationRegistryContract activeContract = registryContract("minecraft:plains");
|
||||||
|
ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
first,
|
||||||
|
71L,
|
||||||
|
candidate(first, activeContract),
|
||||||
|
ModdedGenerationHistoryStorageTest::union,
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
);
|
||||||
|
Path second = pack(root.resolve("second-pack"), "second");
|
||||||
|
GenerationRegistryContract candidateContract = registryContract(
|
||||||
|
"iris:biomes/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
);
|
||||||
|
AtomicInteger validationCalls = new AtomicInteger();
|
||||||
|
|
||||||
|
GenerationHistory staged = ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
second,
|
||||||
|
71L,
|
||||||
|
candidate(second, candidateContract),
|
||||||
|
contracts -> {
|
||||||
|
validationCalls.incrementAndGet();
|
||||||
|
assertEquals(1, contracts.size());
|
||||||
|
assertEquals(activeContract, contracts.iterator().next());
|
||||||
|
return union(contracts);
|
||||||
|
},
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(1, validationCalls.get());
|
||||||
|
assertTrue(staged.pendingActivation().isPresent());
|
||||||
|
assertEquals(
|
||||||
|
candidateContract,
|
||||||
|
staged.manifest()
|
||||||
|
.epoch(staged.pendingActivation().orElseThrow().epochId())
|
||||||
|
.orElseThrow()
|
||||||
|
.registryContract()
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void restoreRejectsASeedMismatchBeforeConsultingTheGlobalPack() throws IOException {
|
||||||
|
Path root = Files.createTempDirectory("iris-modded-history-seed");
|
||||||
|
try {
|
||||||
|
Path source = pack(root.resolve("global-pack"), "seeded");
|
||||||
|
Path dimensionRoot = root.resolve("world-dimension");
|
||||||
|
ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
source,
|
||||||
|
123L,
|
||||||
|
candidate(source, registryContract()),
|
||||||
|
ModdedGenerationHistoryStorageTest::union,
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
);
|
||||||
|
AtomicInteger sourceReads = new AtomicInteger();
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> ModdedGenerationHistoryStorage.restoreOrAdopt(
|
||||||
|
dimensionRoot,
|
||||||
|
124L,
|
||||||
|
() -> {
|
||||||
|
sourceReads.incrementAndGet();
|
||||||
|
return new ModdedGenerationHistoryStorage.AdoptionSource(
|
||||||
|
source,
|
||||||
|
candidate(source, registryContract())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
ModdedGenerationHistoryStorageTest::union
|
||||||
|
));
|
||||||
|
assertEquals(0, sourceReads.get());
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void restoreRejectsUnavailableRetainedRegistryDefinitions() throws IOException {
|
||||||
|
Path root = Files.createTempDirectory("iris-modded-history-registry");
|
||||||
|
try {
|
||||||
|
Path source = pack(root.resolve("global-pack"), "registry");
|
||||||
|
Path dimensionRoot = root.resolve("world-dimension");
|
||||||
|
ModdedGenerationHistoryStorage.createOrStage(
|
||||||
|
dimensionRoot,
|
||||||
|
source,
|
||||||
|
12L,
|
||||||
|
candidate(source, registryContract()),
|
||||||
|
ModdedGenerationHistoryStorageTest::union,
|
||||||
|
TRANSITION_WIDTH_BLOCKS
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> ModdedGenerationHistoryStorage.restoreOrAdopt(
|
||||||
|
dimensionRoot,
|
||||||
|
12L,
|
||||||
|
() -> {
|
||||||
|
throw new AssertionError("Existing history consulted the global pack");
|
||||||
|
},
|
||||||
|
contracts -> GenerationRegistryContract.empty()
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
deleteTree(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModdedGenerationHistoryStorage.Candidate candidate(
|
||||||
|
Path source,
|
||||||
|
GenerationRegistryContract registryContract
|
||||||
|
) throws IOException {
|
||||||
|
return new ModdedGenerationHistoryStorage.Candidate(
|
||||||
|
GenerationPackFingerprint.compute(source, GenerationPackFingerprint.CURRENT_VERSION),
|
||||||
|
dimensionContract(),
|
||||||
|
registryContract
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationEpoch.DimensionContract dimensionContract() {
|
||||||
|
return new GenerationEpoch.DimensionContract(
|
||||||
|
"surface",
|
||||||
|
"irisworldgen:packs/7061636b/dimensions/73757266616365/dimension_type",
|
||||||
|
"NORMAL",
|
||||||
|
"OVERWORLD",
|
||||||
|
127,
|
||||||
|
-64,
|
||||||
|
384,
|
||||||
|
384,
|
||||||
|
1D,
|
||||||
|
false,
|
||||||
|
"none",
|
||||||
|
0,
|
||||||
|
"0".repeat(64),
|
||||||
|
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
|
||||||
|
DIMENSION_FINGERPRINT
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationRegistryContract registryContract() {
|
||||||
|
return registryContract("minecraft:stone");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationRegistryContract registryContract(String resourceKey) {
|
||||||
|
return GenerationRegistryContract.fromDefinitions(Map.of(
|
||||||
|
new GenerationRegistryContract.PhysicalResourceKey(
|
||||||
|
resourceKey.startsWith("iris:biomes/")
|
||||||
|
? "minecraft:worldgen/biome"
|
||||||
|
: "minecraft:block",
|
||||||
|
resourceKey
|
||||||
|
),
|
||||||
|
DEFINITION_FINGERPRINT
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationRegistryContract union(Collection<GenerationRegistryContract> contracts)
|
||||||
|
throws IOException {
|
||||||
|
TreeMap<GenerationRegistryContract.PhysicalResourceKey, String> definitions = new TreeMap<>();
|
||||||
|
for (GenerationRegistryContract contract : contracts) {
|
||||||
|
for (Map.Entry<GenerationRegistryContract.PhysicalResourceKey, String> entry
|
||||||
|
: contract.definitions().entrySet()) {
|
||||||
|
String previous = definitions.putIfAbsent(entry.getKey(), entry.getValue());
|
||||||
|
if (previous != null && !previous.equals(entry.getValue())) {
|
||||||
|
throw new IOException("Conflicting registry definition");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GenerationRegistryContract.fromDefinitions(definitions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path pack(Path root, String content) throws IOException {
|
||||||
|
Files.createDirectories(root);
|
||||||
|
Files.writeString(root.resolve("content.txt"), content, StandardCharsets.UTF_8);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteTree(Path root) throws IOException {
|
||||||
|
if (!Files.exists(root)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (Stream<Path> walk = Files.walk(root)) {
|
||||||
|
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -26,7 +26,9 @@ public class ModdedGenerationLeaseContractTest {
|
|||||||
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
assertTrue(source.contains("catch (GenerationSessionException e)"));
|
||||||
assertTrue(source.contains("e.isExpectedTeardown()"));
|
assertTrue(source.contains("e.isExpectedTeardown()"));
|
||||||
assertTrue(source.contains("Iris structure biome key lookup was rejected during an engine transition"));
|
assertTrue(source.contains("Iris structure biome key lookup was rejected during an engine transition"));
|
||||||
assertTrue(source.contains("source.clearCache()"));
|
assertTrue(source.contains("int runtimeIdentity = engine.getCacheID();"));
|
||||||
|
assertTrue(source.contains("cache.get(runtimeIdentity, key)"));
|
||||||
|
assertTrue(source.contains("cache.put(runtimeIdentity, key, resolved)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -203,7 +205,7 @@ public class ModdedGenerationLeaseContractTest {
|
|||||||
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
|
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
|
||||||
String start = method(jobSource, "public static boolean start(");
|
String start = method(jobSource, "public static boolean start(");
|
||||||
int taskConstruction = start.indexOf("PregenTask task = PregenTask.builder()");
|
int taskConstruction = start.indexOf("PregenTask task = PregenTask.builder()");
|
||||||
int profileMutation = start.indexOf("PregenPerformanceProfile.apply(engine);");
|
int profileMutation = start.indexOf("PregenPerformanceProfile.apply(engine)");
|
||||||
|
|
||||||
assertTrue(taskConstruction >= 0);
|
assertTrue(taskConstruction >= 0);
|
||||||
assertTrue(profileMutation > taskConstruction);
|
assertTrue(profileMutation > taskConstruction);
|
||||||
|
|||||||
+36
-21
@@ -14,15 +14,16 @@ public class ModdedLifecycleFailureContractTest {
|
|||||||
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
|
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void existingDimensionRepointBindsBeforeHandlePublication() throws IOException {
|
public void existingDimensionStagesWithoutReplacingTheLiveEngine() throws IOException {
|
||||||
String managerSource = source("ModdedDimensionManager.java");
|
String managerSource = source("ModdedDimensionManager.java");
|
||||||
String create = method(managerSource, "public static Handle create(");
|
String create = method(managerSource, "private static Handle create(");
|
||||||
int existingStart = requiredIndex(create, "if (existing != null");
|
int existingStart = requiredIndex(create, "if (existing != null");
|
||||||
int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1);
|
int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1);
|
||||||
int injectionStart = requiredIndex(create, "try {", presentStart);
|
int injectionStart = requiredIndex(create, "try {", presentStart);
|
||||||
|
|
||||||
assertRepointBeforePublication(create.substring(existingStart, presentStart));
|
assertFalse(create.substring(existingStart, presentStart).contains("repointAndBind("));
|
||||||
assertRepointBeforePublication(create.substring(presentStart, injectionStart));
|
assertFalse(create.substring(presentStart, injectionStart).contains("repointAndBind("));
|
||||||
|
assertBefore(create, "ModdedGenerationHistoryStorage.createOrStage(", "if (existing != null");
|
||||||
|
|
||||||
String generatorSource = source("IrisModdedChunkGenerator.java");
|
String generatorSource = source("IrisModdedChunkGenerator.java");
|
||||||
String repointAndBind = method(generatorSource, "void repointAndBind(");
|
String repointAndBind = method(generatorSource, "void repointAndBind(");
|
||||||
@@ -40,6 +41,31 @@ public class ModdedLifecycleFailureContractTest {
|
|||||||
assertFalse(installReplacement.contains("catch ("));
|
assertFalse(installReplacement.contains("catch ("));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void persistentPackUpdatesUseAnExplicitRestartBoundPath() throws IOException {
|
||||||
|
String managerSource = source("ModdedDimensionManager.java");
|
||||||
|
String createPersistent = method(managerSource, "public static Handle createPersistent(");
|
||||||
|
String update = method(managerSource, "public static UpdateResult stagePersistentUpdate(");
|
||||||
|
|
||||||
|
assertBefore(createPersistent, "if (previous != null)", "ModdedDimensionRegistryStore.put(");
|
||||||
|
assertTrue(createPersistent.contains("Use the Iris world update command"));
|
||||||
|
assertTrue(update.contains("ModdedGenerationHistoryStorage.createOrStage("));
|
||||||
|
assertTrue(update.contains("active.history().pendingActivation()"));
|
||||||
|
assertTrue(update.contains("restartRequired()"));
|
||||||
|
assertFalse(update.contains("inject("));
|
||||||
|
assertFalse(update.contains("repointAndBind("));
|
||||||
|
assertBefore(update, "ModdedDimensionRegistryStore.put(server, updated);",
|
||||||
|
"ModdedGenerationHistoryStorage.createOrStage(");
|
||||||
|
assertTrue(update.contains("ModdedDimensionRegistryStore.put(server, previous);"));
|
||||||
|
|
||||||
|
String commandsSource = source("command/ModdedWorldCommands.java");
|
||||||
|
assertTrue(commandsSource.contains("root.then(updateTree());"));
|
||||||
|
String command = method(commandsSource, "private static int update(");
|
||||||
|
assertTrue(command.contains("ModdedDimensionManager.stagePersistentUpdate("));
|
||||||
|
assertTrue(command.contains("Restart the server to activate it"));
|
||||||
|
assertTrue(command.contains("running world remains on its current pack"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException {
|
public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException {
|
||||||
String source = source("ModdedDimensionManager.java");
|
String source = source("ModdedDimensionManager.java");
|
||||||
@@ -110,18 +136,13 @@ public class ModdedLifecycleFailureContractTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException {
|
public void persistentRestoreOpensHistoryBeforeConsultingTheGlobalPack() throws IOException {
|
||||||
String source = source("ModdedDimensionManager.java");
|
String source = source("ModdedGenerationHistoryStorage.java");
|
||||||
String loading = method(source, "private static IrisDimension loadPackDimension(");
|
String restore = method(source, "static GenerationHistory restoreOrAdopt(");
|
||||||
|
|
||||||
assertFalse(loading.contains("return null;"));
|
assertBefore(restore, "GenerationHistory.openIfPresent(", "sourceSupplier\").get();");
|
||||||
assertFalse(loading.contains("e.toString()"));
|
assertTrue(restore.contains("if (existing.isPresent())"));
|
||||||
int catchIndex = loading.indexOf("catch (");
|
assertBefore(restore, "requireRegistryDefinitions(existing.get()", "return existing.get();");
|
||||||
if (catchIndex >= 0) {
|
|
||||||
String failure = blockAt(loading, requiredIndex(loading, "{", catchIndex));
|
|
||||||
assertTrue(failure.contains("throw "));
|
|
||||||
assertTrue(failure.contains(", e);") || failure.contains("throw e;"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -249,12 +270,6 @@ public class ModdedLifecycleFailureContractTest {
|
|||||||
assertBefore(failure, "rollback.restore(failure);", "throw ");
|
assertBefore(failure, "rollback.restore(failure);", "throw ");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertRepointBeforePublication(String branch) {
|
|
||||||
assertBefore(branch, "repointAndBind(", "new Handle(");
|
|
||||||
assertBefore(branch, "repointAndBind(", "HANDLES.put(");
|
|
||||||
assertBefore(branch, "repointAndBind(", "return ");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertBefore(String source, String first, String second) {
|
private static void assertBefore(String source, String first, String second) {
|
||||||
int firstIndex = requiredIndex(source, first);
|
int firstIndex = requiredIndex(source, first);
|
||||||
int secondIndex = requiredIndex(source, second);
|
int secondIndex = requiredIndex(source, second);
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class ModdedRiverLocateContractTest {
|
||||||
|
@Test
|
||||||
|
public void riverLookupMergesRecordedAndEligibleActiveFeatures() throws IOException {
|
||||||
|
Path sourcePath = Path.of(System.getProperty("iris.moddedCommonSources"))
|
||||||
|
.resolve("art/arcane/iris/modded/command/ModdedLocateCommands.java");
|
||||||
|
String source = Files.readString(sourcePath);
|
||||||
|
int methodStart = source.indexOf("static int gotoRiver(");
|
||||||
|
int methodEnd = source.indexOf("static int gotoStructure(", methodStart);
|
||||||
|
String method = source.substring(methodStart, methodEnd);
|
||||||
|
|
||||||
|
assertTrue(method.contains("GenerationSemanticQueries.nearestRiver("));
|
||||||
|
assertTrue(method.contains("runtime == null"));
|
||||||
|
assertTrue(method.contains("feature.y() + 2"));
|
||||||
|
assertFalse(method.contains("runtime.nearestFeature("));
|
||||||
|
assertFalse(method.contains("feature.y() + engine.getDimension().getMinHeight()"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package art.arcane.iris.modded.command;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public final class ModdedStructureHistoryContractTest {
|
||||||
|
@Test
|
||||||
|
public void retainedStructureNamesReachTheSemanticSearchOnBothAdmissionBranches() throws IOException {
|
||||||
|
Path root = Path.of(System.getProperty("iris.moddedCommonSources"));
|
||||||
|
String source = Files.readString(root.resolve("art/arcane/iris/modded/command/ModdedLocateCommands.java"));
|
||||||
|
int start = source.indexOf("static int gotoStructure(");
|
||||||
|
int end = source.indexOf("static String registeredStructureUnavailableMessage(", start);
|
||||||
|
String admission = source.substring(start, end);
|
||||||
|
String retainedCheck = "GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)";
|
||||||
|
int firstCheck = admission.indexOf(retainedCheck);
|
||||||
|
assertTrue(firstCheck >= 0);
|
||||||
|
assertTrue(admission.indexOf(retainedCheck, firstCheck + retainedCheck.length()) >= 0);
|
||||||
|
|
||||||
|
int locateStart = source.indexOf("private static void locateIrisStructure(");
|
||||||
|
int locateEnd = source.indexOf("private static void runNativeStructureLocate(", locateStart);
|
||||||
|
String locate = source.substring(locateStart, locateEnd);
|
||||||
|
assertTrue(locate.contains("GenerationSemanticQueries.nearestStructure("));
|
||||||
|
assertFalse(locate.contains("IrisStructureLocator.locate("));
|
||||||
|
|
||||||
|
String suggestions = Files.readString(root.resolve("art/arcane/iris/modded/command/ModdedCommandSuggestions.java"));
|
||||||
|
assertTrue(suggestions.contains("GenerationFindCatalog.retainedStructureKeys(engine)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -21,8 +21,8 @@ public class ModdedStudioTeleportContractTest {
|
|||||||
assertTrue(open.contains("TRANSITIONS.submit("));
|
assertTrue(open.contains("TRANSITIONS.submit("));
|
||||||
assertFalse(open.contains("orTimeout("));
|
assertFalse(open.contains("orTimeout("));
|
||||||
assertFalse(open.contains("deadlineNanos"));
|
assertFalse(open.contains("deadlineNanos"));
|
||||||
assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.create(");
|
assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.createTransientStudio(");
|
||||||
assertBefore(execute, "ModdedDimensionManager.create(", "ModdedDimensionManager.teleportAsync(");
|
assertBefore(execute, "ModdedDimensionManager.createTransientStudio(", "ModdedDimensionManager.teleportAsync(");
|
||||||
assertFalse(execute.contains("deadlineNanos"));
|
assertFalse(execute.contains("deadlineNanos"));
|
||||||
assertFalse(source.contains("player.teleportTo(studio"));
|
assertFalse(source.contains("player.teleportTo(studio"));
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -8,6 +8,7 @@ import art.arcane.iris.engine.framework.NativeStructureOwnershipBundle;
|
|||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructure;
|
import art.arcane.iris.engine.object.IrisNativeStructure;
|
||||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||||
@@ -429,6 +430,14 @@ public class NativeStructureReferenceRepairTest {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
|
||||||
|
int blockX,
|
||||||
|
int blockZ
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isClosing() {
|
public boolean isClosing() {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ plugins {
|
|||||||
alias(libs.plugins.shadow)
|
alias(libs.plugins.shadow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: file('../../gradle/modded-generation-kernel-seals.gradle')
|
||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
||||||
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
|
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.0-26.2'))
|
||||||
@@ -132,6 +134,7 @@ dependencies {
|
|||||||
"art.arcane:core:${irisVersion}".toString(),
|
"art.arcane:core:${irisVersion}".toString(),
|
||||||
"art.arcane:spi:${irisVersion}".toString(),
|
"art.arcane:spi:${irisVersion}".toString(),
|
||||||
libs.paralithic,
|
libs.paralithic,
|
||||||
|
libs.asm,
|
||||||
libs.lru,
|
libs.lru,
|
||||||
libs.caffeine,
|
libs.caffeine,
|
||||||
libs.dom4j,
|
libs.dom4j,
|
||||||
|
|||||||
@@ -426,6 +426,8 @@ tasks.named('neoforgeJar').configure {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<String> commonModdedArtifactEntries = [
|
List<String> commonModdedArtifactEntries = [
|
||||||
|
'META-INF/iris/generation-kernels/catalog.tsv',
|
||||||
|
'META-INF/iris/generation-kernels/abi-1.seal',
|
||||||
'art/arcane/iris/engine/IrisEngine.class',
|
'art/arcane/iris/engine/IrisEngine.class',
|
||||||
'art/arcane/iris/spi/IrisPlatform.class',
|
'art/arcane/iris/spi/IrisPlatform.class',
|
||||||
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
|
'art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class',
|
||||||
@@ -530,6 +532,8 @@ tasks.register('verifyModdedArtifacts') {
|
|||||||
// the two plugin descriptors and the slimjar manifests. Nothing in the reference graph points at
|
// the two plugin descriptors and the slimjar manifests. Nothing in the reference graph points at
|
||||||
// them, so a shadowJar exclude that drops one is silent until a server hits that path.
|
// them, so a shadowJar exclude that drops one is silent until a server hits that path.
|
||||||
List<String> requiredBukkitArtifactEntries = [
|
List<String> requiredBukkitArtifactEntries = [
|
||||||
|
'META-INF/iris/generation-kernels/catalog.tsv',
|
||||||
|
'META-INF/iris/generation-kernels/abi-1.seal',
|
||||||
'plugin.yml',
|
'plugin.yml',
|
||||||
'paper-plugin.yml',
|
'paper-plugin.yml',
|
||||||
'slimjar.dat',
|
'slimjar.dat',
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ repositories {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(gradleApi())
|
implementation(gradleApi())
|
||||||
implementation('org.ow2.asm:asm:9.10.1')
|
implementation('org.ow2.asm:asm:9.10.1')
|
||||||
|
implementation('org.apache.commons:commons-compress:1.28.0')
|
||||||
implementation('com.github.VolmitSoftware:NMSTools:c88961416f')
|
implementation('com.github.VolmitSoftware:NMSTools:c88961416f')
|
||||||
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
|
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
|
import org.apache.commons.compress.archivers.zip.ExtraFieldUtils;
|
||||||
|
import org.apache.commons.compress.archivers.zip.Zip64Mode;
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||||
import org.gradle.api.GradleException;
|
import org.gradle.api.GradleException;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipFile;
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
|
|
||||||
public final class JarCompactor {
|
public final class JarCompactor {
|
||||||
private static final int BUFFER_BYTES = 64 * 1024;
|
private static final int BUFFER_BYTES = 64 * 1024;
|
||||||
@@ -45,31 +49,50 @@ public final class JarCompactor {
|
|||||||
|
|
||||||
private static void rewrite(Path source, Path destination) throws IOException {
|
private static void rewrite(Path source, Path destination) throws IOException {
|
||||||
byte[] buffer = new byte[BUFFER_BYTES];
|
byte[] buffer = new byte[BUFFER_BYTES];
|
||||||
try (InputStream rawInput = new BufferedInputStream(Files.newInputStream(source));
|
try (ZipFile input = new ZipFile(source.toFile());
|
||||||
ZipInputStream input = new ZipInputStream(rawInput);
|
ZipArchiveOutputStream output = new ZipArchiveOutputStream(destination)) {
|
||||||
OutputStream rawOutput = new BufferedOutputStream(Files.newOutputStream(destination));
|
if (!output.isSeekable()) {
|
||||||
ZipOutputStream output = new ZipOutputStream(rawOutput)) {
|
throw new IOException("Jar compaction requires seekable output.");
|
||||||
|
}
|
||||||
output.setLevel(9);
|
output.setLevel(9);
|
||||||
ZipEntry entry;
|
output.setUseZip64(Zip64Mode.Never);
|
||||||
while ((entry = input.getNextEntry()) != null) {
|
output.setCreateUnicodeExtraFields(ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER);
|
||||||
|
Set<String> names = new HashSet<>();
|
||||||
|
Enumeration<? extends ZipEntry> entries = input.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
ZipEntry entry = entries.nextElement();
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ZipEntry compacted = copyMetadata(entry);
|
if (!names.add(entry.getName())) {
|
||||||
output.putNextEntry(compacted);
|
throw new IOException("Duplicate jar entry: " + entry.getName());
|
||||||
int read;
|
|
||||||
while ((read = input.read(buffer)) >= 0) {
|
|
||||||
if (read > 0) {
|
|
||||||
output.write(buffer, 0, read);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
output.closeEntry();
|
output.putArchiveEntry(copyMetadata(entry));
|
||||||
|
copyEntry(input, output, entry, buffer);
|
||||||
|
output.closeArchiveEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ZipEntry copyMetadata(ZipEntry source) {
|
private static void copyEntry(ZipFile input, ZipArchiveOutputStream output, ZipEntry entry, byte[] buffer)
|
||||||
ZipEntry target = new ZipEntry(source.getName());
|
throws IOException {
|
||||||
|
CRC32 crc = new CRC32();
|
||||||
|
long size = 0L;
|
||||||
|
try (InputStream content = input.getInputStream(entry)) {
|
||||||
|
int read;
|
||||||
|
while ((read = content.read(buffer)) != -1) {
|
||||||
|
output.write(buffer, 0, read);
|
||||||
|
crc.update(buffer, 0, read);
|
||||||
|
size += read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (size != entry.getSize() || crc.getValue() != entry.getCrc()) {
|
||||||
|
throw new IOException("Jar entry failed size or CRC verification: " + entry.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ZipArchiveEntry copyMetadata(ZipEntry source) throws IOException {
|
||||||
|
ZipArchiveEntry target = new ZipArchiveEntry(source.getName());
|
||||||
target.setMethod(ZipEntry.DEFLATED);
|
target.setMethod(ZipEntry.DEFLATED);
|
||||||
if (source.getTime() >= 0L) {
|
if (source.getTime() >= 0L) {
|
||||||
target.setTime(source.getTime());
|
target.setTime(source.getTime());
|
||||||
@@ -78,7 +101,8 @@ public final class JarCompactor {
|
|||||||
target.setComment(source.getComment());
|
target.setComment(source.getComment());
|
||||||
}
|
}
|
||||||
if (source.getExtra() != null) {
|
if (source.getExtra() != null) {
|
||||||
target.setExtra(source.getExtra());
|
target.setExtraFields(ExtraFieldUtils.parse(source.getExtra(), true,
|
||||||
|
ZipArchiveEntry.ExtraFieldParsingMode.BEST_EFFORT));
|
||||||
}
|
}
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,463 @@
|
|||||||
|
package art.arcane.iris.buildtools;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
public final class GenerationKernelSeal {
|
||||||
|
public static final String FORMAT = "iris-generation-kernel-seal-v1";
|
||||||
|
private static final Pattern HASH = Pattern.compile("[0-9a-f]{64}");
|
||||||
|
private static final long MAXIMUM_SEAL_BYTES = 8L * 1024L * 1024L;
|
||||||
|
private static final int MAXIMUM_ENTRIES = 30_000;
|
||||||
|
|
||||||
|
private GenerationKernelSeal() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] arguments) throws IOException {
|
||||||
|
if (arguments.length < 4 || arguments.length % 2 != 0 || !arguments[0].equals("verify-dependencies")) {
|
||||||
|
throw new IllegalArgumentException("Expected verify-dependencies <seal> <identity> <artifact> pairs.");
|
||||||
|
}
|
||||||
|
Map<String, Path> dependencies = new TreeMap<>();
|
||||||
|
for (int index = 2; index < arguments.length; index += 2) {
|
||||||
|
if (dependencies.putIfAbsent(arguments[index], Path.of(arguments[index + 1])) != null) {
|
||||||
|
throw new IllegalArgumentException("Duplicate resolved generation dependency.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verifyDependencySubset(Path.of(arguments[1]), dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void verifyDependencySubset(Path seal, Map<String, Path> dependencies) throws IOException {
|
||||||
|
SourceManifest expected = read(seal);
|
||||||
|
for (Map.Entry<String, String> dependency : dependencyHashes(dependencies).entrySet()) {
|
||||||
|
if (!Objects.equals(expected.dependencies().get(dependency.getKey()), dependency.getValue())) {
|
||||||
|
throw new IOException("Resolved platform dependency does not match generation ABI " + expected.abi()
|
||||||
|
+ ": " + dependency.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void verifyCatalog(Path catalog, Map<Integer, SourceManifest> manifests) throws IOException {
|
||||||
|
if (!Files.isRegularFile(catalog) || Files.size(catalog) > 16 * 1024) {
|
||||||
|
throw new IOException("Missing or oversized generation kernel catalog.");
|
||||||
|
}
|
||||||
|
String source = Files.readString(catalog, StandardCharsets.UTF_8);
|
||||||
|
String[] lines = source.split("\n", -1);
|
||||||
|
if (!source.endsWith("\n") || source.indexOf('\r') >= 0 || lines.length < 4 || lines.length > 128
|
||||||
|
|| !lines[0].equals("iris-generation-kernel-catalog-v1")) {
|
||||||
|
throw new IOException("Invalid generation kernel catalog.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String[] current = lines[1].split("\t", -1);
|
||||||
|
if (current.length != 4 || !current[0].equals("current")) {
|
||||||
|
throw new IllegalArgumentException("Missing current generation version.");
|
||||||
|
}
|
||||||
|
int currentAbi = positive(current[1]);
|
||||||
|
AlgorithmVersion algorithms = new AlgorithmVersion(positive(current[2]), positive(current[3]));
|
||||||
|
SourceManifest selected = manifests.get(currentAbi);
|
||||||
|
if (selected == null || !selected.algorithms().contains(algorithms)) {
|
||||||
|
throw new IllegalArgumentException("Current generation version has no matching sealed factory.");
|
||||||
|
}
|
||||||
|
TreeSet<Integer> registered = new TreeSet<>();
|
||||||
|
int previous = 0;
|
||||||
|
for (int index = 2; index < lines.length - 1; index++) {
|
||||||
|
String[] kernel = lines[index].split("\t", -1);
|
||||||
|
if (kernel.length != 2 || !kernel[0].equals("kernel")) {
|
||||||
|
throw new IllegalArgumentException("Invalid kernel registration.");
|
||||||
|
}
|
||||||
|
int abi = positive(kernel[1]);
|
||||||
|
if (abi <= previous) {
|
||||||
|
throw new IllegalArgumentException("Kernel registrations must be unique and sorted.");
|
||||||
|
}
|
||||||
|
previous = abi;
|
||||||
|
registered.add(abi);
|
||||||
|
}
|
||||||
|
if (!registered.equals(manifests.keySet())) {
|
||||||
|
throw new IllegalArgumentException("Kernel catalog must register exactly the retained sealed kernels.");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException failure) {
|
||||||
|
throw new IOException("Invalid generation kernel catalog.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int positive(String value) {
|
||||||
|
if (!value.matches("[1-9][0-9]{0,8}")) {
|
||||||
|
throw new IllegalArgumentException("Invalid generation version.");
|
||||||
|
}
|
||||||
|
return Integer.parseInt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SourceManifest capture(CaptureOptions options, Map<String, Path> dependencies) throws IOException {
|
||||||
|
CaptureOptions required = Objects.requireNonNull(options, "options");
|
||||||
|
Map<String, String> sourceHashes = sources(required.repository(), required.roots(), required.exclusions());
|
||||||
|
if (sourceHashes.isEmpty()) {
|
||||||
|
throw new IOException("Generation kernel source scope is empty.");
|
||||||
|
}
|
||||||
|
return new SourceManifest(required.abi(), required.factoryClass(), required.algorithms(), required.roots(), required.exclusions(),
|
||||||
|
sourceHashes, dependencyHashes(dependencies));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void verify(Path repository, Path seal, Map<String, Path> dependencies) throws IOException {
|
||||||
|
SourceManifest expected = read(seal);
|
||||||
|
SourceManifest actual = capture(new CaptureOptions(repository, expected.abi(), expected.factoryClass(), expected.algorithms(),
|
||||||
|
expected.roots(), expected.exclusions()), dependencies);
|
||||||
|
List<String> failures = new ArrayList<>();
|
||||||
|
differences("source", expected.sources(), actual.sources(), failures);
|
||||||
|
differences("dependency", expected.dependencies(), actual.dependencies(), failures);
|
||||||
|
if (!failures.isEmpty()) {
|
||||||
|
throw new IOException("Generation ABI " + expected.abi() + " no longer matches its sealed implementation:\n"
|
||||||
|
+ String.join("\n", failures)
|
||||||
|
+ "\nRetain this kernel's implementation and add a separately versioned kernel for changed generation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeNew(Path destination, SourceManifest manifest) throws IOException {
|
||||||
|
Files.createDirectories(destination.toAbsolutePath().getParent());
|
||||||
|
Files.writeString(destination, encode(manifest), StandardCharsets.UTF_8,
|
||||||
|
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SourceManifest read(Path path) throws IOException {
|
||||||
|
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) > MAXIMUM_SEAL_BYTES) {
|
||||||
|
throw new IOException("Missing or oversized generation kernel seal: " + path);
|
||||||
|
}
|
||||||
|
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
|
||||||
|
if (lines.size() < 4 || !FORMAT.equals(lines.getFirst()) || lines.size() > MAXIMUM_ENTRIES) {
|
||||||
|
throw new IOException("Invalid generation kernel seal: " + path);
|
||||||
|
}
|
||||||
|
int abi = 0;
|
||||||
|
String factory = null;
|
||||||
|
List<String> roots = new ArrayList<>();
|
||||||
|
List<AlgorithmVersion> algorithms = new ArrayList<>();
|
||||||
|
List<String> exclusions = new ArrayList<>();
|
||||||
|
Map<String, String> sources = new TreeMap<>();
|
||||||
|
Map<String, String> dependencies = new TreeMap<>();
|
||||||
|
try {
|
||||||
|
for (int index = 1; index < lines.size(); index++) {
|
||||||
|
String[] entry = lines.get(index).split("\t", -1);
|
||||||
|
if (entry.length < 2 || entry.length > 3) {
|
||||||
|
throw new IllegalArgumentException("Invalid seal entry.");
|
||||||
|
}
|
||||||
|
switch (entry[0]) {
|
||||||
|
case "abi" -> {
|
||||||
|
if (abi != 0 || entry.length != 2) {
|
||||||
|
throw new IllegalArgumentException("Duplicate or invalid ABI.");
|
||||||
|
}
|
||||||
|
abi = Integer.parseInt(entry[1]);
|
||||||
|
}
|
||||||
|
case "factory" -> {
|
||||||
|
if (factory != null || entry.length != 2) {
|
||||||
|
throw new IllegalArgumentException("Duplicate or invalid factory.");
|
||||||
|
}
|
||||||
|
factory = entry[1];
|
||||||
|
}
|
||||||
|
case "scope" -> addPath(roots, entry);
|
||||||
|
case "algorithm" -> {
|
||||||
|
if (entry.length != 3) {
|
||||||
|
throw new IllegalArgumentException("Invalid algorithm version.");
|
||||||
|
}
|
||||||
|
algorithms.add(new AlgorithmVersion(Integer.parseInt(entry[1]), Integer.parseInt(entry[2])));
|
||||||
|
}
|
||||||
|
case "exclude" -> addPath(exclusions, entry);
|
||||||
|
case "source" -> addHash(sources, entry, true);
|
||||||
|
case "dependency" -> addHash(dependencies, entry, false);
|
||||||
|
default -> throw new IllegalArgumentException("Unknown generation kernel seal entry.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SourceManifest manifest = new SourceManifest(abi, factory, algorithms, roots, exclusions, sources, dependencies);
|
||||||
|
if (!Files.readString(path, StandardCharsets.UTF_8).equals(encode(manifest))) {
|
||||||
|
throw new IllegalArgumentException("Generation kernel seal is not canonical.");
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
throw new IOException("Invalid generation kernel seal: " + path, failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String fingerprint(SourceManifest manifest) {
|
||||||
|
return hash(encode(manifest).getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String encode(SourceManifest manifest) {
|
||||||
|
StringBuilder text = new StringBuilder(FORMAT).append('\n');
|
||||||
|
text.append("abi\t").append(manifest.abi()).append('\n');
|
||||||
|
text.append("factory\t").append(manifest.factoryClass()).append('\n');
|
||||||
|
for (AlgorithmVersion algorithm : manifest.algorithms()) {
|
||||||
|
text.append("algorithm\t").append(algorithm.rng()).append('\t').append(algorithm.seed()).append('\n');
|
||||||
|
}
|
||||||
|
for (String scope : manifest.roots()) {
|
||||||
|
text.append("scope\t").append(scope).append('\n');
|
||||||
|
}
|
||||||
|
for (String exclusion : manifest.exclusions()) {
|
||||||
|
text.append("exclude\t").append(exclusion).append('\n');
|
||||||
|
}
|
||||||
|
appendHashes(text, "source", manifest.sources());
|
||||||
|
appendHashes(text, "dependency", manifest.dependencies());
|
||||||
|
return text.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String artifactHash(Path artifact) throws IOException {
|
||||||
|
if (!Files.isRegularFile(artifact)) {
|
||||||
|
throw new IOException("Missing generation dependency artifact: " + artifact);
|
||||||
|
}
|
||||||
|
MessageDigest digest = digest();
|
||||||
|
try (ZipFile zip = new ZipFile(artifact.toFile())) {
|
||||||
|
hashRuntimeManifest(zip, digest);
|
||||||
|
Map<String, ZipEntry> entries = new TreeMap<>();
|
||||||
|
for (ZipEntry entry : zip.stream().toList()) {
|
||||||
|
String name = entry.getName();
|
||||||
|
if (entry.isDirectory() || metadata(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entries.putIfAbsent(name, entry) != null) {
|
||||||
|
throw new IOException("Duplicate dependency artifact entry: " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, ZipEntry> entry : entries.entrySet()) {
|
||||||
|
update(digest, entry.getKey());
|
||||||
|
try (InputStream input = zip.getInputStream(entry.getValue())) {
|
||||||
|
MessageDigest content = digest();
|
||||||
|
byte[] buffer = new byte[16 * 1024];
|
||||||
|
int read;
|
||||||
|
while ((read = input.read(buffer)) != -1) {
|
||||||
|
content.update(buffer, 0, read);
|
||||||
|
}
|
||||||
|
update(digest, HexFormat.of().formatHex(content.digest()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void hashRuntimeManifest(ZipFile zip, MessageDigest digest) throws IOException {
|
||||||
|
ZipEntry entry = zip.getEntry("META-INF/MANIFEST.MF");
|
||||||
|
if (entry == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Manifest manifest;
|
||||||
|
try (InputStream input = zip.getInputStream(entry)) {
|
||||||
|
manifest = new Manifest(input);
|
||||||
|
}
|
||||||
|
List<String> runtimeAttributes = List.of("Multi-Release", "Automatic-Module-Name", "Class-Path", "Sealed",
|
||||||
|
"Agent-Class", "Premain-Class", "Launcher-Agent-Class", "Boot-Class-Path",
|
||||||
|
"Can-Redefine-Classes", "Can-Retransform-Classes", "Can-Set-Native-Method-Prefix");
|
||||||
|
for (String name : runtimeAttributes) {
|
||||||
|
String value = manifest.getMainAttributes().getValue(name);
|
||||||
|
if (value != null) {
|
||||||
|
update(digest, "manifest/" + name);
|
||||||
|
update(digest, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, Attributes> section : new TreeMap<>(manifest.getEntries()).entrySet()) {
|
||||||
|
String sealed = section.getValue().getValue("Sealed");
|
||||||
|
if (sealed != null) {
|
||||||
|
update(digest, "manifest/section/" + section.getKey());
|
||||||
|
update(digest, sealed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> sources(Path repository, List<String> roots, List<String> exclusions)
|
||||||
|
throws IOException {
|
||||||
|
Path base = repository.toAbsolutePath().normalize();
|
||||||
|
Map<String, String> hashes = new TreeMap<>();
|
||||||
|
for (String root : roots) {
|
||||||
|
Path sourceRoot = base.resolve(root).normalize();
|
||||||
|
if (!sourceRoot.startsWith(base) || !Files.exists(sourceRoot, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
throw new IOException("Missing generation source scope: " + root);
|
||||||
|
}
|
||||||
|
try (Stream<Path> walk = Files.walk(sourceRoot)) {
|
||||||
|
for (Path source : walk.toList()) {
|
||||||
|
String relative = base.relativize(source).toString().replace('\\', '/');
|
||||||
|
if (excluded(relative, exclusions)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Files.isSymbolicLink(source)) {
|
||||||
|
throw new IOException("Generation source must not be a symbolic link: " + relative);
|
||||||
|
}
|
||||||
|
if (Files.isRegularFile(source) && (source.equals(sourceRoot)
|
||||||
|
|| source.getFileName().toString().endsWith(".java"))) {
|
||||||
|
String content = Files.readString(source, StandardCharsets.UTF_8).replace("\r\n", "\n");
|
||||||
|
hashes.put(relative, hash(content.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> dependencyHashes(Map<String, Path> dependencies) throws IOException {
|
||||||
|
Map<String, String> hashes = new TreeMap<>();
|
||||||
|
for (Map.Entry<String, Path> dependency : Objects.requireNonNull(dependencies, "dependencies").entrySet()) {
|
||||||
|
String key = token(dependency.getKey());
|
||||||
|
hashes.put(key, artifactHash(dependency.getValue()));
|
||||||
|
}
|
||||||
|
if (hashes.isEmpty()) {
|
||||||
|
throw new IOException("Generation kernel dependency scope is empty.");
|
||||||
|
}
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void differences(String type, Map<String, String> expected, Map<String, String> actual,
|
||||||
|
List<String> failures) {
|
||||||
|
TreeSet<String> keys = new TreeSet<>(expected.keySet());
|
||||||
|
keys.addAll(actual.keySet());
|
||||||
|
for (String key : keys) {
|
||||||
|
if (!Objects.equals(expected.get(key), actual.get(key))) {
|
||||||
|
String change = !expected.containsKey(key) ? "added" : !actual.containsKey(key) ? "deleted" : "changed";
|
||||||
|
failures.add(type + " " + change + ": " + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean excluded(String path, List<String> exclusions) {
|
||||||
|
for (String exclusion : exclusions) {
|
||||||
|
if (path.equals(exclusion) || path.startsWith(exclusion + "/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean metadata(String name) {
|
||||||
|
return name.equals("META-INF/MANIFEST.MF") || name.startsWith("META-INF/maven/")
|
||||||
|
|| name.startsWith("META-INF/") && (name.endsWith(".SF") || name.endsWith(".RSA") || name.endsWith(".DSA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addPath(List<String> paths, String[] entry) {
|
||||||
|
if (entry.length != 2 || paths.contains(entry[1])) {
|
||||||
|
throw new IllegalArgumentException("Duplicate or invalid source scope.");
|
||||||
|
}
|
||||||
|
paths.add(relative(entry[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addHash(Map<String, String> hashes, String[] entry, boolean path) {
|
||||||
|
if (entry.length != 3 || !HASH.matcher(entry[2]).matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid sealed hash.");
|
||||||
|
}
|
||||||
|
String key = path ? relative(entry[1]) : token(entry[1]);
|
||||||
|
if (hashes.putIfAbsent(key, entry[2]) != null) {
|
||||||
|
throw new IllegalArgumentException("Duplicate sealed entry.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendHashes(StringBuilder text, String type, Map<String, String> hashes) {
|
||||||
|
for (Map.Entry<String, String> entry : new TreeMap<>(hashes).entrySet()) {
|
||||||
|
text.append(type).append('\t').append(entry.getKey()).append('\t').append(entry.getValue()).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String relative(String value) {
|
||||||
|
String path = token(value);
|
||||||
|
if (path.startsWith("/") || path.contains("\\") || path.contains(":")
|
||||||
|
|| !Path.of(path).normalize().toString().replace('\\', '/').equals(path)
|
||||||
|
|| path.equals("..") || path.startsWith("../")) {
|
||||||
|
throw new IllegalArgumentException("Invalid generation source path: " + path);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String token(String value) {
|
||||||
|
String required = Objects.requireNonNull(value, "seal token");
|
||||||
|
if (required.isBlank() || required.chars().anyMatch(Character::isWhitespace)) {
|
||||||
|
throw new IllegalArgumentException("Invalid generation seal token.");
|
||||||
|
}
|
||||||
|
return required;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> paths(Collection<String> values) {
|
||||||
|
TreeSet<String> sorted = new TreeSet<>();
|
||||||
|
for (String value : values) {
|
||||||
|
if (!sorted.add(relative(value))) {
|
||||||
|
throw new IllegalArgumentException("Duplicate generation source scope.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(sorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String hash(byte[] bytes) {
|
||||||
|
return HexFormat.of().formatHex(digest().digest(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MessageDigest digest() {
|
||||||
|
try {
|
||||||
|
return MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException failure) {
|
||||||
|
throw new IllegalStateException(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void update(MessageDigest digest, String value) {
|
||||||
|
digest.update(value.getBytes(StandardCharsets.UTF_8));
|
||||||
|
digest.update((byte) '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CaptureOptions(Path repository, int abi, String factoryClass, List<AlgorithmVersion> algorithms, List<String> roots,
|
||||||
|
List<String> exclusions) {
|
||||||
|
public CaptureOptions {
|
||||||
|
Objects.requireNonNull(repository, "repository");
|
||||||
|
if (abi < 1) {
|
||||||
|
throw new IllegalArgumentException("Generation ABI must be positive.");
|
||||||
|
}
|
||||||
|
factoryClass = token(factoryClass);
|
||||||
|
TreeSet<AlgorithmVersion> sortedAlgorithms = new TreeSet<>(algorithms);
|
||||||
|
if (sortedAlgorithms.isEmpty() || sortedAlgorithms.size() != algorithms.size()) {
|
||||||
|
throw new IllegalArgumentException("Generation algorithm versions must be nonempty and unique.");
|
||||||
|
}
|
||||||
|
algorithms = List.copyOf(sortedAlgorithms);
|
||||||
|
roots = paths(roots);
|
||||||
|
exclusions = paths(exclusions);
|
||||||
|
if (roots.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Generation source scope must not be empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SourceManifest(int abi, String factoryClass, List<AlgorithmVersion> algorithms, List<String> roots, List<String> exclusions,
|
||||||
|
Map<String, String> sources, Map<String, String> dependencies) {
|
||||||
|
public SourceManifest {
|
||||||
|
CaptureOptions options = new CaptureOptions(Path.of("."), abi, factoryClass, algorithms, roots, exclusions);
|
||||||
|
algorithms = options.algorithms();
|
||||||
|
roots = options.roots();
|
||||||
|
exclusions = options.exclusions();
|
||||||
|
sources = Map.copyOf(sources);
|
||||||
|
dependencies = Map.copyOf(dependencies);
|
||||||
|
if (sources.isEmpty() || dependencies.isEmpty() || sources.size() + dependencies.size() > MAXIMUM_ENTRIES) {
|
||||||
|
throw new IllegalArgumentException("Invalid generation kernel seal entry count.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AlgorithmVersion(int rng, int seed) implements Comparable<AlgorithmVersion> {
|
||||||
|
public AlgorithmVersion {
|
||||||
|
if (rng < 1 || seed < 1) {
|
||||||
|
throw new IllegalArgumentException("Generation algorithm versions must be positive.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(AlgorithmVersion other) {
|
||||||
|
int byRng = Integer.compare(rng, other.rng);
|
||||||
|
return byRng == 0 ? Integer.compare(seed, other.seed) : byRng;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import art.arcane.iris.buildtools.GenerationKernelSeal;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class GenerationKernelSealTest {
|
||||||
|
@Rule
|
||||||
|
public final TemporaryFolder temporary = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifiesUnchangedSourceAndCanonicalDependencyContents() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
|
||||||
|
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
|
||||||
|
artifact(fixture.dependency(), "same code", "another version", 1_700_000_000_000L);
|
||||||
|
assertEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
|
||||||
|
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
|
||||||
|
assertEquals(GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())),
|
||||||
|
GenerationKernelSeal.fingerprint(GenerationKernelSeal.capture(fixture.options(), fixture.dependencies())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsChangedAddedAndDeletedGenerationSources() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
Path source = fixture.root().resolve("generation/Noise.java");
|
||||||
|
Files.writeString(source, "class Noise { int value = 2; }");
|
||||||
|
assertTrue(assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
|
||||||
|
.getMessage().contains("source changed"));
|
||||||
|
Files.writeString(source, "class Noise { int value = 1; }");
|
||||||
|
Path added = fixture.root().resolve("generation/Added.java");
|
||||||
|
Files.writeString(added, "class Added {}");
|
||||||
|
assertTrue(assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
|
||||||
|
.getMessage().contains("source added"));
|
||||||
|
Files.delete(added);
|
||||||
|
Files.delete(source);
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsChangedAddedAndDeletedDependencies() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
|
||||||
|
artifact(fixture.dependency(), "changed code", "same version", 0L);
|
||||||
|
assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
|
||||||
|
assertTrue(assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()))
|
||||||
|
.getMessage().contains("dependency changed"));
|
||||||
|
artifact(fixture.dependency(), "same code", "same version", 0L);
|
||||||
|
assertThrows(IOException.class, () -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(),
|
||||||
|
Map.of("math", fixture.dependency(), "new-math", fixture.dependency())));
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), Map.of()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsMultiReleaseManifestChanges() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
|
||||||
|
artifact(fixture.dependency(), "same code", "same version\r\nMulti-Release: true", 0L);
|
||||||
|
assertNotEquals(original, GenerationKernelSeal.artifactHash(fixture.dependency()));
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsAgentEntrypointAndCapabilityChanges() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
String original = GenerationKernelSeal.artifactHash(fixture.dependency());
|
||||||
|
for (String attribute : List.of("Agent-Class", "Premain-Class", "Launcher-Agent-Class", "Boot-Class-Path",
|
||||||
|
"Can-Redefine-Classes", "Can-Retransform-Classes", "Can-Set-Native-Method-Prefix")) {
|
||||||
|
artifact(fixture.dependency(), "same code", "same version\r\n" + attribute + ": changed", 0L);
|
||||||
|
assertNotEquals(attribute, original, GenerationKernelSeal.artifactHash(fixture.dependency()));
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sealsExplicitAgentBuildInputsAndExecutableSources() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
Path agentSource = fixture.root().resolve("agent/src/main/java/Installer.java");
|
||||||
|
Path agentBuild = fixture.root().resolve("agent/build.gradle");
|
||||||
|
Files.createDirectories(agentSource.getParent());
|
||||||
|
Files.writeString(agentSource, "class Installer {}");
|
||||||
|
Files.writeString(agentBuild, "plugins { id 'java' }");
|
||||||
|
GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions(
|
||||||
|
fixture.root(), 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)),
|
||||||
|
List.of("generation", "agent/build.gradle", "agent/src/main/java"), List.of("generation/kernels"));
|
||||||
|
Path seal = fixture.root().resolve("agent.seal");
|
||||||
|
GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, fixture.dependencies()));
|
||||||
|
GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies());
|
||||||
|
Files.writeString(agentBuild, "plugins { id 'application' }");
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies()));
|
||||||
|
Files.writeString(agentBuild, "plugins { id 'java' }");
|
||||||
|
Files.writeString(agentSource, "class Installer { boolean defers; }");
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verify(fixture.root(), seal, fixture.dependencies()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checksTheActualPlatformDependencySubset() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies());
|
||||||
|
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(),
|
||||||
|
Map.of("unknown-library", fixture.dependency())));
|
||||||
|
artifact(fixture.dependency(), "platform override", "same version", 0L);
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.verifyDependencySubset(fixture.seal(), fixture.dependencies()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void catalogCannotSelectAnUnsealedAlgorithmTuple() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
Path catalog = fixture.root().resolve("catalog.tsv");
|
||||||
|
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\n");
|
||||||
|
Map<Integer, GenerationKernelSeal.SourceManifest> manifests = Map.of(1, GenerationKernelSeal.read(fixture.seal()));
|
||||||
|
GenerationKernelSeal.verifyCatalog(catalog, manifests);
|
||||||
|
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t2\t1\nkernel\t1\n");
|
||||||
|
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyCatalog(catalog, manifests));
|
||||||
|
Files.writeString(catalog, "iris-generation-kernel-catalog-v1\ncurrent\t1\t1\t1\nkernel\t1\nkernel\t2\n");
|
||||||
|
assertThrows(IOException.class, () -> GenerationKernelSeal.verifyCatalog(catalog, manifests));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void permitsSeparatelyScopedFutureKernelsWithoutChangingRetainedSeal() throws Exception {
|
||||||
|
Fixture fixture = fixture();
|
||||||
|
Path future = fixture.root().resolve("generation/kernels/v2/NewMath.java");
|
||||||
|
Files.createDirectories(future.getParent());
|
||||||
|
Files.writeString(future, "class NewMath {}");
|
||||||
|
GenerationKernelSeal.verify(fixture.root(), fixture.seal(), fixture.dependencies());
|
||||||
|
GenerationKernelSeal.SourceManifest next = GenerationKernelSeal.capture(new GenerationKernelSeal.CaptureOptions(
|
||||||
|
fixture.root(), 2, "example.KernelV2", List.of(new GenerationKernelSeal.AlgorithmVersion(2, 3)),
|
||||||
|
List.of("generation/kernels/v2"), List.of()), fixture.dependencies());
|
||||||
|
assertNotEquals(GenerationKernelSeal.fingerprint(next),
|
||||||
|
GenerationKernelSeal.fingerprint(GenerationKernelSeal.read(fixture.seal())));
|
||||||
|
assertThrows(IOException.class,
|
||||||
|
() -> GenerationKernelSeal.writeNew(fixture.seal(), next));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Fixture fixture() throws Exception {
|
||||||
|
Path root = temporary.newFolder().toPath();
|
||||||
|
Files.createDirectories(root.resolve("generation"));
|
||||||
|
Files.writeString(root.resolve("generation/Noise.java"), "class Noise { int value = 1; }");
|
||||||
|
Path dependency = root.resolve("math.jar");
|
||||||
|
artifact(dependency, "same code", "same version", 0L);
|
||||||
|
GenerationKernelSeal.CaptureOptions options = new GenerationKernelSeal.CaptureOptions(
|
||||||
|
root, 1, "example.KernelV1", List.of(new GenerationKernelSeal.AlgorithmVersion(1, 1)),
|
||||||
|
List.of("generation"), List.of("generation/kernels"));
|
||||||
|
Path seal = root.resolve("abi-1.seal");
|
||||||
|
Map<String, Path> dependencies = Map.of("math", dependency);
|
||||||
|
GenerationKernelSeal.writeNew(seal, GenerationKernelSeal.capture(options, dependencies));
|
||||||
|
return new Fixture(root, seal, dependency, dependencies, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void artifact(Path path, String content, String version, long timestamp) throws IOException {
|
||||||
|
try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(path))) {
|
||||||
|
ZipEntry metadata = new ZipEntry("META-INF/MANIFEST.MF");
|
||||||
|
metadata.setTime(timestamp);
|
||||||
|
output.putNextEntry(metadata);
|
||||||
|
output.write(("Manifest-Version: 1.0\r\nImplementation-Version: " + version + "\r\n\r\n")
|
||||||
|
.getBytes(StandardCharsets.UTF_8));
|
||||||
|
output.closeEntry();
|
||||||
|
ZipEntry code = new ZipEntry("example/Math.class");
|
||||||
|
code.setTime(timestamp);
|
||||||
|
output.putNextEntry(code);
|
||||||
|
output.write(content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
output.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Fixture(Path root, Path seal, Path dependency, Map<String, Path> dependencies,
|
||||||
|
GenerationKernelSeal.CaptureOptions options) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,33 @@
|
|||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||||
|
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||||
|
import org.gradle.api.GradleException;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
import java.util.jar.JarFile;
|
import java.util.jar.JarFile;
|
||||||
import java.util.jar.JarOutputStream;
|
import java.util.jar.JarOutputStream;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class JarCompactorTest {
|
public class JarCompactorTest {
|
||||||
@Rule
|
@Rule
|
||||||
@@ -36,4 +53,128 @@ public class JarCompactorTest {
|
|||||||
jar.getJarEntry("example/value.txt")).readAllBytes());
|
jar.getJarEntry("example/value.txt")).readAllBytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void preservesJarIdentityAndProducesDeterministicSeekableOutput() throws Exception {
|
||||||
|
File artifact = temporaryFolder.newFile("identity.jar");
|
||||||
|
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact));
|
||||||
|
InputStream classResource = getClass().getResourceAsStream("/JarCompactorTest.class")) {
|
||||||
|
output.setLevel(9);
|
||||||
|
writeEntry(output, "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\r\nMulti-Release: true\r\n\r\n"
|
||||||
|
.getBytes(StandardCharsets.UTF_8));
|
||||||
|
writeEntry(output, "JarCompactorTest.class", classResource.readAllBytes());
|
||||||
|
writeEntry(output, "META-INF/iris/generation-kernels/abi-1.seal", "registry fixture".getBytes(StandardCharsets.UTF_8));
|
||||||
|
writeEntry(output, "example/caf\u00e9.txt", "unicode name".getBytes(StandardCharsets.UTF_8));
|
||||||
|
for (int index = 0; index < 100; index++) {
|
||||||
|
writeEntry(output, "example/item-" + index + ".txt", ("value-" + index).getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, EntryIdentity> original = identity(artifact);
|
||||||
|
long originalBytes = artifact.length();
|
||||||
|
|
||||||
|
JarCompactor.compact(artifact);
|
||||||
|
|
||||||
|
assertEquals(original, identity(artifact));
|
||||||
|
assertTrue(artifact.length() < originalBytes);
|
||||||
|
try (JarFile jar = new JarFile(artifact)) {
|
||||||
|
assertEquals("true", jar.getManifest().getMainAttributes().getValue("Multi-Release"));
|
||||||
|
assertEquals(0xCA, jar.getInputStream(jar.getJarEntry("JarCompactorTest.class")).read());
|
||||||
|
}
|
||||||
|
try (ZipFile zip = ZipFile.builder().setFile(artifact).get()) {
|
||||||
|
Enumeration<ZipArchiveEntry> entries = zip.getEntries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
assertFalse(entries.nextElement().getGeneralPurposeBit().usesDataDescriptor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] first = Files.readAllBytes(artifact.toPath());
|
||||||
|
JarCompactor.compact(artifact);
|
||||||
|
assertArrayEquals(first, Files.readAllBytes(artifact.toPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsMalformedArchivesWithoutReplacingTheSource() throws Exception {
|
||||||
|
File artifact = temporaryFolder.newFile("invalid.jar");
|
||||||
|
byte[] original = "not a zip archive".getBytes(StandardCharsets.UTF_8);
|
||||||
|
Files.write(artifact.toPath(), original);
|
||||||
|
|
||||||
|
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
|
||||||
|
|
||||||
|
assertArrayEquals(original, Files.readAllBytes(artifact.toPath()));
|
||||||
|
assertEquals(1, temporaryFolder.getRoot().listFiles().length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsCorruptPayloadWithoutReplacingTheSource() throws Exception {
|
||||||
|
File artifact = temporaryFolder.newFile("corrupt.jar");
|
||||||
|
byte[] payload = "checked-payload".getBytes(StandardCharsets.UTF_8);
|
||||||
|
CRC32 crc = new CRC32();
|
||||||
|
crc.update(payload);
|
||||||
|
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) {
|
||||||
|
JarEntry entry = new JarEntry("value.txt");
|
||||||
|
entry.setMethod(ZipEntry.STORED);
|
||||||
|
entry.setSize(payload.length);
|
||||||
|
entry.setCrc(crc.getValue());
|
||||||
|
output.putNextEntry(entry);
|
||||||
|
output.write(payload);
|
||||||
|
output.closeEntry();
|
||||||
|
}
|
||||||
|
byte[] corrupt = Files.readAllBytes(artifact.toPath());
|
||||||
|
int payloadOffset = new String(corrupt, StandardCharsets.ISO_8859_1).indexOf("checked-payload");
|
||||||
|
assertTrue(payloadOffset > 0);
|
||||||
|
corrupt[payloadOffset] ^= 1;
|
||||||
|
Files.write(artifact.toPath(), corrupt);
|
||||||
|
|
||||||
|
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
|
||||||
|
|
||||||
|
assertArrayEquals(corrupt, Files.readAllBytes(artifact.toPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectsDuplicateEntriesWithoutReplacingTheSource() throws Exception {
|
||||||
|
File artifact = temporaryFolder.newFile("duplicate.jar");
|
||||||
|
try (ZipArchiveOutputStream output = new ZipArchiveOutputStream(artifact)) {
|
||||||
|
for (int index = 0; index < 2; index++) {
|
||||||
|
output.putArchiveEntry(new ZipArchiveEntry("duplicate.txt"));
|
||||||
|
output.write(index);
|
||||||
|
output.closeArchiveEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] original = Files.readAllBytes(artifact.toPath());
|
||||||
|
|
||||||
|
assertThrows(GradleException.class, () -> JarCompactor.compact(artifact));
|
||||||
|
|
||||||
|
assertArrayEquals(original, Files.readAllBytes(artifact.toPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeEntry(JarOutputStream output, String name, byte[] content) throws Exception {
|
||||||
|
JarEntry entry = new JarEntry(name);
|
||||||
|
entry.setTime(315_532_800_000L);
|
||||||
|
entry.setComment("preserved entry comment");
|
||||||
|
entry.setExtra(new byte[]{(byte) 0xFE, (byte) 0xCA, 1, 0, 7});
|
||||||
|
output.putNextEntry(entry);
|
||||||
|
output.write(content);
|
||||||
|
output.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, EntryIdentity> identity(File artifact) throws Exception {
|
||||||
|
Map<String, EntryIdentity> entries = new TreeMap<>();
|
||||||
|
try (JarFile jar = new JarFile(artifact)) {
|
||||||
|
Enumeration<JarEntry> contents = jar.entries();
|
||||||
|
while (contents.hasMoreElements()) {
|
||||||
|
JarEntry entry = contents.nextElement();
|
||||||
|
byte[] bytes;
|
||||||
|
try (InputStream input = jar.getInputStream(entry)) {
|
||||||
|
bytes = input.readAllBytes();
|
||||||
|
}
|
||||||
|
entries.put(entry.getName(), new EntryIdentity(
|
||||||
|
HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)),
|
||||||
|
entry.getSize(), entry.getCrc(), entry.getTime(), entry.getComment(),
|
||||||
|
entry.getExtra() == null ? "" : HexFormat.of().formatHex(entry.getExtra())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record EntryIdentity(String sha256, long size, long crc, long time, String comment, String extra) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,14 @@ plugins {
|
|||||||
id 'java'
|
id 'java'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation('junit:junit:4.13.2')
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('jar', Jar).configure {
|
tasks.named('jar', Jar).configure {
|
||||||
manifest.attributes(
|
manifest.attributes(
|
||||||
'Agent-Class': 'art.arcane.iris.util.project.agent.Installer',
|
'Agent-Class': 'art.arcane.iris.util.project.agent.Installer',
|
||||||
|
|||||||
@@ -1,9 +1,41 @@
|
|||||||
package art.arcane.iris.util.project.agent;
|
package art.arcane.iris.util.project.agent;
|
||||||
|
|
||||||
import java.lang.instrument.Instrumentation;
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class Installer {
|
public class Installer {
|
||||||
private static volatile Instrumentation instrumentation;
|
private static volatile Instrumentation instrumentation;
|
||||||
|
private static final Map<ClassLoader, Boolean> retainedClassLoaders = new IdentityHashMap<>();
|
||||||
|
|
||||||
|
public static synchronized void retainClassLoader(ClassLoader loader) {
|
||||||
|
ClassLoader requiredLoader = Objects.requireNonNull(loader, "Plugin class loader");
|
||||||
|
if (!(requiredLoader instanceof Closeable)) {
|
||||||
|
throw new IllegalArgumentException("Plugin class loader is not closeable");
|
||||||
|
}
|
||||||
|
retainedClassLoaders.putIfAbsent(requiredLoader, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized boolean deferClassLoaderClose(ClassLoader loader) {
|
||||||
|
if (!retainedClassLoaders.containsKey(loader)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
retainedClassLoaders.put(loader, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void releaseClassLoader(ClassLoader loader) throws IOException {
|
||||||
|
boolean closeRequested;
|
||||||
|
synchronized (Installer.class) {
|
||||||
|
closeRequested = Boolean.TRUE.equals(retainedClassLoaders.remove(loader));
|
||||||
|
}
|
||||||
|
if (closeRequested) {
|
||||||
|
((Closeable) loader).close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Instrumentation getInstrumentation() {
|
public static Instrumentation getInstrumentation() {
|
||||||
Instrumentation instrumentation = Installer.instrumentation;
|
Instrumentation instrumentation = Installer.instrumentation;
|
||||||
@@ -26,4 +58,4 @@ public class Installer {
|
|||||||
return;
|
return;
|
||||||
Installer.instrumentation = instrumentation;
|
Installer.instrumentation = instrumentation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
package art.arcane.iris.util.project.agent;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarOutputStream;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ClassLoaderCloseDeferralTest {
|
||||||
|
@Test
|
||||||
|
public void unarmedPluginDisableClosesNormally() throws Exception {
|
||||||
|
TestLoader loader = new TestLoader(new URL[0]);
|
||||||
|
|
||||||
|
loader.close();
|
||||||
|
Installer.releaseClassLoader(loader);
|
||||||
|
|
||||||
|
assertEquals(1, loader.closes.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void holdingOnePluginDoesNotHoldAnotherEqualLoader() throws Exception {
|
||||||
|
TestLoader iris = new TestLoader(new URL[0]);
|
||||||
|
TestLoader other = new TestLoader(new URL[0]);
|
||||||
|
Installer.retainClassLoader(iris);
|
||||||
|
try {
|
||||||
|
other.close();
|
||||||
|
iris.close();
|
||||||
|
|
||||||
|
assertEquals(1, other.closes.get());
|
||||||
|
assertEquals(0, iris.closes.get());
|
||||||
|
} finally {
|
||||||
|
Installer.releaseClassLoader(iris);
|
||||||
|
}
|
||||||
|
assertEquals(1, iris.closes.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void deferredJarRemainsReadableUntilCleanupReleasesIt() throws Exception {
|
||||||
|
Path jar = Files.createTempFile("iris-loader-close", ".jar");
|
||||||
|
try {
|
||||||
|
try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(jar))) {
|
||||||
|
output.putNextEntry(new JarEntry("late-cleanup.txt"));
|
||||||
|
output.write("available".getBytes(StandardCharsets.UTF_8));
|
||||||
|
output.closeEntry();
|
||||||
|
}
|
||||||
|
TestLoader loader = new TestLoader(new URL[]{jar.toUri().toURL()});
|
||||||
|
Installer.retainClassLoader(loader);
|
||||||
|
try {
|
||||||
|
loader.close();
|
||||||
|
loader.close();
|
||||||
|
try (InputStream input = loader.getResourceAsStream("late-cleanup.txt")) {
|
||||||
|
assertEquals("available", new String(input.readAllBytes(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Installer.releaseClassLoader(loader);
|
||||||
|
}
|
||||||
|
Installer.releaseClassLoader(loader);
|
||||||
|
assertEquals(1, loader.closes.get());
|
||||||
|
assertNull(loader.getResourceAsStream("late-cleanup.txt"));
|
||||||
|
} finally {
|
||||||
|
Files.delete(jar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cancellingBeforePaperRequestsCloseDoesNotCloseTheLoader() throws Exception {
|
||||||
|
TestLoader loader = new TestLoader(new URL[0]);
|
||||||
|
Installer.retainClassLoader(loader);
|
||||||
|
Installer.releaseClassLoader(loader);
|
||||||
|
|
||||||
|
assertEquals(0, loader.closes.get());
|
||||||
|
loader.close();
|
||||||
|
assertEquals(1, loader.closes.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentReleaseClosesOnceOutsideLifecycleLock() throws Exception {
|
||||||
|
TestLoader loader = new TestLoader(new URL[0]);
|
||||||
|
loader.closeEntered = new CountDownLatch(1);
|
||||||
|
loader.finishClose = new CountDownLatch(1);
|
||||||
|
Installer.retainClassLoader(loader);
|
||||||
|
loader.close();
|
||||||
|
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||||
|
Thread first = new Thread(() -> release(loader, failure), "first-loader-release");
|
||||||
|
first.start();
|
||||||
|
try {
|
||||||
|
assertTrue(loader.closeEntered.await(5L, TimeUnit.SECONDS));
|
||||||
|
Thread second = new Thread(() -> release(loader, failure), "second-loader-release");
|
||||||
|
second.start();
|
||||||
|
second.join(5000L);
|
||||||
|
assertFalse(second.isAlive());
|
||||||
|
assertEquals(1, loader.closes.get());
|
||||||
|
} finally {
|
||||||
|
loader.finishClose.countDown();
|
||||||
|
first.join(5000L);
|
||||||
|
}
|
||||||
|
assertFalse(first.isAlive());
|
||||||
|
assertNull(failure.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void release(TestLoader loader, AtomicReference<Throwable> failure) {
|
||||||
|
try {
|
||||||
|
Installer.releaseClassLoader(loader);
|
||||||
|
} catch (Throwable exception) {
|
||||||
|
failure.compareAndSet(null, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestLoader extends URLClassLoader {
|
||||||
|
private final AtomicInteger closes = new AtomicInteger();
|
||||||
|
private CountDownLatch closeEntered;
|
||||||
|
private CountDownLatch finishClose;
|
||||||
|
|
||||||
|
private TestLoader(URL[] urls) {
|
||||||
|
super(urls, ClassLoader.getPlatformClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (Installer.deferClassLoaderClose(this)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assertFalse(Thread.holdsLock(Installer.class));
|
||||||
|
closes.incrementAndGet();
|
||||||
|
if (closeEntered != null) {
|
||||||
|
closeEntered.countDown();
|
||||||
|
try {
|
||||||
|
if (!finishClose.await(5L, TimeUnit.SECONDS)) {
|
||||||
|
throw new IOException("Loader close was not released");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException exception) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
return other instanceof TestLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,6 +128,7 @@ dependencies {
|
|||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
testImplementation('org.mockito:mockito-core:5.23.0')
|
testImplementation('org.mockito:mockito-core:5.23.0')
|
||||||
testImplementation(libs.paper.api)
|
testImplementation(libs.paper.api)
|
||||||
|
testImplementation(libs.log4j.core)
|
||||||
testRuntimeOnly(libs.paper.api)
|
testRuntimeOnly(libs.paper.api)
|
||||||
testImplementation(libs.multiverseCore)
|
testImplementation(libs.multiverseCore)
|
||||||
}
|
}
|
||||||
@@ -520,3 +521,5 @@ tasks.register('bukkitPurityRatchet') {
|
|||||||
tasks.named('check').configure {
|
tasks.named('check').configure {
|
||||||
dependsOn('bukkitPurityRatchet')
|
dependsOn('bukkitPurityRatchet')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply from: rootProject.file('gradle/generation-kernel-seals.gradle')
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,3 @@
|
|||||||
|
iris-generation-kernel-catalog-v1
|
||||||
|
current 1 1 1
|
||||||
|
kernel 1
|
||||||
|
@@ -9,8 +9,14 @@ import art.arcane.iris.core.nms.datapack.DataVersion;
|
|||||||
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||||
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
|
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
|
||||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract.PhysicalResourceKey;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.engine.object.IrisCustomBiomeAliasResolver;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -40,13 +46,14 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.atomic.AtomicIntegerArray;
|
import java.util.concurrent.atomic.AtomicIntegerArray;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
public final class IrisDatapackCompiler {
|
public final class IrisDatapackCompiler {
|
||||||
private static final int INPUT_FINGERPRINT_SCHEMA = 2;
|
private static final int INPUT_FINGERPRINT_SCHEMA = 3;
|
||||||
private static final int INPUT_BUFFER_BYTES = 64 * 1024;
|
private static final int INPUT_BUFFER_BYTES = 64 * 1024;
|
||||||
private static final int WORLD_PACK_SCAN_DEPTH = 8;
|
private static final int WORLD_PACK_SCAN_DEPTH = 8;
|
||||||
private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
private static final Pattern REGISTRY_KEY_PATTERN = Pattern.compile("[a-z0-9_.-]+:[a-z0-9/._-]+");
|
||||||
@@ -90,6 +97,7 @@ public final class IrisDatapackCompiler {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
LinkedHashMap<Path, File> roots = new LinkedHashMap<>();
|
LinkedHashMap<Path, File> roots = new LinkedHashMap<>();
|
||||||
collectInstalledPackRoots(dataDirectory.resolve("packs"), roots, validateWholePack);
|
collectInstalledPackRoots(dataDirectory.resolve("packs"), roots, validateWholePack);
|
||||||
|
collectCanonicalWorldPackRoot(serverRoot, roots, validateWholePack);
|
||||||
collectWorldPackRoots(serverRoot.resolve("dimensions"), roots, validateWholePack);
|
collectWorldPackRoots(serverRoot.resolve("dimensions"), roots, validateWholePack);
|
||||||
return new ArrayList<>(roots.values());
|
return new ArrayList<>(roots.values());
|
||||||
}
|
}
|
||||||
@@ -179,14 +187,18 @@ public final class IrisDatapackCompiler {
|
|||||||
Objects.requireNonNull(packRoots, "packRoots");
|
Objects.requireNonNull(packRoots, "packRoots");
|
||||||
Objects.requireNonNull(fixer, "fixer");
|
Objects.requireNonNull(fixer, "fixer");
|
||||||
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
|
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
|
||||||
Set<String> installedBiomeKeys = new LinkedHashSet<>();
|
Map<String, Set<String>> biomeTags = new TreeMap<>();
|
||||||
for (File packRoot : packRoots) {
|
for (File packRoot : packRoots) {
|
||||||
PackDirectoryResolver.requireSafePackTree(packRoot);
|
PackDirectoryResolver.requireSafePackTree(packRoot);
|
||||||
if (!hasDimensions(packRoot.toPath())) {
|
if (!hasDimensions(packRoot.toPath())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
BiomeAliasContext aliasContext = biomeAliasContext(packRoot.toPath());
|
||||||
IrisData data = IrisData.openDatapackCompiler(packRoot);
|
IrisData data = IrisData.openDatapackCompiler(packRoot);
|
||||||
try {
|
try {
|
||||||
|
if (aliasContext.retainedContract() != null) {
|
||||||
|
data.bindGenerationRegistryContract(aliasContext.retainedContract());
|
||||||
|
}
|
||||||
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
|
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
|
||||||
String[] possibleKeys = loader.getPossibleKeys();
|
String[] possibleKeys = loader.getPossibleKeys();
|
||||||
if (possibleKeys == null || possibleKeys.length == 0) {
|
if (possibleKeys == null || possibleKeys.length == 0) {
|
||||||
@@ -199,14 +211,16 @@ public final class IrisDatapackCompiler {
|
|||||||
}
|
}
|
||||||
collectRegistryRequirements(
|
collectRegistryRequirements(
|
||||||
requirements,
|
requirements,
|
||||||
installedBiomeKeys,
|
biomeTags,
|
||||||
dimension,
|
dimension,
|
||||||
fixer);
|
fixer,
|
||||||
|
aliasContext);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
data.close();
|
data.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addBiomeTagRequirements(requirements, biomeTags);
|
||||||
return Map.copyOf(requirements);
|
return Map.copyOf(requirements);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,23 +230,35 @@ public final class IrisDatapackCompiler {
|
|||||||
) throws IOException {
|
) throws IOException {
|
||||||
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||||
IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer");
|
IDataFixer requiredFixer = Objects.requireNonNull(fixer, "fixer");
|
||||||
|
IrisData data = Objects.requireNonNull(
|
||||||
|
requiredDimension.getLoader(),
|
||||||
|
"Iris dimension loader"
|
||||||
|
);
|
||||||
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
|
LinkedHashMap<String, String> requirements = new LinkedHashMap<>();
|
||||||
|
Map<String, Set<String>> biomeTags = new TreeMap<>();
|
||||||
|
BiomeAliasContext aliasContext = BiomeAliasContext.contentAddressedOnly();
|
||||||
collectRegistryRequirements(
|
collectRegistryRequirements(
|
||||||
requirements,
|
requirements,
|
||||||
new LinkedHashSet<>(),
|
biomeTags,
|
||||||
requiredDimension,
|
requiredDimension,
|
||||||
requiredFixer);
|
requiredFixer,
|
||||||
|
aliasContext);
|
||||||
collectDimensionStackRegistryRequirements(
|
collectDimensionStackRegistryRequirements(
|
||||||
requirements,
|
requirements,
|
||||||
|
biomeTags,
|
||||||
requiredDimension,
|
requiredDimension,
|
||||||
requiredFixer);
|
requiredFixer,
|
||||||
|
aliasContext);
|
||||||
|
addBiomeTagRequirements(requirements, biomeTags);
|
||||||
return Map.copyOf(requirements);
|
return Map.copyOf(requirements);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void collectDimensionStackRegistryRequirements(
|
private static void collectDimensionStackRegistryRequirements(
|
||||||
Map<String, String> requirements,
|
Map<String, String> requirements,
|
||||||
|
Map<String, Set<String>> biomeTags,
|
||||||
IrisDimension hostDimension,
|
IrisDimension hostDimension,
|
||||||
IDataFixer fixer
|
IDataFixer fixer,
|
||||||
|
BiomeAliasContext aliasContext
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
if (!hostDimension.hasDimensionStack()) {
|
if (!hostDimension.hasDimensionStack()) {
|
||||||
return;
|
return;
|
||||||
@@ -255,21 +281,31 @@ public final class IrisDatapackCompiler {
|
|||||||
}
|
}
|
||||||
collectRegistryRequirements(
|
collectRegistryRequirements(
|
||||||
requirements,
|
requirements,
|
||||||
new LinkedHashSet<>(),
|
biomeTags,
|
||||||
sourceDimension,
|
sourceDimension,
|
||||||
fixer);
|
fixer,
|
||||||
|
aliasContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void collectRegistryRequirements(
|
private static void collectRegistryRequirements(
|
||||||
Map<String, String> requirements,
|
Map<String, String> requirements,
|
||||||
Set<String> installedBiomeKeys,
|
Map<String, Set<String>> biomeTags,
|
||||||
IrisDimension dimension,
|
IrisDimension dimension,
|
||||||
IDataFixer fixer
|
IDataFixer fixer,
|
||||||
) {
|
BiomeAliasContext aliasContext
|
||||||
requirements.put(
|
) throws IOException {
|
||||||
"dimension_type/iris:" + dimension.getDimensionTypeKey(),
|
String dimensionTypeResourceKey = "iris:" + dimension.getDimensionTypeKey();
|
||||||
fingerprintContent(dimension.getDimensionType().toJson(fixer)));
|
String dimensionTypeSource = aliasContext.generatedSource(
|
||||||
|
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
|
||||||
|
dimensionTypeResourceKey,
|
||||||
|
dimension.getDimensionType().toJson(fixer),
|
||||||
|
fixer
|
||||||
|
);
|
||||||
|
putRegistryRequirement(
|
||||||
|
requirements,
|
||||||
|
"dimension_type/" + dimensionTypeResourceKey,
|
||||||
|
fingerprintContent(dimensionTypeSource));
|
||||||
|
|
||||||
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
|
ContentGate contentGate = dimension.getLoader() == null ? null : dimension.getLoader().getContentGate();
|
||||||
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
|
for (IrisBiome biome : dimension.getAllBiomes(dimension::getLoader)) {
|
||||||
@@ -281,13 +317,25 @@ public final class IrisDatapackCompiler {
|
|||||||
if (customBiome == null) {
|
if (customBiome == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String biomeKey = dimension.getCustomBiomeKey(customBiome.getId());
|
String json = customBiome.generateJson(fixer, contentGate);
|
||||||
if (!installedBiomeKeys.add(biomeKey)) {
|
String currentPhysicalKey = GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||||
continue;
|
dimension.getLoadKey(),
|
||||||
|
customBiome,
|
||||||
|
contentGate
|
||||||
|
);
|
||||||
|
String physicalKey = aliasContext.physicalResourceKey(
|
||||||
|
dimension,
|
||||||
|
biome,
|
||||||
|
customBiome,
|
||||||
|
currentPhysicalKey
|
||||||
|
);
|
||||||
|
LinkedHashSet<String> resourceKeys = new LinkedHashSet<>();
|
||||||
|
resourceKeys.add(physicalKey);
|
||||||
|
Collection<String> aliases = aliasContext.aliases(dimension, biome, customBiome, physicalKey);
|
||||||
|
if (!aliases.isEmpty() && physicalKey.equals(dimension.getLoader()
|
||||||
|
.customBiomeResourceKey(dimension, customBiome.getId()))) {
|
||||||
|
resourceKeys.addAll(aliases);
|
||||||
}
|
}
|
||||||
requirements.put(
|
|
||||||
"worldgen/biome/" + biomeKey,
|
|
||||||
fingerprintContent(customBiome.generateJson(fixer, contentGate)));
|
|
||||||
TreeSet<String> tags = new TreeSet<>();
|
TreeSet<String> tags = new TreeSet<>();
|
||||||
for (String tag : customBiome.getEffectiveTags(derivativeKey)) {
|
for (String tag : customBiome.getEffectiveTags(derivativeKey)) {
|
||||||
String normalizedTag = normalizeRegistryKey(tag);
|
String normalizedTag = normalizeRegistryKey(tag);
|
||||||
@@ -295,13 +343,48 @@ public final class IrisDatapackCompiler {
|
|||||||
tags.add(normalizedTag);
|
tags.add(normalizedTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requirements.put(
|
for (String resourceKey : resourceKeys) {
|
||||||
"worldgen/biome_tags/" + biomeKey,
|
String emittedJson = aliasContext.generatedSource(
|
||||||
fingerprintContent(String.join("\n", tags)));
|
GenerationRegistryContractFactory.BIOME_REGISTRY,
|
||||||
|
resourceKey,
|
||||||
|
json,
|
||||||
|
fixer
|
||||||
|
);
|
||||||
|
putRegistryRequirement(
|
||||||
|
requirements,
|
||||||
|
"worldgen/biome/" + resourceKey,
|
||||||
|
fingerprintContent(emittedJson)
|
||||||
|
);
|
||||||
|
biomeTags.computeIfAbsent(resourceKey, ignored -> new TreeSet<>()).addAll(tags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addBiomeTagRequirements(
|
||||||
|
Map<String, String> requirements,
|
||||||
|
Map<String, Set<String>> biomeTags
|
||||||
|
) throws IOException {
|
||||||
|
for (Map.Entry<String, Set<String>> entry : biomeTags.entrySet()) {
|
||||||
|
putRegistryRequirement(
|
||||||
|
requirements,
|
||||||
|
"worldgen/biome_tags/" + entry.getKey(),
|
||||||
|
fingerprintContent(String.join("\n", entry.getValue()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putRegistryRequirement(
|
||||||
|
Map<String, String> requirements,
|
||||||
|
String resourceKey,
|
||||||
|
String contentFingerprint
|
||||||
|
) throws IOException {
|
||||||
|
String previous = requirements.putIfAbsent(resourceKey, contentFingerprint);
|
||||||
|
if (previous != null && !previous.equals(contentFingerprint)) {
|
||||||
|
throw new IOException("Conflicting Iris datapack registry resource '" + resourceKey + "'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static String normalizeRegistryKey(String key) {
|
private static String normalizeRegistryKey(String key) {
|
||||||
if (key == null || key.isBlank()) {
|
if (key == null || key.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -359,8 +442,12 @@ public final class IrisDatapackCompiler {
|
|||||||
if (!hasDimensions(packRoot.toPath())) {
|
if (!hasDimensions(packRoot.toPath())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
BiomeAliasContext aliasContext = biomeAliasContext(packRoot.toPath());
|
||||||
IrisData data = IrisData.openDatapackCompiler(packRoot);
|
IrisData data = IrisData.openDatapackCompiler(packRoot);
|
||||||
try {
|
try {
|
||||||
|
if (aliasContext.retainedContract() != null) {
|
||||||
|
data.bindGenerationRegistryContract(aliasContext.retainedContract());
|
||||||
|
}
|
||||||
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
|
ResourceLoader<IrisDimension> loader = data.getDimensionLoader();
|
||||||
String[] possibleKeys = loader.getPossibleKeys();
|
String[] possibleKeys = loader.getPossibleKeys();
|
||||||
if (possibleKeys == null || possibleKeys.length == 0) {
|
if (possibleKeys == null || possibleKeys.length == 0) {
|
||||||
@@ -379,11 +466,22 @@ public final class IrisDatapackCompiler {
|
|||||||
.add(new DimensionCandidate(
|
.add(new DimensionCandidate(
|
||||||
dimension,
|
dimension,
|
||||||
packRoot.toPath().toAbsolutePath().normalize(),
|
packRoot.toPath().toAbsolutePath().normalize(),
|
||||||
dimension.getDimensionType().toJson(fixer)
|
aliasContext.generatedSource(
|
||||||
|
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
|
||||||
|
"iris:" + dimension.getDimensionTypeKey(),
|
||||||
|
dimension.getDimensionType().toJson(fixer),
|
||||||
|
fixer
|
||||||
|
)
|
||||||
));
|
));
|
||||||
KSet<String> seenBiomes = biomes.computeIfAbsent(dimension.getLoadKey(), ignored -> new KSet<>());
|
KSet<String> seenBiomes = biomes.computeIfAbsent(dimension.getLoadKey(), ignored -> new KSet<>());
|
||||||
dimension.installBiomes(fixer, dimension::getLoader, datapackRoots, seenBiomes);
|
dimension.installBiomes(
|
||||||
dimension.installDimensionType(fixer, datapackRoots);
|
fixer,
|
||||||
|
dimension::getLoader,
|
||||||
|
datapackRoots,
|
||||||
|
seenBiomes,
|
||||||
|
aliasContext
|
||||||
|
);
|
||||||
|
dimension.installDimensionType(fixer, datapackRoots, aliasContext);
|
||||||
installedDimensions++;
|
installedDimensions++;
|
||||||
dimensionCount++;
|
dimensionCount++;
|
||||||
}
|
}
|
||||||
@@ -501,6 +599,23 @@ public final class IrisDatapackCompiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void collectCanonicalWorldPackRoot(
|
||||||
|
Path worldRoot,
|
||||||
|
Map<Path, File> roots,
|
||||||
|
boolean validateWholePack
|
||||||
|
) throws IOException {
|
||||||
|
if (Files.isSymbolicLink(worldRoot)
|
||||||
|
|| !Files.isDirectory(worldRoot, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Path> candidates = new ArrayList<>();
|
||||||
|
collectWorldRootPackRoots(worldRoot, candidates);
|
||||||
|
candidates.sort(Comparator.comparing(Path::toString));
|
||||||
|
for (Path candidate : candidates) {
|
||||||
|
addPackRoot(candidate, roots, validateWholePack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void collectNamespaceWorldPackRoots(
|
private static void collectNamespaceWorldPackRoots(
|
||||||
Path namespace,
|
Path namespace,
|
||||||
List<Path> candidates
|
List<Path> candidates
|
||||||
@@ -510,18 +625,12 @@ public final class IrisDatapackCompiler {
|
|||||||
public FileVisitResult preVisitDirectory(
|
public FileVisitResult preVisitDirectory(
|
||||||
Path directory,
|
Path directory,
|
||||||
BasicFileAttributes attributes
|
BasicFileAttributes attributes
|
||||||
) {
|
) throws IOException {
|
||||||
if (!directory.equals(namespace)
|
if (!directory.equals(namespace)
|
||||||
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
|
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())) {
|
||||||
return FileVisitResult.SKIP_SUBTREE;
|
return FileVisitResult.SKIP_SUBTREE;
|
||||||
}
|
}
|
||||||
Path irisRoot = directory.resolve("iris");
|
if (collectWorldRootPackRoots(directory, candidates)) {
|
||||||
Path candidate = irisRoot.resolve("pack");
|
|
||||||
if (!Files.isSymbolicLink(irisRoot)
|
|
||||||
&& !Files.isSymbolicLink(candidate)
|
|
||||||
&& Files.isDirectory(candidate, LinkOption.NOFOLLOW_LINKS)
|
|
||||||
&& hasDimensions(candidate)) {
|
|
||||||
candidates.add(candidate);
|
|
||||||
return FileVisitResult.SKIP_SUBTREE;
|
return FileVisitResult.SKIP_SUBTREE;
|
||||||
}
|
}
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
@@ -529,6 +638,35 @@ public final class IrisDatapackCompiler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean collectWorldRootPackRoots(
|
||||||
|
Path worldRoot,
|
||||||
|
List<Path> candidates
|
||||||
|
) throws IOException {
|
||||||
|
Path irisRoot = worldRoot.resolve("iris");
|
||||||
|
Path generationRoot = irisRoot.resolve("generation");
|
||||||
|
if (!Files.isSymbolicLink(irisRoot)
|
||||||
|
&& (Files.exists(generationRoot, LinkOption.NOFOLLOW_LINKS)
|
||||||
|
|| Files.isSymbolicLink(generationRoot))) {
|
||||||
|
GenerationHistory history = GenerationHistory.open(worldRoot);
|
||||||
|
for (GenerationEpoch epoch : history.manifest().epochs()) {
|
||||||
|
Path retainedPack = history.paths().packRoot(epoch.epochId());
|
||||||
|
if (hasDimensions(retainedPack)) {
|
||||||
|
candidates.add(retainedPack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Path legacyPack = irisRoot.resolve("pack");
|
||||||
|
if (!Files.isSymbolicLink(irisRoot)
|
||||||
|
&& !Files.isSymbolicLink(legacyPack)
|
||||||
|
&& Files.isDirectory(legacyPack, LinkOption.NOFOLLOW_LINKS)
|
||||||
|
&& hasDimensions(legacyPack)) {
|
||||||
|
candidates.add(legacyPack);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Path> visibleDirectories(Path root) throws IOException {
|
private static List<Path> visibleDirectories(Path root) throws IOException {
|
||||||
try (Stream<Path> entries = Files.list(root)) {
|
try (Stream<Path> entries = Files.list(root)) {
|
||||||
return entries
|
return entries
|
||||||
@@ -562,6 +700,43 @@ public final class IrisDatapackCompiler {
|
|||||||
roots.putIfAbsent(identity, normalized.toFile());
|
roots.putIfAbsent(identity, normalized.toFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static BiomeAliasContext biomeAliasContext(Path packRoot) throws IOException {
|
||||||
|
Path normalizedRoot = packRoot.toAbsolutePath().normalize();
|
||||||
|
Path rootParent = normalizedRoot.getParent();
|
||||||
|
if (rootParent == null || !"pack".equals(normalizedRoot.getFileName().toString())) {
|
||||||
|
return BiomeAliasContext.contentAddressedOnly();
|
||||||
|
}
|
||||||
|
if ("iris".equals(rootParent.getFileName().toString())) {
|
||||||
|
return BiomeAliasContext.legacyPack();
|
||||||
|
}
|
||||||
|
|
||||||
|
Path epochsRoot = rootParent.getParent();
|
||||||
|
Path generationRoot = epochsRoot == null ? null : epochsRoot.getParent();
|
||||||
|
Path irisRoot = generationRoot == null ? null : generationRoot.getParent();
|
||||||
|
Path dimensionRoot = irisRoot == null ? null : irisRoot.getParent();
|
||||||
|
if (epochsRoot == null
|
||||||
|
|| generationRoot == null
|
||||||
|
|| irisRoot == null
|
||||||
|
|| dimensionRoot == null
|
||||||
|
|| !"epochs".equals(epochsRoot.getFileName().toString())
|
||||||
|
|| !"generation".equals(generationRoot.getFileName().toString())
|
||||||
|
|| !"iris".equals(irisRoot.getFileName().toString())) {
|
||||||
|
return BiomeAliasContext.contentAddressedOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
String epochId = rootParent.getFileName().toString();
|
||||||
|
GenerationHistory history = GenerationHistory.open(dimensionRoot);
|
||||||
|
GenerationEpoch epoch = history.manifest().epoch(epochId).orElseThrow(
|
||||||
|
() -> new IOException("Retained Iris pack is not referenced by its generation history: "
|
||||||
|
+ normalizedRoot)
|
||||||
|
);
|
||||||
|
Path expectedRoot = history.paths().packRoot(epochId).toAbsolutePath().normalize();
|
||||||
|
if (!expectedRoot.equals(normalizedRoot)) {
|
||||||
|
throw new IOException("Retained Iris pack does not match its generation history: " + normalizedRoot);
|
||||||
|
}
|
||||||
|
return BiomeAliasContext.retained(epoch.registryContract());
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean hasDimensions(Path root) {
|
private static boolean hasDimensions(Path root) {
|
||||||
Path dimensions = root.resolve("dimensions");
|
Path dimensions = root.resolve("dimensions");
|
||||||
if (Files.isSymbolicLink(dimensions)
|
if (Files.isSymbolicLink(dimensions)
|
||||||
@@ -704,6 +879,75 @@ public final class IrisDatapackCompiler {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record BiomeAliasContext(
|
||||||
|
boolean retainAllLegacyAliases,
|
||||||
|
GenerationRegistryContract retainedContract
|
||||||
|
) implements IrisCustomBiomeAliasResolver {
|
||||||
|
private static BiomeAliasContext contentAddressedOnly() {
|
||||||
|
return new BiomeAliasContext(false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BiomeAliasContext legacyPack() {
|
||||||
|
return new BiomeAliasContext(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BiomeAliasContext retained(GenerationRegistryContract registryContract) {
|
||||||
|
return new BiomeAliasContext(
|
||||||
|
false,
|
||||||
|
Objects.requireNonNull(registryContract, "registryContract")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String physicalResourceKey(
|
||||||
|
IrisDimension dimension,
|
||||||
|
IrisBiome biome,
|
||||||
|
IrisBiomeCustom customBiome,
|
||||||
|
String currentPhysicalResourceKey
|
||||||
|
) throws IOException {
|
||||||
|
if (retainedContract == null) {
|
||||||
|
return currentPhysicalResourceKey;
|
||||||
|
}
|
||||||
|
return dimension.getLoader().customBiomeResourceKey(dimension, customBiome);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> aliases(
|
||||||
|
IrisDimension dimension,
|
||||||
|
IrisBiome biome,
|
||||||
|
IrisBiomeCustom customBiome,
|
||||||
|
String physicalResourceKey
|
||||||
|
) {
|
||||||
|
String alias = dimension.getLoadKey().toLowerCase(Locale.ROOT)
|
||||||
|
+ ":" + customBiome.getId().toLowerCase(Locale.ROOT);
|
||||||
|
if (retainAllLegacyAliases) {
|
||||||
|
return List.of(alias);
|
||||||
|
}
|
||||||
|
if (retainedContract == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
PhysicalResourceKey key = new PhysicalResourceKey(
|
||||||
|
GenerationRegistryContractFactory.BIOME_REGISTRY,
|
||||||
|
alias
|
||||||
|
);
|
||||||
|
return retainedContract.definitions().containsKey(key) ? List.of(alias) : List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generatedSource(
|
||||||
|
String registryKey,
|
||||||
|
String resourceKey,
|
||||||
|
String currentSource,
|
||||||
|
IDataFixer fixer
|
||||||
|
) throws IOException {
|
||||||
|
if (retainedContract == null) {
|
||||||
|
return currentSource;
|
||||||
|
}
|
||||||
|
PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey);
|
||||||
|
return GenerationRegistryContractFactory.renderGeneratedSource(retainedContract, key, fixer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static final class DimensionHeight {
|
public static final class DimensionHeight {
|
||||||
private final IDataFixer fixer;
|
private final IDataFixer fixer;
|
||||||
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
|
private final AtomicIntegerArray[] dimensions = new AtomicIntegerArray[3];
|
||||||
|
|||||||
@@ -349,6 +349,11 @@ public class IrisSettings {
|
|||||||
public static class IrisSettingsGenerator {
|
public static class IrisSettingsGenerator {
|
||||||
public String defaultWorldType = "overworld";
|
public String defaultWorldType = "overworld";
|
||||||
public boolean preventLeafDecay = true;
|
public boolean preventLeafDecay = true;
|
||||||
|
public int generationTransitionWidthBlocks = 256;
|
||||||
|
|
||||||
|
public int getGenerationTransitionWidthBlocks() {
|
||||||
|
return Math.max(16, Math.min(generationTransitionWidthBlocks, 8_192));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.core;
|
package art.arcane.iris.core;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
@@ -404,10 +405,6 @@ public final class IrisWorldStorage {
|
|||||||
return Optional.ofNullable(NamespacedKey.fromString(namespace + ":" + key));
|
return Optional.ofNullable(NamespacedKey.fromString(namespace + ":" + key));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File packRoot(NamespacedKey key) {
|
|
||||||
return new File(dimensionRoot(key), "iris/pack");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static File requireFrozenDimensionRoot(
|
public static File requireFrozenDimensionRoot(
|
||||||
File worldContainer,
|
File worldContainer,
|
||||||
File levelRoot,
|
File levelRoot,
|
||||||
@@ -485,26 +482,28 @@ public final class IrisWorldStorage {
|
|||||||
return configuredRoot;
|
return configuredRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File requireFrozenPackRoot(File dimensionRoot) {
|
public static File requireActiveGenerationPackRoot(File dimensionRoot) {
|
||||||
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
|
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
|
||||||
.toPath()
|
.toPath()
|
||||||
.toAbsolutePath()
|
.toAbsolutePath()
|
||||||
.normalize();
|
.normalize();
|
||||||
Path irisRoot = root.resolve("iris");
|
try {
|
||||||
Path packRoot = irisRoot.resolve("pack");
|
return GenerationHistory.open(root).activePackRoot().toFile();
|
||||||
for (Path path : new Path[]{root, irisRoot, packRoot}) {
|
} catch (IOException failure) {
|
||||||
if (Files.isSymbolicLink(path)) {
|
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
|
||||||
throw new IllegalStateException("Frozen Iris pack path contains a symbolic link: " + path);
|
|
||||||
}
|
|
||||||
if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)
|
|
||||||
&& !Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
|
|
||||||
throw new IllegalStateException("Frozen Iris pack path is not a directory: " + path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!Files.isDirectory(packRoot, LinkOption.NOFOLLOW_LINKS)) {
|
}
|
||||||
throw new IllegalStateException("Frozen Iris pack snapshot is missing: " + packRoot);
|
|
||||||
|
public static File requireActiveGenerationPackRoot(File dimensionRoot, long expectedWorldSeed) {
|
||||||
|
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
|
||||||
|
.toPath()
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
try {
|
||||||
|
return GenerationHistory.open(root, expectedWorldSeed).activePackRoot().toFile();
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
|
||||||
}
|
}
|
||||||
return packRoot.toFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isExistingSafeDimensionRoot(Path storageRoot, Path dimensionRoot) {
|
private static boolean isExistingSafeDimensionRoot(Path storageRoot, Path dimensionRoot) {
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ public class IrisWorlds {
|
|||||||
configuredWorldName,
|
configuredWorldName,
|
||||||
worldKey
|
worldKey
|
||||||
);
|
);
|
||||||
return dimensionRoot.map(IrisWorldStorage::requireFrozenPackRoot);
|
return dimensionRoot.map(IrisWorldStorage::requireActiveGenerationPackRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void warnUnusableStorage(String worldIdentity, IllegalStateException failure) {
|
private static void warnUnusableStorage(String worldIdentity, IllegalStateException failure) {
|
||||||
@@ -376,7 +376,7 @@ public class IrisWorlds {
|
|||||||
}
|
}
|
||||||
IrisLogging.error("Iris world %s has unusable world storage and is excluded: %s",
|
IrisLogging.error("Iris world %s has unusable world storage and is excluded: %s",
|
||||||
worldIdentity, failure.getMessage());
|
worldIdentity, failure.getMessage());
|
||||||
IrisLogging.error("Restore its iris/pack snapshot, or drop the world with /iris remove world=%s delete=true.",
|
IrisLogging.error("Restore its iris/generation history, or drop the world with /iris remove world=%s delete=true.",
|
||||||
worldIdentity);
|
worldIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1121,7 +1121,7 @@ public class ServerConfigurator {
|
|||||||
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
for (IrisBiome i : dimension.getAllBiomes(dimension::getLoader)) {
|
||||||
if (i.isCustom()) {
|
if (i.isCustom()) {
|
||||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||||
keys.add(dimension.getCustomBiomeKey(j.getId()));
|
keys.add(dimension.getLoader().customBiomeResourceKey(dimension, j));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package art.arcane.iris.core.lifecycle;
|
|||||||
import art.arcane.iris.core.IrisWorldStorage;
|
import art.arcane.iris.core.IrisWorldStorage;
|
||||||
import art.arcane.iris.core.WorldSlotKey;
|
import art.arcane.iris.core.WorldSlotKey;
|
||||||
import art.arcane.iris.engine.IrisEngineMantle;
|
import art.arcane.iris.engine.IrisEngineMantle;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryPaths;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
import org.bukkit.configuration.InvalidConfigurationException;
|
import org.bukkit.configuration.InvalidConfigurationException;
|
||||||
@@ -229,8 +230,20 @@ public final class BukkitWorldConfiguration {
|
|||||||
}
|
}
|
||||||
Path resolvedRoot = dimensionRoot.toPath().toAbsolutePath().normalize();
|
Path resolvedRoot = dimensionRoot.toPath().toAbsolutePath().normalize();
|
||||||
try {
|
try {
|
||||||
IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
|
IrisWorldStorage.requireActiveGenerationPackRoot(dimensionRoot);
|
||||||
} catch (IllegalStateException unusablePack) {
|
} catch (IllegalStateException unusablePack) {
|
||||||
|
GenerationHistoryPaths historyPaths = GenerationHistoryPaths.forDimension(resolvedRoot);
|
||||||
|
boolean generationHistoryExists = Files.exists(
|
||||||
|
historyPaths.generationRoot(),
|
||||||
|
LinkOption.NOFOLLOW_LINKS
|
||||||
|
) || Files.isSymbolicLink(historyPaths.generationRoot());
|
||||||
|
boolean adoptableLegacyPack = !generationHistoryExists
|
||||||
|
&& Files.isDirectory(historyPaths.legacyPackRoot(), LinkOption.NOFOLLOW_LINKS)
|
||||||
|
&& !Files.isSymbolicLink(historyPaths.legacyPackRoot());
|
||||||
|
if (adoptableLegacyPack) {
|
||||||
|
return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot,
|
||||||
|
IrisWorldStorageState.PRESENT, null);
|
||||||
|
}
|
||||||
// Only a folder that owns something can be corrupted by a vanilla generator writing into it.
|
// Only a folder that owns something can be corrupted by a vanilla generator writing into it.
|
||||||
return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot,
|
return new IrisWorldStorageEntry(configuredName, slotKey, resolvedRoot,
|
||||||
hasWorldData(resolvedRoot)
|
hasWorldData(resolvedRoot)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
|||||||
|
|
||||||
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
import art.arcane.iris.core.ExactWorldSlotPathPolicy;
|
||||||
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
|
import art.arcane.iris.core.SnapshotDirectoryTreeDeleter;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -94,7 +95,7 @@ public final class WorldReplacementFilesystem {
|
|||||||
State state = inspect(requiredPaths);
|
State state = inspect(requiredPaths);
|
||||||
if (state.stagePresent()) {
|
if (state.stagePresent()) {
|
||||||
requireSafeTree(requiredPaths.stage(), "replacement stage");
|
requireSafeTree(requiredPaths.stage(), "replacement stage");
|
||||||
requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint);
|
requireFingerprint(activePackRoot(requiredPaths.stage()), expectedFingerprint);
|
||||||
if (originalTargetPresent) {
|
if (originalTargetPresent) {
|
||||||
Path retainedWorld = state.targetPresent()
|
Path retainedWorld = state.targetPresent()
|
||||||
? requiredPaths.target()
|
? requiredPaths.target()
|
||||||
@@ -118,7 +119,7 @@ public final class WorldReplacementFilesystem {
|
|||||||
throw new IOException("Replacement backup state does not match the original target state.");
|
throw new IOException("Replacement backup state does not match the original target state.");
|
||||||
}
|
}
|
||||||
requireSafeTree(requiredPaths.stage(), "replacement stage");
|
requireSafeTree(requiredPaths.stage(), "replacement stage");
|
||||||
requireFingerprint(requiredPaths.stage().resolve("iris/pack"), expectedFingerprint);
|
requireFingerprint(activePackRoot(requiredPaths.stage()), expectedFingerprint);
|
||||||
move(requiredPaths.stage(), requiredPaths.target());
|
move(requiredPaths.stage(), requiredPaths.target());
|
||||||
state = inspect(requiredPaths);
|
state = inspect(requiredPaths);
|
||||||
}
|
}
|
||||||
@@ -130,11 +131,11 @@ public final class WorldReplacementFilesystem {
|
|||||||
throw new IOException("Replacement publication lost or unexpectedly created its backup.");
|
throw new IOException("Replacement publication lost or unexpectedly created its backup.");
|
||||||
}
|
}
|
||||||
requireSafeTree(requiredPaths.target(), "replacement target");
|
requireSafeTree(requiredPaths.target(), "replacement target");
|
||||||
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
|
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
|
||||||
if (originalTargetPresent) {
|
if (originalTargetPresent) {
|
||||||
preservePaperWorldMetadata(requiredPaths.backup(), requiredPaths.target());
|
preservePaperWorldMetadata(requiredPaths.backup(), requiredPaths.target());
|
||||||
requireSafeTree(requiredPaths.target(), "replacement target");
|
requireSafeTree(requiredPaths.target(), "replacement target");
|
||||||
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
|
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ public final class WorldReplacementFilesystem {
|
|||||||
throw new IOException("Committed replacement storage does not contain one exact target directory.");
|
throw new IOException("Committed replacement storage does not contain one exact target directory.");
|
||||||
}
|
}
|
||||||
requireSafeTree(requiredPaths.target(), "replacement target");
|
requireSafeTree(requiredPaths.target(), "replacement target");
|
||||||
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
|
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void validatePublishedTarget(
|
public static void validatePublishedTarget(
|
||||||
@@ -240,7 +241,7 @@ public final class WorldReplacementFilesystem {
|
|||||||
throw new IOException("Published replacement storage does not match its retained-world contract.");
|
throw new IOException("Published replacement storage does not match its retained-world contract.");
|
||||||
}
|
}
|
||||||
requireSafeTree(requiredPaths.target(), "replacement target");
|
requireSafeTree(requiredPaths.target(), "replacement target");
|
||||||
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
|
requireFingerprint(activePackRoot(requiredPaths.target()), expectedFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String fingerprintPack(Path packRoot) throws IOException {
|
public static String fingerprintPack(Path packRoot) throws IOException {
|
||||||
@@ -279,6 +280,10 @@ public final class WorldReplacementFilesystem {
|
|||||||
return HexFormat.of().formatHex(digest.digest());
|
return HexFormat.of().formatHex(digest.digest());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Path activePackRoot(Path worldRoot) throws IOException {
|
||||||
|
return GenerationHistory.open(worldRoot).activePackRoot();
|
||||||
|
}
|
||||||
|
|
||||||
private static FingerprintEntry fingerprintEntry(Path root, Path path) {
|
private static FingerprintEntry fingerprintEntry(Path root, Path path) {
|
||||||
Path relative = root.relativize(path);
|
Path relative = root.relativize(path);
|
||||||
return new FingerprintEntry(path, relative, relative.toString());
|
return new FingerprintEntry(path, relative, relative.toString());
|
||||||
|
|||||||
@@ -39,9 +39,14 @@ import art.arcane.iris.engine.data.cache.AtomicCache;
|
|||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||||
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
|
import art.arcane.iris.engine.framework.structure.StructureGraphCatalog;
|
||||||
|
import art.arcane.iris.engine.history.GenerationPackFingerprint;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||||
import art.arcane.iris.core.structure.authoring.StructureRecoveryResult;
|
import art.arcane.iris.core.structure.authoring.StructureRecoveryResult;
|
||||||
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
|
import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||||
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
import art.arcane.iris.engine.object.IrisBlockData;
|
import art.arcane.iris.engine.object.IrisBlockData;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisEntity;
|
import art.arcane.iris.engine.object.IrisEntity;
|
||||||
@@ -74,6 +79,7 @@ import art.arcane.iris.util.common.scheduling.J;
|
|||||||
import art.arcane.iris.util.project.context.IrisContext;
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
@@ -87,7 +93,9 @@ import java.util.ArrayList;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -137,6 +145,17 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
private final transient List<Engine> engines = new ArrayList<>();
|
private final transient List<Engine> engines = new ArrayList<>();
|
||||||
|
private final transient Map<IrisDimension, Map<String, String>> dimensionCustomBiomeResourceKeys =
|
||||||
|
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||||
|
private transient volatile String generationPackFingerprint;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
private transient volatile GenerationRegistryContract generationRegistryContract;
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
@EqualsAndHashCode.Exclude
|
||||||
|
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
|
||||||
private transient volatile Engine soleEngine;
|
private transient volatile Engine soleEngine;
|
||||||
|
|
||||||
private IrisData(File dataFolder) {
|
private IrisData(File dataFolder) {
|
||||||
@@ -170,6 +189,90 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
return Optional.ofNullable(dataLoaders.get(dataFolder));
|
return Optional.ofNullable(dataLoaders.get(dataFolder));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String customBiomeResourceKey(IrisDimension dimension, IrisBiomeCustom customBiome) {
|
||||||
|
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||||
|
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
|
||||||
|
if (resolver != null) {
|
||||||
|
try {
|
||||||
|
return resolver.resolve(
|
||||||
|
requiredDimension.getLoadKey(),
|
||||||
|
requiredBiome
|
||||||
|
);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Immutable Iris epoch has no registry mapping for custom biome '"
|
||||||
|
+ requiredDimension.getLoadKey() + ":" + requiredBiome.getId() + "'.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GenerationRegistryContractFactory.customBiomeResourceKey(
|
||||||
|
requiredDimension.getLoadKey(),
|
||||||
|
requiredBiome,
|
||||||
|
getContentGate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String customBiomeResourceKey(IrisDimension dimension, String customBiomeId) {
|
||||||
|
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
|
||||||
|
String logicalKey = requiredDimension.getLoadKey().toLowerCase(Locale.ROOT)
|
||||||
|
+ ":" + Objects.requireNonNull(customBiomeId, "customBiomeId").toLowerCase(Locale.ROOT);
|
||||||
|
String physicalKey = customBiomeResourceKeys(requiredDimension).get(logicalKey);
|
||||||
|
if (physicalKey == null) {
|
||||||
|
throw new IllegalArgumentException("Unknown custom biome '" + logicalKey + "'.");
|
||||||
|
}
|
||||||
|
return physicalKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String physicalBiomeResourceKey(IrisDimension dimension, String resourceKey) {
|
||||||
|
if (resourceKey == null || resourceKey.isBlank()) {
|
||||||
|
return resourceKey;
|
||||||
|
}
|
||||||
|
String normalizedKey = resourceKey.trim().toLowerCase(Locale.ROOT);
|
||||||
|
if (!normalizedKey.contains(":")) {
|
||||||
|
normalizedKey = "minecraft:" + normalizedKey;
|
||||||
|
}
|
||||||
|
return customBiomeResourceKeys(Objects.requireNonNull(dimension, "dimension"))
|
||||||
|
.getOrDefault(normalizedKey, normalizedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generationPackFingerprint() {
|
||||||
|
String fingerprint = generationPackFingerprint;
|
||||||
|
if (fingerprint != null) {
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
fingerprint = generationPackFingerprint;
|
||||||
|
if (fingerprint != null) {
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fingerprint = GenerationPackFingerprint.compute(
|
||||||
|
dataFolder.toPath(),
|
||||||
|
GenerationPackFingerprint.CURRENT_VERSION
|
||||||
|
);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Unable to fingerprint Iris generation pack at "
|
||||||
|
+ dataFolder + ".", failure);
|
||||||
|
}
|
||||||
|
generationPackFingerprint = fingerprint;
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void bindGenerationRegistryContract(GenerationRegistryContract contract) {
|
||||||
|
GenerationRegistryContract required = Objects.requireNonNull(contract, "contract");
|
||||||
|
if (generationRegistryContract != null && !generationRegistryContract.equals(required)) {
|
||||||
|
throw new IllegalStateException("Iris data is already bound to another generation registry contract.");
|
||||||
|
}
|
||||||
|
generationRegistryContract = required;
|
||||||
|
customBiomeResourceResolver = new GenerationRegistryContractFactory.CustomBiomeResourceResolver(
|
||||||
|
required,
|
||||||
|
PlatformGenerationRegistry::contentAddressedCustomBiomeResourceKey
|
||||||
|
);
|
||||||
|
synchronized (dimensionCustomBiomeResourceKeys) {
|
||||||
|
dimensionCustomBiomeResourceKeys.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean invalidateLoadedStructureResources(File dataFolder) {
|
public static boolean invalidateLoadedStructureResources(File dataFolder) {
|
||||||
Path requested = dataFolderIdentity(Objects.requireNonNull(
|
Path requested = dataFolderIdentity(Objects.requireNonNull(
|
||||||
dataFolder,
|
dataFolder,
|
||||||
@@ -273,7 +376,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
try {
|
try {
|
||||||
if (nearest != null) {
|
if (nearest != null) {
|
||||||
T t = nearest.load(type, key, false);
|
T t = nearest.load(type, key, false);
|
||||||
if (t != null) {
|
if (t != null || nearest.generationRegistryContract != null) {
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,6 +619,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
StructureGraphCatalog.invalidate(this);
|
StructureGraphCatalog.invalidate(this);
|
||||||
IrisObjectScale.invalidate(this);
|
IrisObjectScale.invalidate(this);
|
||||||
contentGate = null;
|
contentGate = null;
|
||||||
|
generationPackFingerprint = null;
|
||||||
|
synchronized (dimensionCustomBiomeResourceKeys) {
|
||||||
|
dimensionCustomBiomeResourceKeys.clear();
|
||||||
|
}
|
||||||
compatReport.clear();
|
compatReport.clear();
|
||||||
closed = false;
|
closed = false;
|
||||||
possibleSnippets = new KMap<>();
|
possibleSnippets = new KMap<>();
|
||||||
@@ -831,4 +938,34 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
|||||||
}
|
}
|
||||||
IrisLogging.debug("Loaded Prefetch Cache to reduce generation disk use.");
|
IrisLogging.debug("Loaded Prefetch Cache to reduce generation disk use.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, String> customBiomeResourceKeys(IrisDimension dimension) {
|
||||||
|
synchronized (dimensionCustomBiomeResourceKeys) {
|
||||||
|
Map<String, String> cached = dimensionCustomBiomeResourceKeys.get(dimension);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
String namespace = dimension.getLoadKey().toLowerCase(Locale.ROOT);
|
||||||
|
Map<String, String> resolved = new LinkedHashMap<>();
|
||||||
|
List<IrisBiome> registryBiomes = new ArrayList<>(dimension.getAllBiomes(() -> this));
|
||||||
|
registryBiomes.addAll(dimension.getReachableBiomes(() -> this));
|
||||||
|
for (IrisBiome biome : registryBiomes) {
|
||||||
|
if (biome == null || !biome.isCustom()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (IrisBiomeCustom customBiome : biome.getCustomDerivitives()) {
|
||||||
|
if (customBiome == null || customBiome.getId() == null || customBiome.getId().isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String logicalKey = namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT);
|
||||||
|
String physicalKey = customBiomeResourceKey(dimension, customBiome);
|
||||||
|
resolved.merge(logicalKey, physicalKey,
|
||||||
|
(first, second) -> first.compareTo(second) <= 0 ? first : second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, String> immutable = Map.copyOf(resolved);
|
||||||
|
dimensionCustomBiomeResourceKeys.put(dimension, immutable);
|
||||||
|
return immutable;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -172,15 +172,13 @@ public final class BukkitRuntimeMessages {
|
|||||||
+ " (dimension blockFallbacks, block backup)."
|
+ " (dimension blockFallbacks, block backup)."
|
||||||
);
|
);
|
||||||
public static final LinesKey COMMAND_DEVELOPER_UPDATE_WORLD_WARNING = LinesKey.of(
|
public static final LinesKey COMMAND_DEVELOPER_UPDATE_WORLD_WARNING = LinesKey.of(
|
||||||
"iris.bukkit.runtime.commanddeveloper.update_world_warning",
|
"iris.bukkit.runtime.commanddeveloper.stage_world_generation_update_warning",
|
||||||
C.RED + "You should always make a backup before using this",
|
C.RED + "Back up the complete world before staging this update.",
|
||||||
C.YELLOW + "Issues caused by this can be, but are not limited to:",
|
C.YELLOW + "Existing chunks will not be regenerated or rewritten.",
|
||||||
C.YELLOW + " - Broken chunks (cut-offs) between old and new chunks (before & after the update)",
|
C.YELLOW + "New chunks use the staged pack after restart and blend across the frozen boundary.",
|
||||||
C.YELLOW + " - Regenerated chunks that do not fit in with the old chunks",
|
C.YELLOW + "The world seed, height, environment, and dimension type cannot change.",
|
||||||
C.YELLOW + " - Structures not spawning again when regenerating",
|
C.YELLOW + "Every backup must retain the complete iris/generation directory.",
|
||||||
C.YELLOW + " - Caves not lining up",
|
C.RED + "To stage the update and request a clean restart:",
|
||||||
C.YELLOW + " - Terrain layers not lining up",
|
|
||||||
C.RED + "Now that you are aware of the risks, and have made a back-up:",
|
|
||||||
C.RED + "/iris developer update-world {world} {pack} confirm=true"
|
C.RED + "/iris developer update-world {world} {pack} confirm=true"
|
||||||
);
|
);
|
||||||
public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW = TextKey.of(
|
public static final TextKey BULK_STRUCTURE_IMPORTER_IMPORTING_VANILLA_DATAPACK_STRUCTURES_MODE_INCLUDENONJIGSAW = TextKey.of(
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ public final class DirectorCommandMessages {
|
|||||||
"iris.director.commanddeveloper.param.center_chunk_z",
|
"iris.director.commanddeveloper.param.center_chunk_z",
|
||||||
"Center chunk Z"
|
"Center chunk Z"
|
||||||
);
|
);
|
||||||
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE = TextKey.of(
|
public static final TextKey COMMAND_DEVELOPER_DIRECTOR_STAGE_WORLD_GENERATION_UPDATE = TextKey.of(
|
||||||
"iris.director.commanddeveloper.director.update_pack_world_unsafe",
|
"iris.director.commanddeveloper.director.stage_world_generation_update",
|
||||||
"Update the pack of a world (UNSAFE!)"
|
"Stage a world generation update"
|
||||||
);
|
);
|
||||||
public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_UPDATE = TextKey.of(
|
public static final TextKey COMMAND_DEVELOPER_PARAM_WORLD_UPDATE = TextKey.of(
|
||||||
"iris.director.commanddeveloper.param.world_update",
|
"iris.director.commanddeveloper.param.world_update",
|
||||||
@@ -889,7 +889,7 @@ public final class DirectorCommandMessages {
|
|||||||
COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER,
|
COMMAND_DEVELOPER_PARAM_RADIUS_CHUNKS_AROUND_CENTER,
|
||||||
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X,
|
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_X,
|
||||||
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z,
|
COMMAND_DEVELOPER_PARAM_CENTER_CHUNK_Z,
|
||||||
COMMAND_DEVELOPER_DIRECTOR_UPDATE_PACK_WORLD_UNSAFE,
|
COMMAND_DEVELOPER_DIRECTOR_STAGE_WORLD_GENERATION_UPDATE,
|
||||||
COMMAND_DEVELOPER_PARAM_WORLD_UPDATE,
|
COMMAND_DEVELOPER_PARAM_WORLD_UPDATE,
|
||||||
COMMAND_DEVELOPER_PARAM_PACK_INSTALL_INTO_WORLD,
|
COMMAND_DEVELOPER_PARAM_PACK_INSTALL_INTO_WORLD,
|
||||||
COMMAND_DEVELOPER_PARAM_MAKE_SURE_MAKE_BACKUP_READ_WARNINGS_FIRST,
|
COMMAND_DEVELOPER_PARAM_MAKE_SURE_MAKE_BACKUP_READ_WARNINGS_FIRST,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import art.arcane.iris.engine.framework.Engine;
|
|||||||
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
import art.arcane.iris.engine.framework.NativeStructureVolume;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.spi.PlatformBlockState;
|
import art.arcane.iris.spi.PlatformBlockState;
|
||||||
|
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||||
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
|
||||||
import art.arcane.iris.util.project.hunk.Hunk;
|
import art.arcane.iris.util.project.hunk.Hunk;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
@@ -57,9 +58,14 @@ import java.awt.Color;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public interface INMSBinding {
|
public interface INMSBinding {
|
||||||
|
default PlatformGenerationRegistry generationRegistry() {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"The active NMS binding does not expose generation registry definitions."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
boolean hasTile(Material material);
|
boolean hasTile(Material material);
|
||||||
|
|
||||||
boolean hasTile(Location l);
|
boolean hasTile(Location l);
|
||||||
@@ -273,8 +279,18 @@ public interface INMSBinding {
|
|||||||
default void uninjectBukkit() {
|
default void uninjectBukkit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
default boolean awaitServerShutdownBoundary(long timeout, TimeUnit unit) {
|
default void deferPluginClassLoaderClose() {
|
||||||
return true;
|
}
|
||||||
|
|
||||||
|
default void releasePluginClassLoaderClose() {
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean isServerStopping() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default ServerShutdownBoundary createServerShutdownBoundary() {
|
||||||
|
return new ServerShutdownBoundary(() -> true, Thread.currentThread());
|
||||||
}
|
}
|
||||||
|
|
||||||
KMap<Material, List<BlockProperty>> getBlockProperties();
|
KMap<Material, List<BlockProperty>> getBlockProperties();
|
||||||
|
|||||||
@@ -7,33 +7,32 @@ import java.util.function.BooleanSupplier;
|
|||||||
public final class ServerShutdownBoundary {
|
public final class ServerShutdownBoundary {
|
||||||
private static final long MAX_JOIN_SLICE_MILLIS = 1000L;
|
private static final long MAX_JOIN_SLICE_MILLIS = 1000L;
|
||||||
|
|
||||||
private ServerShutdownBoundary() {
|
private final BooleanSupplier boundaryReached;
|
||||||
|
private final Thread serverThread;
|
||||||
|
|
||||||
|
public ServerShutdownBoundary(BooleanSupplier boundaryReached, Thread serverThread) {
|
||||||
|
this.boundaryReached = Objects.requireNonNull(boundaryReached, "Server shutdown boundary");
|
||||||
|
this.serverThread = Objects.requireNonNull(serverThread, "Server thread");
|
||||||
|
this.boundaryReached.getAsBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean await(
|
public boolean await(long timeout, TimeUnit unit) {
|
||||||
BooleanSupplier boundaryReached,
|
|
||||||
Thread serverThread,
|
|
||||||
long timeout,
|
|
||||||
TimeUnit unit
|
|
||||||
) {
|
|
||||||
BooleanSupplier reached = Objects.requireNonNull(boundaryReached, "Server shutdown boundary");
|
|
||||||
Thread activeServerThread = Objects.requireNonNull(serverThread, "Server thread");
|
|
||||||
TimeUnit activeUnit = Objects.requireNonNull(unit, "Server shutdown timeout unit");
|
TimeUnit activeUnit = Objects.requireNonNull(unit, "Server shutdown timeout unit");
|
||||||
if (reached.getAsBoolean()) {
|
if (boundaryReached.getAsBoolean()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (activeServerThread == Thread.currentThread()) {
|
if (serverThread == Thread.currentThread()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
long timeoutNanos = Math.max(0L, activeUnit.toNanos(timeout));
|
long timeoutNanos = Math.max(0L, activeUnit.toNanos(timeout));
|
||||||
long started = System.nanoTime();
|
long started = System.nanoTime();
|
||||||
boolean interrupted = false;
|
boolean interrupted = false;
|
||||||
while (!reached.getAsBoolean()) {
|
while (!boundaryReached.getAsBoolean()) {
|
||||||
long remaining = timeoutNanos - (System.nanoTime() - started);
|
long remaining = timeoutNanos - (System.nanoTime() - started);
|
||||||
if (remaining <= 0L || !activeServerThread.isAlive()) {
|
if (remaining <= 0L || !serverThread.isAlive()) {
|
||||||
restoreInterrupt(interrupted);
|
restoreInterrupt(interrupted);
|
||||||
return reached.getAsBoolean();
|
return boundaryReached.getAsBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
long joinMillis = Math.max(
|
long joinMillis = Math.max(
|
||||||
@@ -41,7 +40,7 @@ public final class ServerShutdownBoundary {
|
|||||||
Math.min(MAX_JOIN_SLICE_MILLIS, TimeUnit.NANOSECONDS.toMillis(remaining))
|
Math.min(MAX_JOIN_SLICE_MILLIS, TimeUnit.NANOSECONDS.toMillis(remaining))
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
activeServerThread.join(joinMillis);
|
serverThread.join(joinMillis);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
interrupted = true;
|
interrupted = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.apache.logging.log4j.Marker;
|
|||||||
import org.apache.logging.log4j.core.Filter;
|
import org.apache.logging.log4j.core.Filter;
|
||||||
import org.apache.logging.log4j.core.LogEvent;
|
import org.apache.logging.log4j.core.LogEvent;
|
||||||
import org.apache.logging.log4j.core.Logger;
|
import org.apache.logging.log4j.core.Logger;
|
||||||
|
import org.apache.logging.log4j.core.config.LoggerConfig;
|
||||||
import org.apache.logging.log4j.message.Message;
|
import org.apache.logging.log4j.message.Message;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -21,14 +22,15 @@ public class LogFilterSVC implements IrisService, Filter {
|
|||||||
// so a mutable static here was both a CME hazard and grew by three entries per enable.
|
// so a mutable static here was both a CME hazard and grew by three entries per enable.
|
||||||
private static final List<String> FILTERS = List.of(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID);
|
private static final List<String> FILTERS = List.of(HEIGHTMAP_MISMATCH, RAID_PERSISTENCE, DUPLICATE_ENTITY_UUID);
|
||||||
|
|
||||||
private boolean installed = false;
|
private LoggerConfig installedLogger;
|
||||||
|
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
if (installed) {
|
if (installedLogger != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
((Logger) LogManager.getRootLogger()).addFilter(this);
|
LoggerConfig logger = ((Logger) LogManager.getRootLogger()).get();
|
||||||
installed = true;
|
logger.addFilter(this);
|
||||||
|
installedLogger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
@@ -41,11 +43,12 @@ public class LogFilterSVC implements IrisService, Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
if (!installed) {
|
LoggerConfig logger = installedLogger;
|
||||||
|
if (logger == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
((Logger) LogManager.getRootLogger()).get().removeFilter(this);
|
logger.removeFilter(this);
|
||||||
installed = false;
|
installedLogger = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isStarted() {
|
public boolean isStarted() {
|
||||||
|
|||||||
@@ -69,10 +69,27 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
|
|||||||
public void dereference() {
|
public void dereference() {
|
||||||
IrisData.dereference();
|
IrisData.dereference();
|
||||||
threads.removeIf((i) -> !i.isAlive());
|
threads.removeIf((i) -> !i.isAlive());
|
||||||
services.removeIf(ExecutorService::isShutdown);
|
services.removeIf(ExecutorService::isTerminated);
|
||||||
updateCaches();
|
updateCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasActiveResources() {
|
||||||
|
if (dereferencer != null && dereferencer.isAlive()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (Thread thread : threads) {
|
||||||
|
if (thread.isAlive()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (ExecutorService executor : services) {
|
||||||
|
if (!executor.isTerminated()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package art.arcane.iris.core.service;
|
package art.arcane.iris.core.service;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.core.IrisSettings;
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.IrisStartupValidation;
|
import art.arcane.iris.core.IrisStartupValidation;
|
||||||
@@ -46,6 +47,14 @@ import art.arcane.iris.core.runtime.WorldDeletionQueue;
|
|||||||
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||||
|
import art.arcane.iris.engine.history.GenerationActivation;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryPaths;
|
||||||
|
import art.arcane.iris.engine.history.GenerationPackFingerprint;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
@@ -166,18 +175,197 @@ public class StudioSVC implements IrisService {
|
|||||||
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
|
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimension installIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) {
|
public IrisDimension installIntoWorld(
|
||||||
return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), false);
|
VolmitSender sender,
|
||||||
|
IrisDimension dimension,
|
||||||
|
File dimensionRoot,
|
||||||
|
long worldSeed
|
||||||
|
) {
|
||||||
|
return publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimension replaceIntoWorld(VolmitSender sender, IrisDimension dimension, File folder) {
|
public IrisDimension replaceIntoWorld(
|
||||||
return installIntoDirectory(sender, dimension, new File(folder, "iris/pack"), true);
|
VolmitSender sender,
|
||||||
|
IrisDimension dimension,
|
||||||
|
File dimensionRoot,
|
||||||
|
long worldSeed
|
||||||
|
) {
|
||||||
|
return publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IrisDimension installIntoTransientWorld(
|
||||||
|
VolmitSender sender,
|
||||||
|
IrisDimension dimension,
|
||||||
|
File dimensionRoot
|
||||||
|
) {
|
||||||
|
return installIntoDirectory(sender, dimension, new File(dimensionRoot, "iris/pack"), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IrisDimension replaceIntoPackDirectory(VolmitSender sender, IrisDimension dimension, File folder) {
|
public IrisDimension replaceIntoPackDirectory(VolmitSender sender, IrisDimension dimension, File folder) {
|
||||||
return installIntoDirectory(sender, dimension, folder, true);
|
return installIntoDirectory(sender, dimension, folder, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IrisDimension publishGenerationHistory(
|
||||||
|
VolmitSender sender,
|
||||||
|
IrisDimension dimension,
|
||||||
|
File dimensionRoot,
|
||||||
|
long worldSeed,
|
||||||
|
boolean stageUpdate
|
||||||
|
) {
|
||||||
|
if (J.isPrimaryThread()) {
|
||||||
|
sender.sendMessage(IrisLanguage.text(
|
||||||
|
BukkitRuntimeMessages.STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD
|
||||||
|
));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String dimensionKey = dimension.getLoadKey();
|
||||||
|
try {
|
||||||
|
Path source = resolveSafePackSource(dimension.getLoader().getDataFolder());
|
||||||
|
GenerationCandidate candidate = captureGenerationCandidate(source, dimensionKey);
|
||||||
|
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
|
||||||
|
.toPath()
|
||||||
|
.toAbsolutePath()
|
||||||
|
.normalize();
|
||||||
|
if (!stageUpdate && !Files.exists(root, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
Path parent = Objects.requireNonNull(root.getParent(), "dimensionRoot parent");
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
Files.createDirectory(root);
|
||||||
|
}
|
||||||
|
GenerationHistory history;
|
||||||
|
long previousActivationId = 0L;
|
||||||
|
boolean restartRequired = false;
|
||||||
|
if (stageUpdate) {
|
||||||
|
history = openOrAdoptGenerationHistory(root, dimensionKey, worldSeed);
|
||||||
|
previousActivationId = history.activeActivation().activationId();
|
||||||
|
GenerationActivation activation = history.stageUpdate(
|
||||||
|
source,
|
||||||
|
candidate.packFingerprint(),
|
||||||
|
candidate.dimensionContract(),
|
||||||
|
candidate.registryContract(),
|
||||||
|
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
||||||
|
);
|
||||||
|
restartRequired = activation.activationId() != previousActivationId;
|
||||||
|
} else {
|
||||||
|
history = GenerationHistory.create(
|
||||||
|
root,
|
||||||
|
source,
|
||||||
|
candidate.packFingerprint(),
|
||||||
|
worldSeed,
|
||||||
|
candidate.dimensionContract(),
|
||||||
|
candidate.registryContract()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Path installedPack = stageUpdate && history.pendingActivation().isPresent()
|
||||||
|
? history.packRoot(history.pendingActivation().orElseThrow().activationId())
|
||||||
|
: history.activePackRoot();
|
||||||
|
IrisDimension installed = loadInstalledDimension(installedPack, dimensionKey);
|
||||||
|
if (restartRequired) {
|
||||||
|
ServerConfigurator.restart("An Iris generation epoch update is pending activation.");
|
||||||
|
}
|
||||||
|
return installed;
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
IrisLogging.reportError("Failed to publish generation history for dimension '"
|
||||||
|
+ dimensionKey + "' into " + dimensionRoot.getPath(), failure);
|
||||||
|
sender.sendMessage(IrisLanguage.text(
|
||||||
|
BukkitRuntimeMessages.STUDIO_S_V_C_PACK_INSTALL_FAILED,
|
||||||
|
MessageArgument.untrusted("dimension", dimensionKey),
|
||||||
|
MessageArgument.untrusted("error", errorDetail(failure))
|
||||||
|
));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationHistory openOrAdoptGenerationHistory(
|
||||||
|
Path dimensionRoot,
|
||||||
|
String dimensionKey,
|
||||||
|
long worldSeed
|
||||||
|
) throws IOException {
|
||||||
|
Optional<GenerationHistory> current = GenerationHistory.openIfPresent(dimensionRoot, worldSeed);
|
||||||
|
if (current.isPresent()) {
|
||||||
|
return current.get();
|
||||||
|
}
|
||||||
|
Path legacyPack = GenerationHistoryPaths.forDimension(dimensionRoot).legacyPackRoot();
|
||||||
|
PackValidationResult validation = validatePublishedPack(legacyPack);
|
||||||
|
if (!validation.isLoadable()) {
|
||||||
|
throw new BrokenPackException(legacyPack.toString(), validation.getBlockingErrors());
|
||||||
|
}
|
||||||
|
GenerationCandidate legacy = captureGenerationCandidate(
|
||||||
|
legacyPack,
|
||||||
|
dimensionKey,
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.RETAIN_LEGACY_ALIASES
|
||||||
|
);
|
||||||
|
return GenerationHistory.adoptLegacyPack(
|
||||||
|
dimensionRoot,
|
||||||
|
legacy.packFingerprint(),
|
||||||
|
worldSeed,
|
||||||
|
legacy.dimensionContract(),
|
||||||
|
legacy.registryContract()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationCandidate captureGenerationCandidate(Path packRoot, String dimensionKey)
|
||||||
|
throws IOException {
|
||||||
|
return captureGenerationCandidate(
|
||||||
|
packRoot,
|
||||||
|
dimensionKey,
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeAliasPolicy.CONTENT_ADDRESSED_ONLY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerationCandidate captureGenerationCandidate(
|
||||||
|
Path packRoot,
|
||||||
|
String dimensionKey,
|
||||||
|
GenerationRegistryContractFactory.CustomBiomeAliasPolicy aliasPolicy
|
||||||
|
) throws IOException {
|
||||||
|
String fingerprint = GenerationPackFingerprint.compute(
|
||||||
|
packRoot,
|
||||||
|
GenerationPackFingerprint.CURRENT_VERSION
|
||||||
|
);
|
||||||
|
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
|
||||||
|
try {
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||||
|
if (dimension == null) {
|
||||||
|
throw new IOException("Generation pack does not contain a loadable dimension '"
|
||||||
|
+ dimensionKey + "'.");
|
||||||
|
}
|
||||||
|
String dimensionTypeKey = IrisPlatforms.get()
|
||||||
|
.registries()
|
||||||
|
.generationRegistry()
|
||||||
|
.dimensionTypeResourceKey(
|
||||||
|
data.getDataFolder().getName(),
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
dimension.getDimensionTypeKey()
|
||||||
|
);
|
||||||
|
GenerationEpoch.DimensionContract dimensionContract = GenerationEpochContractFactory.create(
|
||||||
|
dimension,
|
||||||
|
dimension.getLoadKey(),
|
||||||
|
dimensionTypeKey
|
||||||
|
);
|
||||||
|
GenerationRegistryContract registryContract = GenerationRegistryContractFactory.create(
|
||||||
|
data,
|
||||||
|
dimension,
|
||||||
|
fingerprint,
|
||||||
|
aliasPolicy
|
||||||
|
);
|
||||||
|
return new GenerationCandidate(fingerprint, dimensionContract, registryContract);
|
||||||
|
} finally {
|
||||||
|
data.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisDimension loadInstalledDimension(Path packRoot, String dimensionKey) throws IOException {
|
||||||
|
PackValidationResult validation = validatePublishedPack(packRoot);
|
||||||
|
if (!validation.isLoadable()) {
|
||||||
|
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
|
||||||
|
}
|
||||||
|
IrisDimension installed = IrisData.get(packRoot.toFile()).getDimensionLoader().load(dimensionKey, false);
|
||||||
|
if (installed == null) {
|
||||||
|
throw new IOException("Immutable generation pack does not contain dimension '"
|
||||||
|
+ dimensionKey + "'.");
|
||||||
|
}
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
private IrisDimension installIntoDirectory(
|
private IrisDimension installIntoDirectory(
|
||||||
VolmitSender sender,
|
VolmitSender sender,
|
||||||
IrisDimension dimension,
|
IrisDimension dimension,
|
||||||
@@ -1534,6 +1722,13 @@ public class StudioSVC implements IrisService {
|
|||||||
RESTART
|
RESTART
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record GenerationCandidate(
|
||||||
|
String packFingerprint,
|
||||||
|
GenerationEpoch.DimensionContract dimensionContract,
|
||||||
|
GenerationRegistryContract registryContract
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public IrisProject getActiveProject() {
|
public IrisProject getActiveProject() {
|
||||||
return activeProject;
|
return activeProject;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,8 +263,10 @@ public class IrisCreator {
|
|||||||
IrisDimension installedDimension = resolvedDimension;
|
IrisDimension installedDimension = resolvedDimension;
|
||||||
if (!studio() || benchmark) {
|
if (!studio() || benchmark) {
|
||||||
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
|
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
|
||||||
installedDimension = IrisServices.get(StudioSVC.class)
|
StudioSVC studioService = IrisServices.get(StudioSVC.class);
|
||||||
.installIntoWorld(sender, resolvedDimension, dimensionRoot);
|
installedDimension = benchmark
|
||||||
|
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
|
||||||
|
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
|
||||||
if (installedDimension == null) {
|
if (installedDimension == null) {
|
||||||
throw new IrisException("Failed to install dimension pack for " + dimension());
|
throw new IrisException("Failed to install dimension pack for " + dimension());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import art.arcane.iris.core.IrisSettings;
|
|||||||
import art.arcane.iris.core.IrisWorldStorage;
|
import art.arcane.iris.core.IrisWorldStorage;
|
||||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
|
import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
|
||||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||||
@@ -57,7 +58,6 @@ import org.jetbrains.annotations.ApiStatus;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -74,7 +74,6 @@ import art.arcane.volmlib.util.localization.MessageArgument;
|
|||||||
public class IrisToolbelt {
|
public class IrisToolbelt {
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
public static Map<String, Boolean> toolbeltConfiguration = new HashMap<>();
|
public static Map<String, Boolean> toolbeltConfiguration = new HashMap<>();
|
||||||
private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an installed dimension or returns null.
|
* Finds an installed dimension or returns null.
|
||||||
@@ -582,26 +581,7 @@ public class IrisToolbelt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isServerStopping() {
|
public static boolean isServerStopping() {
|
||||||
Method method = BUKKIT_IS_STOPPING_METHOD;
|
return INMS.isBound() && INMS.get().isServerStopping();
|
||||||
if (method != null) {
|
|
||||||
try {
|
|
||||||
Object value = method.invoke(null);
|
|
||||||
if (value instanceof Boolean) {
|
|
||||||
return (Boolean) value;
|
|
||||||
}
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return !BukkitPlatform.hasPlugin() || !BukkitPlatform.plugin().isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Method resolveBukkitIsStoppingMethod() {
|
|
||||||
try {
|
|
||||||
return Bukkit.class.getMethod("isStopping");
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void beginWorldMaintenance(World world, String reason) {
|
public static void beginWorldMaintenance(World world, String reason) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import art.arcane.iris.core.WorldCreatorCompat;
|
|||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
|
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
@@ -31,6 +32,7 @@ import org.bukkit.WorldCreator;
|
|||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
public class IrisWorldCreator {
|
public class IrisWorldCreator {
|
||||||
private String name;
|
private String name;
|
||||||
@@ -100,15 +102,42 @@ public class IrisWorldCreator {
|
|||||||
.seed(seed)
|
.seed(seed)
|
||||||
.worldFolder(worldFolder)
|
.worldFolder(worldFolder)
|
||||||
.build();
|
.build();
|
||||||
ChunkGenerator g = new BukkitChunkGenerator(w, studio, studio
|
GenerationHistory generationHistory = persistent
|
||||||
? dim.getLoader().getDataFolder() :
|
? requireGenerationHistory(w.worldFolder(), seed)
|
||||||
new File(w.worldFolder(), "iris/pack"), dimensionName);
|
: null;
|
||||||
|
File packRoot = generationHistory != null
|
||||||
|
? requireActivePack(generationHistory)
|
||||||
|
: studio
|
||||||
|
? dim.getLoader().getDataFolder()
|
||||||
|
: new File(w.worldFolder(), "iris/pack");
|
||||||
|
ChunkGenerator g = new BukkitChunkGenerator(
|
||||||
|
w,
|
||||||
|
studio,
|
||||||
|
packRoot,
|
||||||
|
dimensionName,
|
||||||
|
generationHistory);
|
||||||
|
|
||||||
return creator.environment(environment)
|
return creator.environment(environment)
|
||||||
.generateStructures(true)
|
.generateStructures(true)
|
||||||
.generator(g).seed(seed);
|
.generator(g).seed(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GenerationHistory requireGenerationHistory(File dimensionRoot, long seed) {
|
||||||
|
try {
|
||||||
|
return GenerationHistory.open(dimensionRoot.toPath(), seed);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris generation history is unusable at " + dimensionRoot + ".", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File requireActivePack(GenerationHistory history) {
|
||||||
|
try {
|
||||||
|
return history.activePackRoot().toFile();
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalStateException("Iris active generation pack is unusable.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private World.Environment findEnvironment() {
|
private World.Environment findEnvironment() {
|
||||||
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
||||||
return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment());
|
return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment());
|
||||||
|
|||||||
@@ -121,10 +121,10 @@ public final class DimensionTerrainContext implements DataProvider {
|
|||||||
engine.getDimension(),
|
engine.getDimension(),
|
||||||
engine.getData(),
|
engine.getData(),
|
||||||
engine.getHeight(),
|
engine.getHeight(),
|
||||||
natural ? complex.getNaturalHeightStream() : complex.getHeightStream(),
|
natural ? complex.getUnblendedNaturalHeightStream() : complex.getHeightStream(),
|
||||||
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
|
natural ? complex.getNaturalSlopeStream() : complex.getSlopeStream(),
|
||||||
fluidHeightStream,
|
fluidHeightStream,
|
||||||
natural ? complex.getNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
|
natural ? complex.getUnblendedNaturalTrueBiomeStream() : complex.getTrueBiomeStream(),
|
||||||
complex.getRegionStream(),
|
complex.getRegionStream(),
|
||||||
complex.getRockStream(),
|
complex.getRockStream(),
|
||||||
fluidBlockSampler,
|
fluidBlockSampler,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ package art.arcane.iris.engine;
|
|||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
import art.arcane.iris.core.protocol.IrisProtocolServer;
|
||||||
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
|
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
|
||||||
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
|
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
|
||||||
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
@@ -53,32 +53,40 @@ final class EngineHotloader {
|
|||||||
void hotloadComplex() {
|
void hotloadComplex() {
|
||||||
synchronized (engine.lifecycleLock) {
|
synchronized (engine.lifecycleLock) {
|
||||||
engine.requireRunning("rebuild the biome complex");
|
engine.requireRunning("rebuild the biome complex");
|
||||||
|
requireMutableGenerationRuntime("rebuild the biome complex");
|
||||||
engine.awaitNativeStructureBootstrap("complex hotload");
|
engine.awaitNativeStructureBootstrap("complex hotload");
|
||||||
engine.lifecycleState = LifecycleState.HOTLOADING;
|
engine.lifecycleState = LifecycleState.HOTLOADING;
|
||||||
EngineRuntime previous = engine.runtime;
|
EngineRuntime previous = engine.runtime;
|
||||||
|
GenerationRuntime previousGeneration = previous.generation();
|
||||||
IrisComplex nextComplex = null;
|
IrisComplex nextComplex = null;
|
||||||
try {
|
try {
|
||||||
engine.sealForTransition("complex hotload", false);
|
engine.sealForTransition("complex hotload", false);
|
||||||
engine.prepareRuntimeHotload();
|
engine.prepareRuntimeHotload();
|
||||||
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), previous.target());
|
RuntimeAssembly assembly = new RuntimeAssembly(
|
||||||
|
RuntimeAssembly.nextRuntimeId(),
|
||||||
|
previousGeneration.target(),
|
||||||
|
previousGeneration.mantleStorageDirectory(),
|
||||||
|
previousGeneration.runtimeKernel(),
|
||||||
|
previousGeneration.transitionPlan());
|
||||||
engine.runtimeAssembly.set(assembly);
|
engine.runtimeAssembly.set(assembly);
|
||||||
EngineRuntime next;
|
EngineRuntime next;
|
||||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||||
assembly.complex = new IrisComplex(engine);
|
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
|
||||||
nextComplex = assembly.complex;
|
nextComplex = assembly.complex;
|
||||||
assembly.dimensionStackContext = engine.runtimeBuilder.buildDimensionStackContext();
|
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
|
||||||
assembly.upperContext = engine.runtimeBuilder.buildUpperContext();
|
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
|
||||||
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
|
BiomeMaxes biomeMaxes = engine.runtimeBuilder.computeBiomeMaxes();
|
||||||
next = previous.withComplex(
|
GenerationRuntime nextGeneration = previousGeneration.withComplex(
|
||||||
assembly.cacheId,
|
assembly.cacheId,
|
||||||
assembly.complex,
|
assembly.complex,
|
||||||
assembly.upperContext,
|
assembly.upperContext,
|
||||||
assembly.dimensionStackContext,
|
assembly.dimensionStackContext,
|
||||||
biomeMaxes);
|
biomeMaxes);
|
||||||
|
next = previous.withGeneration(nextGeneration);
|
||||||
} finally {
|
} finally {
|
||||||
engine.runtimeAssembly.remove();
|
engine.runtimeAssembly.remove();
|
||||||
}
|
}
|
||||||
Throwable retirementFailure = runCleanup(null, previous.complex()::close);
|
Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close);
|
||||||
if (retirementFailure != null) {
|
if (retirementFailure != null) {
|
||||||
retirementFailure = runCleanup(retirementFailure, nextComplex::close);
|
retirementFailure = runCleanup(retirementFailure, nextComplex::close);
|
||||||
nextComplex = null;
|
nextComplex = null;
|
||||||
@@ -91,7 +99,7 @@ final class EngineHotloader {
|
|||||||
engine.getClosing().set(false);
|
engine.getClosing().set(false);
|
||||||
engine.backgroundTasks.openBackgroundTaskAdmission();
|
engine.backgroundTasks.openBackgroundTaskAdmission();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
if (nextComplex != null && nextComplex != previous.complex()) {
|
if (nextComplex != null && nextComplex != previousGeneration.complex()) {
|
||||||
Throwable cleanupFailure = runCleanup(null, nextComplex::close);
|
Throwable cleanupFailure = runCleanup(null, nextComplex::close);
|
||||||
if (cleanupFailure != null) {
|
if (cleanupFailure != null) {
|
||||||
e.addSuppressed(cleanupFailure);
|
e.addSuppressed(cleanupFailure);
|
||||||
@@ -108,6 +116,7 @@ final class EngineHotloader {
|
|||||||
void hotloadSilently() {
|
void hotloadSilently() {
|
||||||
synchronized (engine.lifecycleLock) {
|
synchronized (engine.lifecycleLock) {
|
||||||
engine.requireRunning("hotload");
|
engine.requireRunning("hotload");
|
||||||
|
requireMutableGenerationRuntime("hotload");
|
||||||
engine.awaitNativeStructureBootstrap("hotload");
|
engine.awaitNativeStructureBootstrap("hotload");
|
||||||
engine.lifecycleState = LifecycleState.HOTLOADING;
|
engine.lifecycleState = LifecycleState.HOTLOADING;
|
||||||
EngineRuntime previousRuntime = engine.runtime;
|
EngineRuntime previousRuntime = engine.runtime;
|
||||||
@@ -183,6 +192,13 @@ final class EngineHotloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requireMutableGenerationRuntime(String operation) {
|
||||||
|
if (engine.getGenerationHistoryRuntimeRouter().isPresent()) {
|
||||||
|
throw new IllegalStateException("Cannot " + operation
|
||||||
|
+ " while immutable Iris generation history is attached; stage a world update and restart instead.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Never throws. It is called from the hotload failure handler, where a frame-cap
|
* Never throws. It is called from the hotload failure handler, where a frame-cap
|
||||||
* {@code IllegalStateException} out of the codec (a long pack key or exception message overruns
|
* {@code IllegalStateException} out of the codec (a long pack key or exception message overruns
|
||||||
|
|||||||
@@ -19,48 +19,26 @@
|
|||||||
package art.arcane.iris.engine;
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
import art.arcane.iris.engine.framework.EngineEffects;
|
import art.arcane.iris.engine.framework.EngineEffects;
|
||||||
import art.arcane.iris.engine.framework.EngineMode;
|
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
|
||||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable snapshot of everything an {@link IrisEngine} publishes as a single unit.
|
* Immutable snapshot of everything an {@link IrisEngine} publishes as a single unit.
|
||||||
* Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}.
|
* Instances are built by {@link EngineRuntimeBuilder} and retired by {@link EngineShutdownSequence}.
|
||||||
*/
|
*/
|
||||||
record EngineRuntime(
|
record EngineRuntime(
|
||||||
int cacheId,
|
GenerationRuntime generation,
|
||||||
EngineTarget target,
|
|
||||||
IrisComplex complex,
|
|
||||||
UpperDimensionContext upperContext,
|
|
||||||
DimensionStackContext dimensionStackContext,
|
|
||||||
EngineEffects effects,
|
EngineEffects effects,
|
||||||
EngineMode mode,
|
EngineWorldManager worldManager
|
||||||
EngineWorldManager worldManager,
|
|
||||||
CompletableFuture<Long> hash32,
|
|
||||||
BiomeMaxes biomeMaxes
|
|
||||||
) {
|
) {
|
||||||
EngineRuntime withComplex(
|
EngineRuntime {
|
||||||
int nextCacheId,
|
Objects.requireNonNull(generation, "generation runtime");
|
||||||
IrisComplex nextComplex,
|
Objects.requireNonNull(effects, "engine effects");
|
||||||
UpperDimensionContext nextUpperContext,
|
Objects.requireNonNull(worldManager, "engine world manager");
|
||||||
DimensionStackContext nextDimensionStackContext,
|
|
||||||
BiomeMaxes nextBiomeMaxes
|
|
||||||
) {
|
|
||||||
return new EngineRuntime(
|
|
||||||
nextCacheId,
|
|
||||||
target,
|
|
||||||
nextComplex,
|
|
||||||
nextUpperContext,
|
|
||||||
nextDimensionStackContext,
|
|
||||||
effects,
|
|
||||||
mode,
|
|
||||||
worldManager,
|
|
||||||
hash32,
|
|
||||||
nextBiomeMaxes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) {
|
EngineRuntime withGeneration(GenerationRuntime nextGeneration) {
|
||||||
|
return new EngineRuntime(nextGeneration, effects, worldManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,24 +20,22 @@ package art.arcane.iris.engine;
|
|||||||
|
|
||||||
import art.arcane.iris.core.loader.IrisData;
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
import art.arcane.iris.core.loader.ResourceLoader;
|
import art.arcane.iris.core.loader.ResourceLoader;
|
||||||
import art.arcane.iris.engine.EngineRuntime.BiomeMaxes;
|
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
|
||||||
|
import art.arcane.iris.engine.framework.SeedManager;
|
||||||
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
||||||
import art.arcane.iris.engine.actuator.IrisDimensionStackActuator;
|
|
||||||
import art.arcane.iris.engine.framework.EngineEffects;
|
import art.arcane.iris.engine.framework.EngineEffects;
|
||||||
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
import art.arcane.iris.engine.framework.EngineEffectsProvider;
|
||||||
import art.arcane.iris.engine.framework.EngineMode;
|
import art.arcane.iris.engine.framework.EngineMode;
|
||||||
import art.arcane.iris.engine.framework.EngineTarget;
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||||
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
import art.arcane.iris.engine.framework.EngineWorldManagerProvider;
|
||||||
import art.arcane.iris.engine.modifier.IrisCustomModifier;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
|
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||||
|
import art.arcane.iris.engine.history.GenerationKernelRegistry;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
import art.arcane.iris.engine.object.IrisBiomePaletteLayer;
|
||||||
import art.arcane.iris.engine.object.IrisDecorator;
|
import art.arcane.iris.engine.object.IrisDecorator;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
|
||||||
import art.arcane.iris.engine.object.IrisDimensionMode;
|
|
||||||
import art.arcane.iris.engine.object.IrisDimensionModeType;
|
|
||||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||||
import art.arcane.iris.engine.object.IrisStaticObjectLayer;
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
import art.arcane.iris.util.project.context.IrisContext;
|
import art.arcane.iris.util.project.context.IrisContext;
|
||||||
@@ -45,6 +43,9 @@ import art.arcane.volmlib.util.io.IO;
|
|||||||
import art.arcane.volmlib.util.math.M;
|
import art.arcane.volmlib.util.math.M;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@@ -64,22 +65,56 @@ final class EngineRuntimeBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EngineRuntime buildRuntime() {
|
EngineRuntime buildRuntime() {
|
||||||
return buildRuntime(engine.getTarget());
|
return buildRuntime(
|
||||||
|
engine.getTarget(),
|
||||||
|
engine.getInitialMantleStorageDirectory(),
|
||||||
|
engine.getInitialKernelVersion(),
|
||||||
|
engine.getInitialTransitionPlan(),
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
|
EngineRuntime buildRuntime(EngineTarget runtimeTarget) {
|
||||||
RuntimeAssembly assembly = new RuntimeAssembly(RuntimeAssembly.nextRuntimeId(), runtimeTarget);
|
GenerationRuntime active = requireRuntime("hotload the engine runtime").generation();
|
||||||
|
return buildRuntime(
|
||||||
|
runtimeTarget,
|
||||||
|
active.mantleStorageDirectory(),
|
||||||
|
active.kernelVersion(),
|
||||||
|
active.transitionPlan(),
|
||||||
|
active.mantle());
|
||||||
|
}
|
||||||
|
|
||||||
|
private EngineRuntime buildRuntime(
|
||||||
|
EngineTarget runtimeTarget,
|
||||||
|
Path mantleStorageDirectory,
|
||||||
|
GenerationKernelRegistry.Version kernelVersion,
|
||||||
|
TransitionGenerationPlan transitionPlan,
|
||||||
|
EngineMantle transferredMantle
|
||||||
|
) {
|
||||||
|
RuntimeAssembly assembly = new RuntimeAssembly(
|
||||||
|
RuntimeAssembly.nextRuntimeId(),
|
||||||
|
runtimeTarget,
|
||||||
|
mantleStorageDirectory,
|
||||||
|
selectRuntimeKernel(kernelVersion),
|
||||||
|
transitionPlan);
|
||||||
engine.runtimeAssembly.set(assembly);
|
engine.runtimeAssembly.set(assembly);
|
||||||
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||||
IrisLogging.debug("Setup Engine " + assembly.cacheId);
|
IrisLogging.debug("Setup Engine " + assembly.cacheId);
|
||||||
long started = M.ms();
|
long started = M.ms();
|
||||||
assembly.complex = new IrisComplex(engine);
|
if (transferredMantle == null) {
|
||||||
|
assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory);
|
||||||
|
assembly.ownsMantle = true;
|
||||||
|
} else {
|
||||||
|
assembly.mantle = transferredMantle;
|
||||||
|
}
|
||||||
|
IrisLogging.debug("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - started) + "ms");
|
||||||
|
started = M.ms();
|
||||||
|
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
|
||||||
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] complex=" + (M.ms() - started) + "ms");
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
assembly.dimensionStackContext = buildDimensionStackContext();
|
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
|
||||||
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] buildDimensionStackContext=" + (M.ms() - started) + "ms");
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
assembly.upperContext = buildUpperContext();
|
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
|
||||||
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - started) + "ms");
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine);
|
assembly.effects = IrisServices.get(EngineEffectsProvider.class).create(engine);
|
||||||
@@ -89,21 +124,16 @@ final class EngineRuntimeBuilder {
|
|||||||
IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] EngineEffects=" + (M.ms() - started) + "ms");
|
||||||
assembly.hash32 = new CompletableFuture<>();
|
assembly.hash32 = new CompletableFuture<>();
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
engine.getMantle().hotload();
|
assembly.mantle.hotload();
|
||||||
IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - started) + "ms");
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
assembly.mode = createMode();
|
assembly.mode = assembly.runtimeKernel.createMode(engine);
|
||||||
IrisStaticObjectLayer staticObjects = engine.getDimension().getStaticObjectLayer(engine.getData());
|
assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode);
|
||||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
assembly.runtimeKernel.registerDimensionStack(
|
||||||
staticObjects.apply(engine, x, z, blocks));
|
engine,
|
||||||
if (assembly.dimensionStackContext != null) {
|
assembly.mode,
|
||||||
IrisDimensionStackActuator dimensionStack = new IrisDimensionStackActuator(engine);
|
assembly.dimensionStackContext
|
||||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
);
|
||||||
dimensionStack.actuate(x, z, blocks, multicore, context));
|
|
||||||
IrisCustomModifier stackCustom = new IrisCustomModifier(engine);
|
|
||||||
assembly.mode.registerStage((x, z, blocks, biomes, multicore, context) ->
|
|
||||||
stackCustom.modify(x, z, blocks, multicore, context));
|
|
||||||
}
|
|
||||||
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] setupMode=" + (M.ms() - started) + "ms");
|
||||||
started = M.ms();
|
started = M.ms();
|
||||||
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
|
assembly.worldManager = IrisServices.get(EngineWorldManagerProvider.class).create(engine);
|
||||||
@@ -112,7 +142,7 @@ final class EngineRuntimeBuilder {
|
|||||||
}
|
}
|
||||||
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
|
IrisLogging.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - started) + "ms");
|
||||||
BiomeMaxes biomeMaxes = computeBiomeMaxes();
|
BiomeMaxes biomeMaxes = computeBiomeMaxes();
|
||||||
return assembly.freeze(biomeMaxes);
|
return assembly.freezeRuntime(biomeMaxes);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
|
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
|
||||||
if (cleanupFailure != e) {
|
if (cleanupFailure != e) {
|
||||||
@@ -124,8 +154,60 @@ final class EngineRuntimeBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GenerationRuntime buildDetachedGenerationRuntime(EngineTarget runtimeTarget, Path mantleStorageDirectory) {
|
||||||
|
return buildDetachedGenerationRuntime(
|
||||||
|
runtimeTarget,
|
||||||
|
mantleStorageDirectory,
|
||||||
|
GenerationKernelRegistry.standard().current(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerationRuntime buildDetachedGenerationRuntime(
|
||||||
|
EngineTarget runtimeTarget,
|
||||||
|
Path mantleStorageDirectory,
|
||||||
|
GenerationKernelRegistry.Version kernelVersion,
|
||||||
|
TransitionGenerationPlan transitionPlan
|
||||||
|
) {
|
||||||
|
RuntimeAssembly assembly = new RuntimeAssembly(
|
||||||
|
RuntimeAssembly.nextRuntimeId(),
|
||||||
|
runtimeTarget,
|
||||||
|
mantleStorageDirectory,
|
||||||
|
selectRuntimeKernel(kernelVersion),
|
||||||
|
transitionPlan);
|
||||||
|
engine.runtimeAssembly.set(assembly);
|
||||||
|
try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) {
|
||||||
|
IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId);
|
||||||
|
assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory);
|
||||||
|
assembly.ownsMantle = true;
|
||||||
|
assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan);
|
||||||
|
assembly.dimensionStackContext = assembly.runtimeKernel.createDimensionStackContext(engine);
|
||||||
|
assembly.upperContext = assembly.runtimeKernel.createUpperContext(engine);
|
||||||
|
assembly.hash32 = CompletableFuture.completedFuture(computePackHash(runtimeTarget.getData()));
|
||||||
|
assembly.mantle.hotload();
|
||||||
|
assembly.mode = assembly.runtimeKernel.createMode(engine);
|
||||||
|
assembly.runtimeKernel.registerStaticObjects(engine, assembly.mode);
|
||||||
|
assembly.runtimeKernel.registerDimensionStack(
|
||||||
|
engine,
|
||||||
|
assembly.mode,
|
||||||
|
assembly.dimensionStackContext
|
||||||
|
);
|
||||||
|
return assembly.freezeGeneration(computeBiomeMaxes());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Throwable cleanupFailure = engine.shutdownSequence.closeAssembly(assembly, e);
|
||||||
|
if (cleanupFailure != e) {
|
||||||
|
e.addSuppressed(cleanupFailure);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Failed to build a detached Iris generation runtime.", e);
|
||||||
|
} finally {
|
||||||
|
engine.runtimeAssembly.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void publishRuntime(EngineRuntime next, EngineRuntime previous) {
|
void publishRuntime(EngineRuntime next, EngineRuntime previous) {
|
||||||
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(previous, null);
|
Throwable retirementFailure = engine.shutdownSequence.closeRuntime(
|
||||||
|
previous,
|
||||||
|
next.generation().mantle(),
|
||||||
|
null);
|
||||||
if (retirementFailure != null) {
|
if (retirementFailure != null) {
|
||||||
retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure);
|
retirementFailure = engine.shutdownSequence.closeRuntime(next, retirementFailure);
|
||||||
engine.lifecycleState = LifecycleState.FAILED;
|
engine.lifecycleState = LifecycleState.FAILED;
|
||||||
@@ -136,7 +218,7 @@ final class EngineRuntimeBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
engine.runtime = next;
|
engine.runtime = next;
|
||||||
engine.publishedTarget = next.target();
|
engine.publishedTarget = next.generation().target();
|
||||||
engine.getGenerationSessions().activateNextSession();
|
engine.getGenerationSessions().activateNextSession();
|
||||||
engine.lifecycleState = LifecycleState.RUNNING;
|
engine.lifecycleState = LifecycleState.RUNNING;
|
||||||
engine.getClosing().set(false);
|
engine.getClosing().set(false);
|
||||||
@@ -163,33 +245,11 @@ final class EngineRuntimeBuilder {
|
|||||||
throw new IllegalStateException("Failed to start the Iris world manager.", e);
|
throw new IllegalStateException("Failed to start the Iris world manager.", e);
|
||||||
}
|
}
|
||||||
scheduleRuntimeTasks(next);
|
scheduleRuntimeTasks(next);
|
||||||
IrisLogging.debug("Engine Setup Complete " + next.cacheId());
|
IrisLogging.debug("Engine Setup Complete " + next.generation().cacheId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
|
private void scheduleRuntimeTasks(EngineRuntime engineRuntime) {
|
||||||
try {
|
schedulePackHash(engineRuntime.generation());
|
||||||
if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
|
|
||||||
try {
|
|
||||||
File[] roots = engine.getData().getLoaders()
|
|
||||||
.values()
|
|
||||||
.stream()
|
|
||||||
.map(ResourceLoader::getFolderName)
|
|
||||||
.map(name -> new File(engine.getData().getDataFolder(), name))
|
|
||||||
.filter(File::exists)
|
|
||||||
.filter(File::isDirectory)
|
|
||||||
.toArray(File[]::new);
|
|
||||||
engineRuntime.hash32().complete(IO.hashRecursiveMeta(roots));
|
|
||||||
} catch (Throwable e) {
|
|
||||||
engineRuntime.hash32().completeExceptionally(e);
|
|
||||||
throw propagate(e);
|
|
||||||
}
|
|
||||||
})) {
|
|
||||||
throw new IllegalStateException("Iris background task admission closed before pack hashing.");
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
engineRuntime.hash32().completeExceptionally(e);
|
|
||||||
IrisLogging.reportError(e);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) {
|
if (!engine.backgroundTasks.scheduleTrackedTask(() -> engine.getPlatformHooks().refreshDatapackWorkspace(engine))) {
|
||||||
throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh.");
|
throw new IllegalStateException("Iris background task admission closed before datapack workspace refresh.");
|
||||||
@@ -199,71 +259,34 @@ final class EngineRuntimeBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UpperDimensionContext buildUpperContext() {
|
private void schedulePackHash(GenerationRuntime generationRuntime) {
|
||||||
IrisDimension dim = engine.getDimension();
|
|
||||||
if (!dim.hasUpperDimension()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String upperKey = dim.getUpperDimension();
|
|
||||||
IrisDimension upperDim = upperKey.equals(dim.getLoadKey())
|
|
||||||
? dim
|
|
||||||
: IrisData.loadAnyDimension(upperKey, engine.getData());
|
|
||||||
if (upperDim != null) {
|
|
||||||
UpperDimensionContext ctx = UpperDimensionContext.create(engine, upperDim);
|
|
||||||
IrisLogging.info("Upper dimension enabled: " + upperKey
|
|
||||||
+ (ctx.isSelfReferencing() ? " (self-referencing)" : " (cross-referencing)"));
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
IrisLogging.warn("Upper dimension '" + upperKey + "' could not be resolved, skipping upper terrain.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DimensionStackContext buildDimensionStackContext() {
|
|
||||||
IrisDimension dimension = engine.getDimension();
|
|
||||||
if (!dimension.hasDimensionStack()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (dimension.hasUpperDimension()) {
|
|
||||||
throw new IllegalStateException("Dimension '" + dimension.getLoadKey()
|
|
||||||
+ "' cannot enable both dimensionStack and upperDimension.");
|
|
||||||
}
|
|
||||||
DimensionStackContext context = DimensionStackContext.create(engine, dimension.getDimensionStack());
|
|
||||||
IrisLogging.info("Dimension stack enabled: "
|
|
||||||
+ String.join(" -> ", dimension.getDimensionStack().getDimensions()));
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EngineMode createMode() {
|
|
||||||
Throwable configuredFailure = null;
|
|
||||||
try {
|
try {
|
||||||
IrisDimensionMode configuredMode = engine.getDimension().getMode();
|
if (!engine.backgroundTasks.scheduleTrackedTask(() -> {
|
||||||
if (configuredMode == null) {
|
try {
|
||||||
configuredMode = new IrisDimensionMode();
|
generationRuntime.hash32().complete(computePackHash(generationRuntime.data()));
|
||||||
engine.getDimension().setMode(configuredMode);
|
} catch (Throwable e) {
|
||||||
|
generationRuntime.hash32().completeExceptionally(e);
|
||||||
|
throw propagate(e);
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
throw new IllegalStateException("Iris background task admission closed before pack hashing.");
|
||||||
}
|
}
|
||||||
EngineMode configured = configuredMode.create(engine);
|
|
||||||
if (configured == null) {
|
|
||||||
throw new IllegalStateException("Dimension mode factory returned null");
|
|
||||||
}
|
|
||||||
return configured;
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
configuredFailure = e;
|
generationRuntime.hash32().completeExceptionally(e);
|
||||||
IrisLogging.reportError(e);
|
IrisLogging.reportError(e);
|
||||||
if (engine.getModeFallbackLogged().compareAndSet(false, true)) {
|
|
||||||
IrisLogging.warn("Failed to initialize configured dimension mode for " + engine.getDimension().getLoadKey() + ", falling back to OVERWORLD mode.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
private long computePackHash(IrisData data) {
|
||||||
EngineMode fallback = IrisDimensionModeType.OVERWORLD.create(engine);
|
File[] roots = data.getLoaders()
|
||||||
if (fallback == null) {
|
.values()
|
||||||
throw new IllegalStateException("OVERWORLD mode factory returned null");
|
.stream()
|
||||||
}
|
.map(ResourceLoader::getFolderName)
|
||||||
return fallback;
|
.map(name -> new File(data.getDataFolder(), name))
|
||||||
} catch (Throwable fallbackFailure) {
|
.filter(File::exists)
|
||||||
fallbackFailure.addSuppressed(configuredFailure);
|
.filter(File::isDirectory)
|
||||||
throw new IllegalStateException("Both configured and fallback Iris engine modes failed.", fallbackFailure);
|
.toArray(File[]::new);
|
||||||
}
|
return IO.hashRecursiveMeta(roots);
|
||||||
}
|
}
|
||||||
|
|
||||||
BiomeMaxes computeBiomeMaxes() {
|
BiomeMaxes computeBiomeMaxes() {
|
||||||
@@ -317,11 +340,27 @@ final class EngineRuntimeBuilder {
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static GenerationKernelRegistry.RuntimeKernel selectRuntimeKernel(
|
||||||
|
GenerationKernelRegistry.Version version
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return GenerationKernelRegistry.standard().select(version);
|
||||||
|
} catch (IOException failure) {
|
||||||
|
throw new IllegalArgumentException("Unsupported executable Iris generation kernel "
|
||||||
|
+ version + ".", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static final class RuntimeAssembly {
|
static final class RuntimeAssembly {
|
||||||
private static final AtomicInteger RUNTIME_IDS = new AtomicInteger();
|
private static final AtomicInteger RUNTIME_IDS = new AtomicInteger();
|
||||||
|
|
||||||
final int cacheId;
|
final int cacheId;
|
||||||
final EngineTarget target;
|
final EngineTarget target;
|
||||||
|
final Path mantleStorageDirectory;
|
||||||
|
final GenerationKernelRegistry.Version kernelVersion;
|
||||||
|
final GenerationKernelRegistry.RuntimeKernel runtimeKernel;
|
||||||
|
final SeedManager seedManager;
|
||||||
|
final TransitionGenerationPlan transitionPlan;
|
||||||
|
|
||||||
static int nextRuntimeId() {
|
static int nextRuntimeId() {
|
||||||
return RUNTIME_IDS.incrementAndGet();
|
return RUNTIME_IDS.incrementAndGet();
|
||||||
@@ -331,29 +370,76 @@ final class EngineRuntimeBuilder {
|
|||||||
DimensionStackContext dimensionStackContext;
|
DimensionStackContext dimensionStackContext;
|
||||||
EngineEffects effects;
|
EngineEffects effects;
|
||||||
EngineMode mode;
|
EngineMode mode;
|
||||||
|
EngineMantle mantle;
|
||||||
EngineWorldManager worldManager;
|
EngineWorldManager worldManager;
|
||||||
CompletableFuture<Long> hash32;
|
CompletableFuture<Long> hash32;
|
||||||
|
boolean ownsMantle;
|
||||||
|
|
||||||
RuntimeAssembly(int cacheId, EngineTarget target) {
|
RuntimeAssembly(int cacheId, EngineTarget target) {
|
||||||
this.cacheId = cacheId;
|
this(
|
||||||
this.target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
EngineRuntime freeze(BiomeMaxes biomeMaxes) {
|
|
||||||
if (complex == null || effects == null || mode == null || worldManager == null || hash32 == null) {
|
|
||||||
throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime.");
|
|
||||||
}
|
|
||||||
return new EngineRuntime(
|
|
||||||
cacheId,
|
cacheId,
|
||||||
target,
|
target,
|
||||||
|
null,
|
||||||
|
selectRuntimeKernel(GenerationKernelRegistry.standard().current()),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeAssembly(int cacheId, EngineTarget target, Path mantleStorageDirectory) {
|
||||||
|
this(
|
||||||
|
cacheId,
|
||||||
|
target,
|
||||||
|
mantleStorageDirectory,
|
||||||
|
selectRuntimeKernel(GenerationKernelRegistry.standard().current()),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeAssembly(
|
||||||
|
int cacheId,
|
||||||
|
EngineTarget target,
|
||||||
|
Path mantleStorageDirectory,
|
||||||
|
GenerationKernelRegistry.RuntimeKernel runtimeKernel,
|
||||||
|
TransitionGenerationPlan transitionPlan
|
||||||
|
) {
|
||||||
|
this.cacheId = cacheId;
|
||||||
|
this.target = target;
|
||||||
|
this.mantleStorageDirectory = mantleStorageDirectory == null
|
||||||
|
? null
|
||||||
|
: IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
|
||||||
|
this.runtimeKernel = Objects.requireNonNull(runtimeKernel, "executable generation kernel");
|
||||||
|
this.seedManager = this.runtimeKernel.createSeedManager(target.getWorld().getRawWorldSeed());
|
||||||
|
this.kernelVersion = this.runtimeKernel.version();
|
||||||
|
this.transitionPlan = transitionPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerationRuntime freezeGeneration(BiomeMaxes biomeMaxes) {
|
||||||
|
if (complex == null || mode == null || mantle == null || mantleStorageDirectory == null || hash32 == null) {
|
||||||
|
throw new IllegalStateException("Cannot publish an incomplete Iris generation runtime.");
|
||||||
|
}
|
||||||
|
return new GenerationRuntime(
|
||||||
|
cacheId,
|
||||||
|
target,
|
||||||
|
target.getData(),
|
||||||
|
target.getDimension(),
|
||||||
|
kernelVersion,
|
||||||
|
runtimeKernel,
|
||||||
|
seedManager,
|
||||||
|
transitionPlan,
|
||||||
complex,
|
complex,
|
||||||
upperContext,
|
upperContext,
|
||||||
dimensionStackContext,
|
dimensionStackContext,
|
||||||
effects,
|
|
||||||
mode,
|
mode,
|
||||||
worldManager,
|
mantle,
|
||||||
|
mantleStorageDirectory,
|
||||||
hash32,
|
hash32,
|
||||||
biomeMaxes);
|
biomeMaxes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EngineRuntime freezeRuntime(BiomeMaxes biomeMaxes) {
|
||||||
|
if (effects == null || worldManager == null) {
|
||||||
|
throw new IllegalStateException("Cannot publish an incomplete Iris engine runtime.");
|
||||||
|
}
|
||||||
|
return new EngineRuntime(freezeGeneration(biomeMaxes), effects, worldManager);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,8 +22,10 @@ import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain;
|
|||||||
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
|
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
|
||||||
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
import art.arcane.iris.engine.IrisEngine.LifecycleState;
|
||||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||||
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||||
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
|
||||||
@@ -38,7 +40,6 @@ final class EngineShutdownSequence {
|
|||||||
private final IrisEngine engine;
|
private final IrisEngine engine;
|
||||||
private boolean runtimeReleased;
|
private boolean runtimeReleased;
|
||||||
private boolean targetReleased;
|
private boolean targetReleased;
|
||||||
private boolean mantleReleased;
|
|
||||||
private boolean engineDataReleased;
|
private boolean engineDataReleased;
|
||||||
private boolean preservationReleased;
|
private boolean preservationReleased;
|
||||||
|
|
||||||
@@ -47,6 +48,17 @@ final class EngineShutdownSequence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void close() {
|
void close() {
|
||||||
|
if (!engine.beginShutdown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Throwable routerFailure = runCleanup(null, engine::closeAttachedGenerationHistoryRuntimeRouter);
|
||||||
|
if (routerFailure != null) {
|
||||||
|
synchronized (engine.lifecycleLock) {
|
||||||
|
engine.lifecycleState = LifecycleState.FAILED;
|
||||||
|
}
|
||||||
|
reportIncompleteClose(routerFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Throwable failure;
|
Throwable failure;
|
||||||
synchronized (engine.lifecycleLock) {
|
synchronized (engine.lifecycleLock) {
|
||||||
if (engine.closed) {
|
if (engine.closed) {
|
||||||
@@ -122,14 +134,10 @@ final class EngineShutdownSequence {
|
|||||||
if (runtimeReleased) {
|
if (runtimeReleased) {
|
||||||
failure = releaseTarget(failure);
|
failure = releaseTarget(failure);
|
||||||
}
|
}
|
||||||
if (targetReleased) {
|
|
||||||
failure = releaseMantle(failure);
|
|
||||||
}
|
|
||||||
if (prefetchFailure == null
|
if (prefetchFailure == null
|
||||||
&& engineDataFailure == null
|
&& engineDataFailure == null
|
||||||
&& runtimeReleased
|
&& runtimeReleased
|
||||||
&& targetReleased
|
&& targetReleased) {
|
||||||
&& mantleReleased) {
|
|
||||||
failure = releaseEngineDataForShutdown(failure);
|
failure = releaseEngineDataForShutdown(failure);
|
||||||
}
|
}
|
||||||
if (engineDataReleased) {
|
if (engineDataReleased) {
|
||||||
@@ -140,7 +148,6 @@ final class EngineShutdownSequence {
|
|||||||
if (failure == null
|
if (failure == null
|
||||||
&& runtimeReleased
|
&& runtimeReleased
|
||||||
&& targetReleased
|
&& targetReleased
|
||||||
&& mantleReleased
|
|
||||||
&& engineDataReleased
|
&& engineDataReleased
|
||||||
&& preservationReleased) {
|
&& preservationReleased) {
|
||||||
engine.closed = true;
|
engine.closed = true;
|
||||||
@@ -185,10 +192,10 @@ final class EngineShutdownSequence {
|
|||||||
() -> NativeStructureOwnershipStore.close(engine));
|
() -> NativeStructureOwnershipStore.close(engine));
|
||||||
cleanupFailure = appendFailure(cleanupFailure, ownershipFailure);
|
cleanupFailure = appendFailure(cleanupFailure, ownershipFailure);
|
||||||
if (ownershipFailure == null) {
|
if (ownershipFailure == null) {
|
||||||
|
cleanupFailure = closeDetachedGenerationRuntimes(cleanupFailure);
|
||||||
cleanupFailure = closeRuntime(engine.runtime, cleanupFailure);
|
cleanupFailure = closeRuntime(engine.runtime, cleanupFailure);
|
||||||
engine.runtime = null;
|
engine.runtime = null;
|
||||||
cleanupFailure = runCleanup(cleanupFailure, engine.getTarget()::close);
|
cleanupFailure = runCleanup(cleanupFailure, engine.publishedTarget::close);
|
||||||
cleanupFailure = runCleanup(cleanupFailure, engine.getMantle()::close);
|
|
||||||
cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData);
|
cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData);
|
||||||
engine.closed = true;
|
engine.closed = true;
|
||||||
cleanupFailure = runCleanup(cleanupFailure, () -> {
|
cleanupFailure = runCleanup(cleanupFailure, () -> {
|
||||||
@@ -232,18 +239,69 @@ final class EngineShutdownSequence {
|
|||||||
assembly.hash32.cancel(true);
|
assembly.hash32.cancel(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (assembly.mantle != null && assembly.ownsMantle) {
|
||||||
|
assembly.ownsMantle = false;
|
||||||
|
failure = runCleanup(failure, assembly.mantle::saveAllNow);
|
||||||
|
failure = runCleanup(failure, assembly.mantle::close);
|
||||||
|
}
|
||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) {
|
Throwable closeRuntime(EngineRuntime engineRuntime, Throwable failure) {
|
||||||
|
return closeRuntime(engineRuntime, null, failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
Throwable closeRuntime(
|
||||||
|
EngineRuntime engineRuntime,
|
||||||
|
EngineMantle retainedMantle,
|
||||||
|
Throwable failure
|
||||||
|
) {
|
||||||
if (engineRuntime == null) {
|
if (engineRuntime == null) {
|
||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
failure = runCleanup(failure, engineRuntime.worldManager()::close);
|
failure = runCleanup(failure, engineRuntime.worldManager()::close);
|
||||||
failure = runCleanup(failure, engineRuntime.effects()::close);
|
failure = runCleanup(failure, engineRuntime.effects()::close);
|
||||||
failure = runCleanup(failure, engineRuntime.mode()::close);
|
return closeGenerationRuntime(engineRuntime.generation(), retainedMantle, failure);
|
||||||
failure = runCleanup(failure, engineRuntime.complex()::close);
|
}
|
||||||
failure = runCleanup(failure, () -> engineRuntime.hash32().cancel(true));
|
|
||||||
|
Throwable closeGenerationRuntime(GenerationRuntime generationRuntime, Throwable failure) {
|
||||||
|
return closeGenerationRuntime(generationRuntime, null, failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
Throwable closeGenerationRuntime(
|
||||||
|
GenerationRuntime generationRuntime,
|
||||||
|
EngineMantle retainedMantle,
|
||||||
|
Throwable failure
|
||||||
|
) {
|
||||||
|
if (generationRuntime == null) {
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
failure = runCleanup(failure, generationRuntime.mode()::close);
|
||||||
|
failure = runCleanup(failure, generationRuntime.complex()::close);
|
||||||
|
failure = runCleanup(failure, () -> generationRuntime.hash32().cancel(true));
|
||||||
|
if (generationRuntime.mantle() != retainedMantle) {
|
||||||
|
failure = runCleanup(failure, generationRuntime.mantle()::saveAllNow);
|
||||||
|
failure = runCleanup(failure, generationRuntime.mantle()::close);
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
Throwable closeDetachedGenerationRuntime(GenerationRuntime generationRuntime, Throwable failure) {
|
||||||
|
IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding(
|
||||||
|
engine,
|
||||||
|
generationRuntime);
|
||||||
|
try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) {
|
||||||
|
failure = closeGenerationRuntime(generationRuntime, failure);
|
||||||
|
} catch (Throwable scopeFailure) {
|
||||||
|
failure = appendFailure(failure, scopeFailure);
|
||||||
|
}
|
||||||
|
return closeDetachedTarget(generationRuntime.target(), failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
Throwable closeDetachedTarget(EngineTarget target, Throwable failure) {
|
||||||
|
failure = runCleanup(failure, () -> target.getData().unregisterEngine(engine));
|
||||||
|
failure = runCleanup(failure, target::close);
|
||||||
|
failure = runCleanup(failure, target.getData()::close);
|
||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +309,10 @@ final class EngineShutdownSequence {
|
|||||||
if (runtimeReleased) {
|
if (runtimeReleased) {
|
||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
Throwable detachedFailure = closeDetachedGenerationRuntimes(null);
|
||||||
|
if (detachedFailure != null) {
|
||||||
|
return appendFailure(failure, detachedFailure);
|
||||||
|
}
|
||||||
Throwable runtimeFailure = closeRuntime(engine.runtime, null);
|
Throwable runtimeFailure = closeRuntime(engine.runtime, null);
|
||||||
if (runtimeFailure != null) {
|
if (runtimeFailure != null) {
|
||||||
return appendFailure(failure, runtimeFailure);
|
return appendFailure(failure, runtimeFailure);
|
||||||
@@ -260,11 +322,23 @@ final class EngineShutdownSequence {
|
|||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Throwable closeDetachedGenerationRuntimes(Throwable failure) {
|
||||||
|
GenerationRuntime[] detachedRuntimes = engine.detachedGenerationRuntimes.toArray(new GenerationRuntime[0]);
|
||||||
|
for (GenerationRuntime detached : detachedRuntimes) {
|
||||||
|
Throwable detachedFailure = closeDetachedGenerationRuntime(detached, null);
|
||||||
|
failure = appendFailure(failure, detachedFailure);
|
||||||
|
if (detachedFailure == null) {
|
||||||
|
engine.detachedGenerationRuntimes.remove(detached);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
private Throwable releaseTarget(Throwable failure) {
|
private Throwable releaseTarget(Throwable failure) {
|
||||||
if (targetReleased) {
|
if (targetReleased) {
|
||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
Throwable targetFailure = runCleanup(null, engine.getTarget()::close);
|
Throwable targetFailure = runCleanup(null, engine.publishedTarget::close);
|
||||||
if (targetFailure != null) {
|
if (targetFailure != null) {
|
||||||
return appendFailure(failure, targetFailure);
|
return appendFailure(failure, targetFailure);
|
||||||
}
|
}
|
||||||
@@ -272,18 +346,6 @@ final class EngineShutdownSequence {
|
|||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Throwable releaseMantle(Throwable failure) {
|
|
||||||
if (mantleReleased) {
|
|
||||||
return failure;
|
|
||||||
}
|
|
||||||
Throwable mantleFailure = runCleanup(null, engine.getMantle()::close);
|
|
||||||
if (mantleFailure != null) {
|
|
||||||
return appendFailure(failure, mantleFailure);
|
|
||||||
}
|
|
||||||
mantleReleased = true;
|
|
||||||
return failure;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Throwable releaseEngineDataForShutdown(Throwable failure) {
|
private Throwable releaseEngineDataForShutdown(Throwable failure) {
|
||||||
if (engineDataReleased) {
|
if (engineDataReleased) {
|
||||||
return failure;
|
return failure;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.framework.EngineMode;
|
||||||
|
import art.arcane.iris.engine.framework.EngineTarget;
|
||||||
|
import art.arcane.iris.engine.framework.SeedManager;
|
||||||
|
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||||
|
import art.arcane.iris.engine.history.GenerationKernelRegistry;
|
||||||
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
record GenerationRuntime(
|
||||||
|
int cacheId,
|
||||||
|
EngineTarget target,
|
||||||
|
IrisData data,
|
||||||
|
IrisDimension dimension,
|
||||||
|
GenerationKernelRegistry.Version kernelVersion,
|
||||||
|
GenerationKernelRegistry.RuntimeKernel runtimeKernel,
|
||||||
|
SeedManager seedManager,
|
||||||
|
TransitionGenerationPlan transitionPlan,
|
||||||
|
IrisComplex complex,
|
||||||
|
UpperDimensionContext upperContext,
|
||||||
|
DimensionStackContext dimensionStackContext,
|
||||||
|
EngineMode mode,
|
||||||
|
EngineMantle mantle,
|
||||||
|
Path mantleStorageDirectory,
|
||||||
|
CompletableFuture<Long> hash32,
|
||||||
|
BiomeMaxes biomeMaxes
|
||||||
|
) {
|
||||||
|
GenerationRuntime {
|
||||||
|
Objects.requireNonNull(target, "generation target");
|
||||||
|
Objects.requireNonNull(data, "generation data");
|
||||||
|
Objects.requireNonNull(dimension, "generation dimension");
|
||||||
|
Objects.requireNonNull(kernelVersion, "generation kernel version");
|
||||||
|
Objects.requireNonNull(runtimeKernel, "executable generation kernel");
|
||||||
|
Objects.requireNonNull(seedManager, "generation seed manager");
|
||||||
|
if (!kernelVersion.equals(runtimeKernel.version())) {
|
||||||
|
throw new IllegalArgumentException("Generation kernel version does not match its executable runtime.");
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(complex, "generation complex");
|
||||||
|
Objects.requireNonNull(mode, "generation mode");
|
||||||
|
Objects.requireNonNull(mantle, "generation mantle");
|
||||||
|
mantleStorageDirectory = IrisEngineMantle.normalizeStorageDirectory(mantleStorageDirectory);
|
||||||
|
Objects.requireNonNull(hash32, "generation pack hash");
|
||||||
|
Objects.requireNonNull(biomeMaxes, "generation biome maximums");
|
||||||
|
if (target.getData() != data || target.getDimension() != dimension) {
|
||||||
|
throw new IllegalArgumentException("Generation target, data, and dimension must describe one runtime.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerationRuntime withComplex(
|
||||||
|
int nextCacheId,
|
||||||
|
IrisComplex nextComplex,
|
||||||
|
UpperDimensionContext nextUpperContext,
|
||||||
|
DimensionStackContext nextDimensionStackContext,
|
||||||
|
BiomeMaxes nextBiomeMaxes
|
||||||
|
) {
|
||||||
|
return new GenerationRuntime(
|
||||||
|
nextCacheId,
|
||||||
|
target,
|
||||||
|
data,
|
||||||
|
dimension,
|
||||||
|
kernelVersion,
|
||||||
|
runtimeKernel,
|
||||||
|
seedManager,
|
||||||
|
transitionPlan,
|
||||||
|
nextComplex,
|
||||||
|
nextUpperContext,
|
||||||
|
nextDimensionStackContext,
|
||||||
|
mode,
|
||||||
|
mantle,
|
||||||
|
mantleStorageDirectory,
|
||||||
|
hash32,
|
||||||
|
nextBiomeMaxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
record BiomeMaxes(double objectDensity, double layerDensity, double decoratorDensity) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||||
|
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.IrisEngine.GenerationRuntimeBinding;
|
||||||
|
import art.arcane.iris.engine.IrisEngine.GenerationRuntimeScope;
|
||||||
|
|
||||||
|
final class GenerationRuntimeScopeState {
|
||||||
|
private final ThreadLocal<GenerationRuntimeBinding> current = new ThreadLocal<>();
|
||||||
|
|
||||||
|
GenerationRuntimeBinding current() {
|
||||||
|
return current.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerationRuntimeScope open(GenerationRuntimeBinding binding) {
|
||||||
|
Thread owner = Thread.currentThread();
|
||||||
|
GenerationRuntimeBinding previous = current.get();
|
||||||
|
current.set(binding);
|
||||||
|
return new GenerationRuntimeScope(this, owner, previous, binding);
|
||||||
|
}
|
||||||
|
|
||||||
|
void close(
|
||||||
|
Thread owner,
|
||||||
|
GenerationRuntimeBinding previous,
|
||||||
|
GenerationRuntimeBinding installed
|
||||||
|
) {
|
||||||
|
if (Thread.currentThread() != owner) {
|
||||||
|
throw new IllegalStateException("Iris generation runtime scope closed from a different thread.");
|
||||||
|
}
|
||||||
|
if (current.get() != installed) {
|
||||||
|
throw new IllegalStateException("Iris generation runtime scopes must close in LIFO order.");
|
||||||
|
}
|
||||||
|
if (previous == null) {
|
||||||
|
current.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
current.set(previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user