mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Added generation history
- World-local pack epochs preserve generated chunks across restart-only updates - Kernel catalogs and source seals retain executable generation behavior across jar changes - Crash-safe ownership, boundary, registry, semantic, and mantle records survive activation changes - New terrain blends lower surfaces and inverted ceilings against frozen historical boundaries - Cave, object, structure, biome, and hydrology work respects activation ownership - Find commands merge recorded facts with eligible active predictions for biomes, regions, structures, objects, and rivers - Bukkit, Fabric, Forge, and NeoForge route generation through retained runtime scopes - Shutdown ordering preserves class-loader resources until world and service cleanup finishes - Tests cover activation chains, failure recovery, retained registries, bounded lookup, and transition geometry
This commit is contained in:
1 parent
c1bcdd0a9d
commit
30113c4133
258 files changed
+38965
-1659
No files matched your search
+145
-92
@@ -18,9 +18,11 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.util.project.context.IrisContext;
|
||||
@@ -43,6 +45,7 @@ import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -63,7 +66,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
|
||||
private final BiomeSource serializedSource;
|
||||
private final Set<String> warnedUnresolvedBiomeKeys = ConcurrentHashMap.newKeySet();
|
||||
private final Set<StructureStateBiomeSource> structureStateSources = ConcurrentHashMap.newKeySet();
|
||||
// Pack generation. Every cache on this source is keyed by it, which is what keeps memoized tables from
|
||||
// surviving a repoint with the previous pack's content.
|
||||
private final AtomicLong packGeneration = new AtomicLong();
|
||||
@@ -96,9 +98,12 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
possibleStructureBiomeKeys = null;
|
||||
biomeKeySets = null;
|
||||
possibleBiomesCache = null;
|
||||
for (StructureStateBiomeSource source : structureStateSources) {
|
||||
source.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
void evictRuntime(int runtimeIdentity) {
|
||||
visibleBiomeCache.evictRuntime(runtimeIdentity);
|
||||
structureBiomeCache.evictRuntime(runtimeIdentity);
|
||||
surfaceStructureBiomeCache.evictRuntime(runtimeIdentity);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +137,6 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
});
|
||||
StructureStateBiomeSource source = new StructureStateBiomeSource(this, Set.copyOf(possible));
|
||||
structureStateSources.add(source);
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -261,15 +265,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (engine == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
engine, quartX << 2, quartZ << 2, "modded_structure_biome");
|
||||
try (historyScope) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
}
|
||||
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,13 +287,14 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return getSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||
}
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
int runtimeIdentity = engine.getCacheID();
|
||||
BiomeHolderTable cache = structureBiomeCache;
|
||||
Holder<Biome> cached = cache.get(key);
|
||||
Holder<Biome> cached = cache.get(runtimeIdentity, key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
cache.put(key, resolved);
|
||||
cache.put(runtimeIdentity, key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -294,23 +303,28 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (engine == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
engine, quartX << 2, quartZ << 2, "modded_visible_biome");
|
||||
try (historyScope) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_visible_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris visible biome lookup was rejected during an engine transition");
|
||||
}
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
BiomeHolderTable cache = visibleBiomeCache;
|
||||
Holder<Biome> cached = cache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris visible biome lookup has no active engine runtime");
|
||||
}
|
||||
long key = packNoiseKey(quartX, quartY, quartZ);
|
||||
int runtimeIdentity = engine.getCacheID();
|
||||
BiomeHolderTable cache = visibleBiomeCache;
|
||||
Holder<Biome> cached = cache.get(runtimeIdentity, key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
cache.put(runtimeIdentity, key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
Holder<Biome> resolved = resolveVisibleBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
cache.put(key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,13 +363,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris biome radius lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||
boolean surfaceQuery;
|
||||
try (lease) {
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
engine, x, z, "modded_biomes_within");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris biome radius lookup has no active engine runtime");
|
||||
}
|
||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
||||
surfaceQuery = monumentQuery || isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight());
|
||||
}
|
||||
boolean monumentQuery = isMonumentSurfaceBiomeQuery(
|
||||
y, radius, engine.getMinHeight(), engine.getDimension().getFluidHeight());
|
||||
if (!monumentQuery && !isGuaranteedSurfaceBiome(minQuartY, engine.getMinHeight())) {
|
||||
if (!surfaceQuery) {
|
||||
return super.getBiomesWithin(x, y, z, radius, sampler);
|
||||
}
|
||||
int minQuartX = QuartPos.fromBlock(x - radius);
|
||||
@@ -366,7 +386,11 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
Set<Holder<Biome>> biomes = new HashSet<>(columns);
|
||||
for (int quartZ = minQuartZ; quartZ <= maxQuartZ; quartZ++) {
|
||||
for (int quartX = minQuartX; quartX <= maxQuartX; quartX++) {
|
||||
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
engine, quartX << 2, quartZ << 2, "modded_biomes_within");
|
||||
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
biomes.add(getSurfaceStructureBiome(engine, quartX, quartZ, sampler));
|
||||
}
|
||||
}
|
||||
}
|
||||
return biomes;
|
||||
@@ -376,13 +400,14 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
private Holder<Biome> getSurfaceStructureBiome(Engine engine, int quartX, int quartZ,
|
||||
Climate.Sampler sampler) {
|
||||
long key = packColumnKey(quartX, quartZ);
|
||||
int runtimeIdentity = engine.getCacheID();
|
||||
BiomeHolderTable cache = surfaceStructureBiomeCache;
|
||||
Holder<Biome> cached = cache.get(key);
|
||||
Holder<Biome> cached = cache.get(runtimeIdentity, key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
|
||||
cache.put(key, resolved);
|
||||
cache.put(runtimeIdentity, key, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -436,6 +461,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
if (resolution == null || registry == null) {
|
||||
return serializedSource.getNoiseBiome(quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
String historicalBiomeKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
|
||||
resolution.blockX(),
|
||||
resolution.blockY(),
|
||||
resolution.blockZ()).orElse(null);
|
||||
if (historicalBiomeKey != null) {
|
||||
Holder<Biome> historical = resolveHolder(registry, historicalBiomeKey);
|
||||
if (historical == null) {
|
||||
throw new IllegalStateException("Historical Iris biome '" + historicalBiomeKey
|
||||
+ "' is not registered at block " + resolution.blockX() + ","
|
||||
+ resolution.blockY() + "," + resolution.blockZ());
|
||||
}
|
||||
return historical;
|
||||
}
|
||||
String biomeKey;
|
||||
if (resolution.irisBiome().isCustom()) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome().getCustomBiome(
|
||||
@@ -490,42 +528,30 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, new RNG(seed));
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveRequiredStructureBiome(int quartX, int quartY, int quartZ) {
|
||||
private Holder<Biome> resolveRequiredStructureBiome(
|
||||
int quartX,
|
||||
int quartY,
|
||||
int quartZ,
|
||||
Climate.Sampler sampler
|
||||
) {
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
if (current == null) {
|
||||
throw new IllegalStateException("Iris structure biome source is not bound to its generator");
|
||||
}
|
||||
Engine engine = current.awaitStructureEngine();
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||
}
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
engine, quartX << 2, quartZ << 2, "modded_structure_state_biome");
|
||||
try (historyScope) {
|
||||
GenerationSessionLease lease = tryAcquireGenerationLease(engine, "modded_structure_state_biome");
|
||||
if (lease == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup was rejected during an engine transition");
|
||||
}
|
||||
Registry<Biome> registry = biomeRegistry();
|
||||
if (registry == null) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no biome registry");
|
||||
try (lease; IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
|
||||
if (!isReady(engine)) {
|
||||
throw new IllegalStateException("Iris structure biome lookup has no active engine runtime");
|
||||
}
|
||||
return getNoiseBiome(engine, quartX, quartY, quartZ, sampler);
|
||||
}
|
||||
IrisBiome irisBiome;
|
||||
if (isGuaranteedSurfaceBiome(quartY, engine.getMinHeight())) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(quartX << 2, quartZ << 2);
|
||||
} else {
|
||||
BiomeResolution resolution = resolveBiomeResolution(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,6 +602,30 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return current == null ? null : current.structureEngineOrNull();
|
||||
}
|
||||
|
||||
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||
Engine engine,
|
||||
int blockX,
|
||||
int blockZ,
|
||||
String operation
|
||||
) {
|
||||
IrisModdedChunkGenerator current = generator;
|
||||
if (current != null && current.allowsGenerationHistoryBypass(engine)) {
|
||||
return null;
|
||||
}
|
||||
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||
throw new IllegalStateException("Iris " + operation + " requires an IrisEngine runtime.");
|
||||
}
|
||||
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElseThrow(() ->
|
||||
new IllegalStateException("Iris " + operation
|
||||
+ " requires an attached generation-history runtime router."));
|
||||
try {
|
||||
return router.openCoordinateScope(blockX, blockZ);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation + " could not route block "
|
||||
+ blockX + "," + blockZ + " through generation history.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationSessionLease tryAcquireGenerationLease(Engine engine, String operation) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
@@ -652,6 +702,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
}
|
||||
LinkedHashSet<String> required = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> visible = new LinkedHashSet<>();
|
||||
required.addAll(IrisModdedChunkGenerator.retainedBiomeKeys(engine));
|
||||
for (IrisBiome irisBiome : engine.getAllBiomes()) {
|
||||
String derivative = normalizeKey(irisBiome.getStructureDerivativeKey());
|
||||
if (derivative != null) {
|
||||
@@ -782,35 +833,36 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
|
||||
private volatile AtomicReferenceArray<Entry> table;
|
||||
|
||||
Holder<Biome> get(long key) {
|
||||
Holder<Biome> get(int runtimeIdentity, long key) {
|
||||
AtomicReferenceArray<Entry> slots = table;
|
||||
if (slots == null) {
|
||||
return null;
|
||||
}
|
||||
int home = home(key);
|
||||
int home = home(runtimeIdentity, key);
|
||||
for (int probe = 0; probe < PROBE_LIMIT; probe++) {
|
||||
Entry entry = slots.get((home + probe) & MASK);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
if (entry.key() == key) {
|
||||
if (entry.runtimeIdentity() == runtimeIdentity && entry.key() == key) {
|
||||
return entry.value();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void put(long key, Holder<Biome> value) {
|
||||
void put(int runtimeIdentity, long key, Holder<Biome> value) {
|
||||
AtomicReferenceArray<Entry> slots = table;
|
||||
if (slots == null) {
|
||||
slots = install();
|
||||
}
|
||||
int home = home(key);
|
||||
Entry entry = new Entry(key, value);
|
||||
int home = home(runtimeIdentity, key);
|
||||
Entry entry = new Entry(runtimeIdentity, key, value);
|
||||
for (int probe = 0; probe < PROBE_LIMIT; probe++) {
|
||||
int slot = (home + probe) & MASK;
|
||||
Entry existing = slots.get(slot);
|
||||
if (existing == null || existing.key() == key) {
|
||||
if (existing == null
|
||||
|| (existing.runtimeIdentity() == runtimeIdentity && existing.key() == key)) {
|
||||
slots.set(slot, entry);
|
||||
return;
|
||||
}
|
||||
@@ -818,6 +870,19 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
slots.set(home, entry);
|
||||
}
|
||||
|
||||
void evictRuntime(int runtimeIdentity) {
|
||||
AtomicReferenceArray<Entry> slots = table;
|
||||
if (slots == null) {
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < slots.length(); index++) {
|
||||
Entry entry = slots.get(index);
|
||||
if (entry != null && entry.runtimeIdentity() == runtimeIdentity) {
|
||||
slots.compareAndSet(index, entry, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized AtomicReferenceArray<Entry> install() {
|
||||
AtomicReferenceArray<Entry> existing = table;
|
||||
if (existing != null) {
|
||||
@@ -828,29 +893,25 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
return created;
|
||||
}
|
||||
|
||||
private static int home(long key) {
|
||||
long mixed = key * 0x9E3779B97F4A7C15L;
|
||||
private static int home(int runtimeIdentity, long key) {
|
||||
long mixed = (key ^ ((long) runtimeIdentity * 0xC2B2AE3D27D4EB4FL))
|
||||
* 0x9E3779B97F4A7C15L;
|
||||
return (int) ((mixed ^ (mixed >>> 32)) & MASK);
|
||||
}
|
||||
|
||||
private record Entry(long key, Holder<Biome> value) {
|
||||
private record Entry(int runtimeIdentity, long key, Holder<Biome> value) {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StructureStateBiomeSource extends BiomeSource {
|
||||
private final IrisModdedBiomeSource delegate;
|
||||
private final Set<Holder<Biome>> possibleBiomes;
|
||||
private volatile BiomeHolderTable resolvedBiomes = new BiomeHolderTable();
|
||||
|
||||
private StructureStateBiomeSource(IrisModdedBiomeSource delegate, Set<Holder<Biome>> possibleBiomes) {
|
||||
this.delegate = delegate;
|
||||
this.possibleBiomes = possibleBiomes;
|
||||
}
|
||||
|
||||
private void clearCache() {
|
||||
resolvedBiomes = new BiomeHolderTable();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Structure state biome sources are not serializable");
|
||||
@@ -863,15 +924,7 @@ final class IrisModdedBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
long key = packNoiseKey(x, y, z);
|
||||
BiomeHolderTable cache = resolvedBiomes;
|
||||
Holder<Biome> cached = cache.get(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Holder<Biome> resolved = delegate.resolveRequiredStructureBiome(x, y, z);
|
||||
cache.put(key, resolved);
|
||||
return resolved;
|
||||
return delegate.resolveRequiredStructureBiome(x, y, z, sampler);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+449
-54
@@ -20,11 +20,17 @@ package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||
import art.arcane.iris.engine.framework.NativeFeatureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||
import art.arcane.iris.engine.history.GenerationHistory;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.nativegen.NativeStructureStartInjector;
|
||||
@@ -72,6 +78,7 @@ import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
@@ -84,16 +91,24 @@ import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
// Vanilla-shaped fallback for an unbound generator (matches IrisDimension defaults). getMinY,
|
||||
@@ -124,16 +139,20 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
private final ModdedSpawnTableMerger spawnTables = new ModdedSpawnTableMerger(this);
|
||||
private final ModdedImportedFeatureStage importedFeatures;
|
||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||
private final IntConsumer generationRuntimeRetirementListener = this::retireGenerationRuntimeCaches;
|
||||
private volatile boolean unloading;
|
||||
private volatile Engine engine;
|
||||
private volatile String activePack;
|
||||
private volatile String activeDimensionKey;
|
||||
private volatile Path immutablePackRoot;
|
||||
private volatile ModdedGenerationMode generationMode = ModdedGenerationMode.PERSISTENT_RESTORE;
|
||||
private volatile long seedOverride = Long.MIN_VALUE;
|
||||
private volatile long lastChunkGenAt = 0L;
|
||||
private volatile Set<String> configuredStructureBiomeKeys;
|
||||
private volatile ModdedDimensionMetadata.ConfiguredPack configuredPack;
|
||||
private volatile ModdedDimensionMetadata.DimensionMetadata heightMetadata;
|
||||
private volatile ServerLevel boundLevel;
|
||||
private volatile IrisEngine retirementListenerEngine;
|
||||
|
||||
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
|
||||
this(biomeSource, dimensionKey, new IrisModdedBiomeSource(biomeSource));
|
||||
@@ -166,12 +185,39 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
|
||||
repoint(
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed,
|
||||
null,
|
||||
ModdedGenerationMode.PERSISTENT_RESTORE
|
||||
);
|
||||
}
|
||||
|
||||
synchronized void repoint(String pack, String packDimensionKey, long seed, Path packRoot) {
|
||||
repoint(
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed,
|
||||
packRoot,
|
||||
ModdedGenerationMode.PERSISTENT_RESTORE
|
||||
);
|
||||
}
|
||||
|
||||
synchronized void repoint(
|
||||
String pack,
|
||||
String packDimensionKey,
|
||||
long seed,
|
||||
Path packRoot,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
this.generationMode = generationMode;
|
||||
repointAndBind(level, pack, packDimensionKey, seed);
|
||||
return;
|
||||
}
|
||||
applyUnboundConfiguration(pack, packDimensionKey, seed);
|
||||
applyUnboundConfiguration(pack, packDimensionKey, seed, packRoot, generationMode);
|
||||
}
|
||||
|
||||
synchronized void repointAndBind(ServerLevel level, String pack, String packDimensionKey, long seed) {
|
||||
@@ -181,7 +227,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
requireGlobalStructureGeneration(
|
||||
level.getServer().getWorldGenSettings().options().generateStructures(), dimensionKey);
|
||||
Engine replacement = ModdedWorldEngines.prepareReplacement(level, pack, packDimensionKey, seed);
|
||||
Engine replacement = ModdedWorldEngines.prepareReplacement(
|
||||
level,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed,
|
||||
generationMode
|
||||
);
|
||||
try {
|
||||
ModdedWorldEngines.installReplacement(level, replacement);
|
||||
replacement.getPlatformHooks().applyWorldBoundary(replacement);
|
||||
@@ -204,6 +256,8 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.activeDimensionKey = packDimensionKey;
|
||||
this.seedOverride = seed;
|
||||
this.engine = replacement;
|
||||
bindGenerationRuntimeRetirementListener(replacement);
|
||||
this.immutablePackRoot = replacement.getData().getDataFolder().toPath().toAbsolutePath().normalize();
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
this.configuredPack = null;
|
||||
this.heightMetadata = engineHeights(replacement);
|
||||
@@ -235,10 +289,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
private void clearEngineBinding() {
|
||||
unbindGenerationRuntimeRetirementListener();
|
||||
this.engine = null;
|
||||
this.boundLevel = null;
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
this.configuredPack = null;
|
||||
this.immutablePackRoot = null;
|
||||
this.engineBinding.reset();
|
||||
this.announced.set(false);
|
||||
this.structureBiomeSource.clearCaches();
|
||||
@@ -247,10 +303,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.spawnTables.resetVanillaSpawnBiomes();
|
||||
}
|
||||
|
||||
private void applyUnboundConfiguration(String pack, String packDimensionKey, long seed) {
|
||||
private void applyUnboundConfiguration(
|
||||
String pack,
|
||||
String packDimensionKey,
|
||||
long seed,
|
||||
Path packRoot,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
unbindGenerationRuntimeRetirementListener();
|
||||
this.activePack = pack;
|
||||
this.activeDimensionKey = packDimensionKey;
|
||||
this.seedOverride = seed;
|
||||
this.immutablePackRoot = packRoot == null ? null : packRoot.toAbsolutePath().normalize();
|
||||
this.generationMode = Objects.requireNonNull(generationMode, "generationMode");
|
||||
this.engine = null;
|
||||
this.boundLevel = null;
|
||||
this.configuredStructureBiomeKeys = null;
|
||||
@@ -265,7 +330,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
|
||||
public synchronized void resetToDefault() {
|
||||
repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE);
|
||||
repoint(
|
||||
defaultPack,
|
||||
defaultDimensionKey,
|
||||
Long.MIN_VALUE,
|
||||
null,
|
||||
ModdedGenerationMode.PERSISTENT_RESTORE
|
||||
);
|
||||
}
|
||||
|
||||
public String activePack() {
|
||||
@@ -305,7 +376,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
BlockPos pos, int radius,
|
||||
boolean findUnexplored) {
|
||||
Engine current = engine();
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
|
||||
int chunkX = Math.floorDiv(pos.getX(), 16);
|
||||
int chunkZ = Math.floorDiv(pos.getZ(), 16);
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
current, chunkX, chunkZ, "modded_structure_locate");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_structure_locate");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
HolderSet<Structure> reachable = nativeStructures.filterReachableNativeStructures(
|
||||
level, holders, current);
|
||||
@@ -474,14 +550,22 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
||||
Engine created = ModdedWorldEngines.get(
|
||||
level,
|
||||
activePack,
|
||||
activeDimensionKey,
|
||||
seedOverride,
|
||||
generationMode
|
||||
);
|
||||
requireCompletedShutdown(created);
|
||||
if (created.isClosed() || created.getComplex() == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey
|
||||
+ "' created an engine without a ready biome complex");
|
||||
}
|
||||
engine = created;
|
||||
bindGenerationRuntimeRetirementListener(created);
|
||||
boundLevel = level;
|
||||
immutablePackRoot = created.getData().getDataFolder().toPath().toAbsolutePath().normalize();
|
||||
heightMetadata = engineHeights(created);
|
||||
configuredStructureBiomeKeys = null;
|
||||
structureBiomeSource.clearCaches();
|
||||
@@ -642,10 +726,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
Engine current = engine;
|
||||
if (current != null && !current.isClosed() && !current.isClosing()) {
|
||||
Set<String> retained = retainedBiomeKeys(current);
|
||||
if (!retained.isEmpty()) {
|
||||
configuredStructureBiomeKeys = retained;
|
||||
return retained;
|
||||
}
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_configured_biome_keys");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
Set<String> resolved = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
||||
current.getAllBiomes(), current.getDimension().getLoadKey());
|
||||
current.getAllBiomes(),
|
||||
customBiome -> current.getData().customBiomeResourceKey(
|
||||
current.getDimension(),
|
||||
customBiome
|
||||
));
|
||||
configuredStructureBiomeKeys = resolved;
|
||||
return resolved;
|
||||
} catch (GenerationSessionException e) {
|
||||
@@ -662,6 +755,27 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
static Set<String> retainedBiomeKeys(Engine engine) {
|
||||
if (!(engine instanceof IrisEngine irisEngine)) {
|
||||
return Set.of();
|
||||
}
|
||||
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
|
||||
if (router == null) {
|
||||
return Set.of();
|
||||
}
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
Collection<GenerationEpoch> epochs = router.history().manifest().epochs();
|
||||
for (GenerationEpoch epoch : epochs) {
|
||||
GenerationRegistryContract contract = epoch.registryContract();
|
||||
for (GenerationRegistryContract.PhysicalResourceKey key : contract.definitions().keySet()) {
|
||||
if (GenerationRegistryContractFactory.BIOME_REGISTRY.equals(key.registryKey())) {
|
||||
keys.add(key.resourceKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Set.copyOf(keys);
|
||||
}
|
||||
|
||||
private static ModdedDimensionMetadata.DimensionMetadata engineHeights(Engine engine) {
|
||||
IrisDimension dimension = engine.getDimension();
|
||||
int minY = engine.getMinHeight();
|
||||
@@ -706,8 +820,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
if (configuredPack != null) {
|
||||
return configuredPack;
|
||||
}
|
||||
PackValidationRegistry.requireLoadable(activePack);
|
||||
File packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
|
||||
Path packRoot = immutablePackRoot;
|
||||
File packDirectory;
|
||||
if (packRoot == null) {
|
||||
PackValidationRegistry.requireLoadable(activePack);
|
||||
packDirectory = ModdedWorldEngines.resolvePack(activePack, activeDimensionKey);
|
||||
} else {
|
||||
PackValidationRegistry.requireLoadable(packRoot);
|
||||
packDirectory = packRoot.toFile();
|
||||
}
|
||||
IrisData data = IrisData.get(packDirectory);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(activeDimensionKey);
|
||||
if (dimension == null) {
|
||||
@@ -762,36 +883,54 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
|
||||
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns = biome.value().getMobSettings().getMobs(category);
|
||||
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
|
||||
biome, structureManager, category, pos);
|
||||
if (resolvedSpawns != explicitSpawns) {
|
||||
return resolvedSpawns;
|
||||
}
|
||||
Engine current = engine();
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
current, pos.getX(), pos.getZ(), "modded_mob_spawn_table");
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_mob_spawn_table");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns =
|
||||
biome.value().getMobSettings().getMobs(category);
|
||||
WeightedList<MobSpawnSettings.SpawnerData> resolvedSpawns = super.getMobsAt(
|
||||
biome, structureManager, category, pos);
|
||||
if (resolvedSpawns != explicitSpawns) {
|
||||
return resolvedSpawns;
|
||||
}
|
||||
|
||||
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(current, biome.value());
|
||||
if (vanillaSpawnBiome == null) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns = vanillaSpawnBiome.value().getMobSettings().getMobs(category);
|
||||
if (explicitSpawns.isEmpty()) {
|
||||
return vanillaSpawns;
|
||||
}
|
||||
if (vanillaSpawns.isEmpty()) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns =
|
||||
vanillaSpawnBiome.value().getMobSettings().getMobs(category);
|
||||
if (explicitSpawns.isEmpty()) {
|
||||
return vanillaSpawns;
|
||||
}
|
||||
if (vanillaSpawns.isEmpty()) {
|
||||
return explicitSpawns;
|
||||
}
|
||||
|
||||
return spawnTables.mergedSpawnTable(biome.value(), category, vanillaSpawns, explicitSpawns);
|
||||
return spawnTables.mergedSpawnTable(
|
||||
current,
|
||||
biome.value(),
|
||||
category,
|
||||
vanillaSpawns,
|
||||
explicitSpawns
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomState, Blender blender,
|
||||
StructureManager structureManager, ChunkAccess chunk) {
|
||||
Engine current = engine();
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes");
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
current, chunkPos.x(), chunkPos.z(), "modded_create_biomes");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
chunk.fillBiomesFromNoise(structureBiomeSource::getVisibleNoiseBiome, randomState.sampler());
|
||||
return CompletableFuture.completedFuture(chunk);
|
||||
@@ -806,19 +945,35 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
ModdedIrisLog.debug("Iris generating chunk {},{}", pos.x(), pos.z());
|
||||
|
||||
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
generationEngine, pos.x(), pos.z(), "modded_chunk_pipeline");
|
||||
|
||||
if (ModdedGenPool.parallelChunkSystem()) {
|
||||
return CompletableFuture.completedFuture(
|
||||
generateTerrain(chunk, generationEngine, pos, air));
|
||||
try {
|
||||
if (ModdedGenPool.parallelChunkSystem()) {
|
||||
try {
|
||||
return CompletableFuture.completedFuture(
|
||||
generateTerrain(chunk, generationEngine, pos, air, route));
|
||||
} finally {
|
||||
if (route != null) {
|
||||
route.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
CompletableFuture<ChunkAccess> pipeline = CompletableFuture.supplyAsync(
|
||||
() -> generateTerrain(chunk, generationEngine, pos, air, route),
|
||||
ModdedGenPool.pool());
|
||||
return closeRouteOnCompletion(pipeline, route);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
closeHistoryRoute(route, failure);
|
||||
throw failure;
|
||||
}
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> generateTerrain(chunk, generationEngine, pos, air),
|
||||
ModdedGenPool.pool());
|
||||
}
|
||||
|
||||
private ChunkAccess generateTerrain(ChunkAccess chunk, Engine generationEngine, ChunkPos pos,
|
||||
PlatformBlockState air) {
|
||||
try (GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
|
||||
PlatformBlockState air,
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route) {
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = generationEngine.acquireGenerationLease("modded_chunk_pipeline");
|
||||
IrisContext.Scope ignored = IrisContext.open(generationEngine, lease.sessionId(), null)) {
|
||||
if (announced.compareAndSet(false, true)) {
|
||||
ModdedIrisLog.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
|
||||
@@ -837,6 +992,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
Heightmap.Types.MOTION_BLOCKING,
|
||||
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES));
|
||||
ModdedWorldManager.enqueueGenerated(generationEngine, pos.x(), pos.z());
|
||||
if (route != null) {
|
||||
route.claimGeneratedSemantics();
|
||||
}
|
||||
return chunk;
|
||||
} catch (GenerationSessionException e) {
|
||||
if (generationEngine.isClosing() || e.isExpectedTeardown()) {
|
||||
@@ -852,6 +1010,67 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private static CompletableFuture<ChunkAccess> closeRouteOnCompletion(
|
||||
CompletableFuture<ChunkAccess> pipeline,
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route
|
||||
) {
|
||||
if (route == null) {
|
||||
return pipeline;
|
||||
}
|
||||
CompletableFuture<ChunkAccess> completion = new CompletableFuture<>();
|
||||
pipeline.whenComplete((ChunkAccess chunk, Throwable failure) -> {
|
||||
boolean cancelled = isCancellationFailure(failure);
|
||||
Throwable completionFailure = failure;
|
||||
try {
|
||||
route.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
completionFailure = appendFailure(completionFailure, closeFailure);
|
||||
}
|
||||
if (completionFailure == null) {
|
||||
completion.complete(chunk);
|
||||
} else if (cancelled) {
|
||||
completion.cancel(false);
|
||||
} else {
|
||||
completion.completeExceptionally(completionFailure);
|
||||
}
|
||||
});
|
||||
return completion;
|
||||
}
|
||||
|
||||
private static void closeHistoryRoute(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
Throwable failure
|
||||
) {
|
||||
if (route == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
route.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Throwable appendFailure(Throwable failure, Throwable closeFailure) {
|
||||
if (failure == null) {
|
||||
return closeFailure;
|
||||
}
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
private static boolean isCancellationFailure(Throwable failure) {
|
||||
Throwable current = failure;
|
||||
while (current instanceof CompletionException && current.getCause() != null) {
|
||||
current = current.getCause();
|
||||
}
|
||||
return current instanceof CancellationException;
|
||||
}
|
||||
|
||||
private void writeTerrainHeightmaps(ChunkAccess chunk, Engine generationEngine, ChunkPos pos, int height) {
|
||||
int baseX = pos.getMinBlockX();
|
||||
int baseZ = pos.getMinBlockZ();
|
||||
@@ -923,18 +1142,47 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion region, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk) {
|
||||
Engine current = engine(region.getLevel());
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
current, chunkPos.x(), chunkPos.z(), "modded_apply_carvers");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_apply_carvers");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion region, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) {
|
||||
Engine current = engine(region.getLevel());
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
current, chunkPos.x(), chunkPos.z(), "modded_build_surface");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_build_surface");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) {
|
||||
Engine current = engine();
|
||||
importedFeatures.prepare(current);
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
||||
Engine current = engine(level.getLevel());
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
current, chunkPos.x(), chunkPos.z(), "modded_biome_decoration");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_biome_decoration");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
if (!allowsRoutedDiscreteGeneration(
|
||||
current,
|
||||
route,
|
||||
chunkPos,
|
||||
chunk,
|
||||
ChunkStatus.FEATURES
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
importedFeatures.prepare(current);
|
||||
nativeStructures.placeVanillaStructures(level, chunk, structureManager);
|
||||
// Vanilla's placed-feature pass, on THIS thread and never on ModdedGenPool: the FEATURES chunk
|
||||
// step writes into the eight neighbouring chunks and is not parallel-safe. Inert unless the
|
||||
@@ -946,8 +1194,21 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
Engine current = engine(levelKey);
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
current, chunkPos.x(), chunkPos.z(), "modded_create_structures");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_structures");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
if (!allowsRoutedDiscreteGeneration(
|
||||
current,
|
||||
route,
|
||||
chunkPos,
|
||||
chunk,
|
||||
ChunkStatus.STRUCTURE_STARTS
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
Map<Structure, StructureStart> previousStarts = new HashMap<>(chunk.getAllStarts());
|
||||
super.createStructures(registryAccess, structureState, structureManager, chunk, templateManager, levelKey);
|
||||
Map<Structure, NativeStructureStartPlan> configuredStarts = NativeStructureStartInjector.inject(
|
||||
@@ -970,7 +1231,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
@Override
|
||||
public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) {
|
||||
Engine current = engine(level.getLevel());
|
||||
try (GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
current, chunkPos.x(), chunkPos.z(), "modded_create_references");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_create_references");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
NativeStructureReferenceRepair.createReferences(
|
||||
current, level, structureManager, chunk);
|
||||
@@ -983,15 +1248,32 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion region) {
|
||||
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||
ChunkPos center = region.getCenter();
|
||||
Holder<Biome> visibleBiome = 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);
|
||||
Engine current = engine(region.getLevel());
|
||||
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = openHistoryRoute(
|
||||
current, center.x(), center.z(), "modded_spawn_original_mobs");
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope runtimeScope = openHistoryRuntimeScope(route);
|
||||
GenerationSessionLease lease = requireGenerationLease(current, "modded_spawn_original_mobs");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
ChunkAccess centerChunk = region.getChunk(center.x(), center.z());
|
||||
if (!allowsRoutedDiscreteGeneration(
|
||||
current,
|
||||
route,
|
||||
center,
|
||||
centerChunk,
|
||||
ChunkStatus.SPAWN
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
Registry<Biome> registry = region.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
spawnTables.initializeVanillaSpawnBiomes(registry);
|
||||
Holder<Biome> visibleBiome = region.getBiome(center.getWorldPosition().atY(region.getMaxY()));
|
||||
Holder<Biome> vanillaBiome = spawnTables.vanillaSpawnBiome(current, 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
|
||||
@@ -1027,7 +1309,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor heightAccessor, RandomState randomState) {
|
||||
Engine current = requireDataQueryEngine("base height");
|
||||
boolean ignoreFluid = !type.isOpaque().test(Blocks.WATER.defaultBlockState());
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
current, x, z, "modded_base_height");
|
||||
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_height");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
return heightAccessor.getMinY() + current.getHeight(x, z, ignoreFluid) + 1;
|
||||
} catch (GenerationSessionException e) {
|
||||
@@ -1040,7 +1324,9 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
int minY = heightAccessor.getMinY();
|
||||
BlockState[] states = new BlockState[heightAccessor.getHeight()];
|
||||
Engine current = requireDataQueryEngine("base column");
|
||||
try (GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
||||
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
|
||||
current, x, z, "modded_base_column");
|
||||
GenerationSessionLease lease = current.acquireGenerationLease("modded_base_column");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
BlockState airState = Blocks.AIR.defaultBlockState();
|
||||
int surface = current.getHeight(x, z, true);
|
||||
@@ -1058,6 +1344,115 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean allowsRoutedDiscreteGeneration(
|
||||
Engine current,
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route,
|
||||
ChunkPos chunkPos,
|
||||
ChunkAccess chunk,
|
||||
ChunkStatus stage
|
||||
) {
|
||||
if (chunk.getPersistedStatus().isOrAfter(stage)) {
|
||||
return false;
|
||||
}
|
||||
if (route == null) {
|
||||
return allowsGenerationHistoryBypass(current);
|
||||
}
|
||||
return current.getComplex().allowsNewGenerationChunk(chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
|
||||
private void bindGenerationRuntimeRetirementListener(Engine current) {
|
||||
if (!(current instanceof IrisEngine irisEngine)) {
|
||||
unbindGenerationRuntimeRetirementListener();
|
||||
return;
|
||||
}
|
||||
IrisEngine previous = retirementListenerEngine;
|
||||
if (previous == irisEngine) {
|
||||
return;
|
||||
}
|
||||
if (previous != null) {
|
||||
previous.removeGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
|
||||
}
|
||||
irisEngine.addGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
|
||||
retirementListenerEngine = irisEngine;
|
||||
}
|
||||
|
||||
private void unbindGenerationRuntimeRetirementListener() {
|
||||
IrisEngine previous = retirementListenerEngine;
|
||||
if (previous == null) {
|
||||
return;
|
||||
}
|
||||
retirementListenerEngine = null;
|
||||
previous.removeGenerationRuntimeRetirementListener(generationRuntimeRetirementListener);
|
||||
}
|
||||
|
||||
private void retireGenerationRuntimeCaches(int runtimeIdentity) {
|
||||
structureBiomeSource.evictRuntime(runtimeIdentity);
|
||||
importedFeatures.evictRuntime(runtimeIdentity);
|
||||
spawnTables.evictRuntime(runtimeIdentity);
|
||||
}
|
||||
|
||||
private GenerationHistoryRuntimeRouter.RuntimeRoute openHistoryRoute(
|
||||
Engine current,
|
||||
int chunkX,
|
||||
int chunkZ,
|
||||
String operation
|
||||
) {
|
||||
if (allowsGenerationHistoryBypass(current)) {
|
||||
return null;
|
||||
}
|
||||
GenerationHistoryRuntimeRouter router = requireHistoryRouter(current, operation);
|
||||
try {
|
||||
return router.openRoute(chunkX, chunkZ);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation + " could not route chunk "
|
||||
+ chunkX + "," + chunkZ + " through generation history.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationHistoryRuntimeRouter.CoordinateScope openHistoryCoordinateScope(
|
||||
Engine current,
|
||||
int blockX,
|
||||
int blockZ,
|
||||
String operation
|
||||
) {
|
||||
if (allowsGenerationHistoryBypass(current)) {
|
||||
return null;
|
||||
}
|
||||
GenerationHistoryRuntimeRouter router = requireHistoryRouter(current, operation);
|
||||
try {
|
||||
return router.openCoordinateScope(blockX, blockZ);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris " + operation + " could not route block "
|
||||
+ blockX + "," + blockZ + " through generation history.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationHistoryRuntimeRouter requireHistoryRouter(Engine current, String operation) {
|
||||
if (!(current instanceof IrisEngine irisEngine)) {
|
||||
throw new IllegalStateException("Iris " + operation + " requires an IrisEngine runtime.");
|
||||
}
|
||||
return irisEngine.getGenerationHistoryRuntimeRouter().orElseThrow(() ->
|
||||
new IllegalStateException("Iris " + operation
|
||||
+ " requires an attached generation-history runtime router."));
|
||||
}
|
||||
|
||||
boolean allowsGenerationHistoryBypass(Engine current) {
|
||||
if (generationMode != ModdedGenerationMode.TRANSIENT_STUDIO) {
|
||||
return false;
|
||||
}
|
||||
if (current instanceof IrisEngine irisEngine && irisEngine.isStudio()) {
|
||||
return true;
|
||||
}
|
||||
throw new IllegalStateException("Only an explicitly transient Iris Studio engine may bypass "
|
||||
+ "generation-history routing.");
|
||||
}
|
||||
|
||||
private static GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope openHistoryRuntimeScope(
|
||||
GenerationHistoryRuntimeRouter.RuntimeRoute route
|
||||
) {
|
||||
return route == null ? null : route.openRuntimeScope();
|
||||
}
|
||||
|
||||
GenerationSessionLease requireGenerationLease(Engine current, String operation) {
|
||||
try {
|
||||
return current.acquireGenerationLease(operation);
|
||||
|
||||
@@ -142,6 +142,19 @@ public final class MainWorldService {
|
||||
}
|
||||
}
|
||||
|
||||
static Path configuredWorldRootIfPresent() throws IOException {
|
||||
Path instanceRoot = Path.of("").toAbsolutePath().normalize();
|
||||
Path properties = instanceRoot.resolve(PROPERTIES_NAME);
|
||||
if (!Files.isRegularFile(properties)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return resolveWorldRoot(instanceRoot, properties);
|
||||
} catch (MissingWorldRootException missing) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* net.minecraft.server.Main reads server.properties as Paths.get("server.properties"), so the authoritative
|
||||
* instance root is the JVM working directory - not configDir().getParent(), which points somewhere else
|
||||
|
||||
@@ -97,11 +97,7 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
return key;
|
||||
}
|
||||
Engine engine = context.getEngine();
|
||||
String prefix = engine.getDimension().getLoadKey() + ":";
|
||||
if (!key.regionMatches(true, 0, prefix, 0, prefix.length())) {
|
||||
return key;
|
||||
}
|
||||
return ModdedWorldgenIds.biomeRef(engine, key.substring(prefix.length()));
|
||||
return engine.getData().physicalBiomeResourceKey(engine.getDimension(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+279
-53
@@ -19,7 +19,9 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.history.GenerationActivation;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
@@ -47,9 +49,12 @@ import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.level.storage.ServerLevelData;
|
||||
import net.minecraft.world.level.storage.WorldData;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -120,14 +125,76 @@ public final class ModdedDimensionManager {
|
||||
return generator.commandEngine();
|
||||
}
|
||||
|
||||
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||
ModdedStartup.requirePackForWorldCreation(pack);
|
||||
public static Handle createTransientStudio(
|
||||
MinecraftServer server,
|
||||
String dimensionId,
|
||||
String pack,
|
||||
String packDimensionKey,
|
||||
long seed
|
||||
) {
|
||||
return create(
|
||||
server,
|
||||
dimensionId,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed,
|
||||
ModdedGenerationMode.TRANSIENT_STUDIO
|
||||
);
|
||||
}
|
||||
|
||||
static Handle restorePersistent(
|
||||
MinecraftServer server,
|
||||
String dimensionId,
|
||||
String pack,
|
||||
String packDimensionKey,
|
||||
long seed
|
||||
) {
|
||||
return create(
|
||||
server,
|
||||
dimensionId,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed,
|
||||
ModdedGenerationMode.PERSISTENT_RESTORE
|
||||
);
|
||||
}
|
||||
|
||||
private static Handle create(
|
||||
MinecraftServer server,
|
||||
String dimensionId,
|
||||
String pack,
|
||||
String packDimensionKey,
|
||||
long seed,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
ModdedServerAccess serverAccess = requireAccess();
|
||||
synchronized (LOCK) {
|
||||
ResourceKey<Level> key = levelKey(dimensionId);
|
||||
RuntimePack runtimePack = switch (generationMode) {
|
||||
case TRANSIENT_STUDIO -> transientStudioPack(pack, packDimensionKey);
|
||||
case PERSISTENT_CREATE -> persistentRuntimePack(
|
||||
ModdedGenerationHistoryStorage.createOrStage(
|
||||
server,
|
||||
key,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed
|
||||
),
|
||||
generationMode
|
||||
);
|
||||
case PERSISTENT_RESTORE -> persistentRuntimePack(
|
||||
ModdedGenerationHistoryStorage.restoreOrAdopt(
|
||||
server,
|
||||
key,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed
|
||||
),
|
||||
generationMode
|
||||
);
|
||||
};
|
||||
Handle existing = HANDLES.get(dimensionId);
|
||||
if (existing != null && serverAccess.hasLevel(server, key)) {
|
||||
existing.generator().repointAndBind(existing.level(), pack, packDimensionKey, seed);
|
||||
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
|
||||
HANDLES.put(dimensionId, refreshed);
|
||||
return refreshed;
|
||||
@@ -138,14 +205,22 @@ public final class ModdedDimensionManager {
|
||||
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
|
||||
}
|
||||
ModdedIrisLog.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
|
||||
generator.repointAndBind(present, pack, packDimensionKey, seed);
|
||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
try {
|
||||
Handle handle = inject(server, serverAccess, dimensionId, key, pack, packDimensionKey, seed);
|
||||
Handle handle = inject(
|
||||
server,
|
||||
serverAccess,
|
||||
dimensionId,
|
||||
key,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed,
|
||||
runtimePack
|
||||
);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
ModdedIrisLog.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
|
||||
return handle;
|
||||
@@ -157,28 +232,109 @@ public final class ModdedDimensionManager {
|
||||
}
|
||||
|
||||
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||
ModdedDimensionRegistryStore.PersistentDimension previous =
|
||||
ModdedDimensionRegistryStore.get(server, dimensionId);
|
||||
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed));
|
||||
try {
|
||||
return create(server, dimensionId, pack, packDimensionKey, seed);
|
||||
} catch (Throwable e) {
|
||||
synchronized (LOCK) {
|
||||
ModdedDimensionRegistryStore.PersistentDimension previous =
|
||||
ModdedDimensionRegistryStore.get(server, dimensionId);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Iris dimension '" + dimensionId
|
||||
+ "' already exists. Use the Iris world update command to stage a pack update.");
|
||||
}
|
||||
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(
|
||||
dimensionId,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed
|
||||
));
|
||||
try {
|
||||
if (previous == null) {
|
||||
return create(
|
||||
server,
|
||||
dimensionId,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
seed,
|
||||
ModdedGenerationMode.PERSISTENT_CREATE
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
try {
|
||||
ModdedDimensionRegistryStore.remove(server, dimensionId);
|
||||
} else {
|
||||
ModdedDimensionRegistryStore.put(server, previous);
|
||||
} catch (Throwable rollbackFailure) {
|
||||
e.addSuppressed(rollbackFailure);
|
||||
}
|
||||
} catch (Throwable rollbackFailure) {
|
||||
e.addSuppressed(rollbackFailure);
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (e instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
|
||||
}
|
||||
if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
}
|
||||
|
||||
public static UpdateResult stagePersistentUpdate(
|
||||
MinecraftServer server,
|
||||
String dimensionId,
|
||||
String pack,
|
||||
String packDimensionKey
|
||||
) {
|
||||
synchronized (LOCK) {
|
||||
ModdedDimensionRegistryStore.PersistentDimension previous =
|
||||
ModdedDimensionRegistryStore.get(server, dimensionId);
|
||||
if (previous == null) {
|
||||
throw new IllegalStateException("Iris dimension '" + dimensionId
|
||||
+ "' is not registered. Create it before staging an update.");
|
||||
}
|
||||
if (e instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
ModdedDimensionRegistryStore.PersistentDimension updated =
|
||||
new ModdedDimensionRegistryStore.PersistentDimension(
|
||||
dimensionId,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
previous.seed()
|
||||
);
|
||||
ModdedDimensionRegistryStore.put(server, updated);
|
||||
try {
|
||||
ModdedGenerationHistoryStorage.ActivePack active =
|
||||
ModdedGenerationHistoryStorage.createOrStage(
|
||||
server,
|
||||
levelKey(dimensionId),
|
||||
pack,
|
||||
packDimensionKey,
|
||||
previous.seed()
|
||||
);
|
||||
Optional<GenerationActivation> pending = active.history().pendingActivation();
|
||||
long activeActivationId = active.history().activeActivation().activationId();
|
||||
OptionalLong pendingActivationId = pending.isPresent()
|
||||
? OptionalLong.of(pending.orElseThrow().activationId())
|
||||
: OptionalLong.empty();
|
||||
UpdateResult result = new UpdateResult(
|
||||
pending.isPresent(),
|
||||
pending.isPresent(),
|
||||
activeActivationId,
|
||||
pendingActivationId
|
||||
);
|
||||
if (result.restartRequired()) {
|
||||
ModdedIrisLog.warn("Iris dimension '{}' staged generation activation {} from pack={}:{}. Restart the server to activate it.",
|
||||
dimensionId, pendingActivationId.orElseThrow(), pack, packDimensionKey);
|
||||
} else {
|
||||
ModdedIrisLog.info("Iris dimension '{}' already uses pack={}:{}; no generation update was staged.",
|
||||
dimensionId, pack, packDimensionKey);
|
||||
}
|
||||
return result;
|
||||
} catch (Throwable failure) {
|
||||
try {
|
||||
ModdedDimensionRegistryStore.put(server, previous);
|
||||
} catch (Throwable rollbackFailure) {
|
||||
failure.addSuppressed(rollbackFailure);
|
||||
}
|
||||
if (failure instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (failure instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris generation update staging failed for "
|
||||
+ dimensionId + ".", failure);
|
||||
}
|
||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,48 +680,47 @@ public final class ModdedDimensionManager {
|
||||
server.execute(task);
|
||||
}
|
||||
|
||||
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
|
||||
private static Holder<DimensionType> resolveDimensionType(
|
||||
RegistryAccess registryAccess,
|
||||
String pack,
|
||||
String packDimensionKey,
|
||||
RuntimePack runtimePack
|
||||
) {
|
||||
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
|
||||
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
|
||||
String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey);
|
||||
String typeRef = runtimePack.dimensionTypeKey();
|
||||
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
|
||||
ModdedRuntimeRegistry.ensureCustomBiomes(registryAccess, dimension, pack);
|
||||
ModdedRuntimeRegistry.ensureDimensionType(registry, typeKey, typeRef);
|
||||
return ModdedForcedDatapack.requireRegisteredDimensionType(
|
||||
typeRef, registry.get(typeKey), pack, packDimensionKey);
|
||||
}
|
||||
|
||||
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
|
||||
try {
|
||||
File packFolder = ModdedWorldEngines.packFolder(pack);
|
||||
if (!packFolder.isDirectory()) {
|
||||
throw new IllegalStateException("Iris pack directory is missing: " + packFolder.getAbsolutePath());
|
||||
}
|
||||
IrisDimension dimension = IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris pack '" + pack
|
||||
+ "' does not contain dimension '" + packDimensionKey + "'");
|
||||
}
|
||||
return dimension;
|
||||
} catch (Throwable e) {
|
||||
ModdedIrisLog.error("Iris could not load pack '{}' dimension '{}' for dimension type resolution",
|
||||
pack, packDimensionKey, e);
|
||||
if (e instanceof Error fatalError) {
|
||||
throw fatalError;
|
||||
}
|
||||
throw new IllegalStateException("Iris could not load pack '" + pack + "' dimension '"
|
||||
+ packDimensionKey + "' for dimension type resolution", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String pack, String packDimensionKey, long seed) {
|
||||
private static Handle inject(
|
||||
MinecraftServer server,
|
||||
ModdedServerAccess serverAccess,
|
||||
String dimensionId,
|
||||
ResourceKey<Level> key,
|
||||
String pack,
|
||||
String packDimensionKey,
|
||||
long seed,
|
||||
RuntimePack runtimePack
|
||||
) {
|
||||
RegistryAccess registryAccess = server.registryAccess();
|
||||
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess, pack, packDimensionKey);
|
||||
Holder<DimensionType> dimensionType = resolveDimensionType(
|
||||
registryAccess,
|
||||
pack,
|
||||
packDimensionKey,
|
||||
runtimePack
|
||||
);
|
||||
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
|
||||
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
|
||||
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
|
||||
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef);
|
||||
generator.repoint(pack, packDimensionKey, seed);
|
||||
generator.repoint(
|
||||
pack,
|
||||
runtimePack.dimensionKey(),
|
||||
seed,
|
||||
runtimePack.packRoot(),
|
||||
runtimePack.generationMode()
|
||||
);
|
||||
LevelStem stem = new LevelStem(dimensionType, generator);
|
||||
|
||||
WorldData worldData = server.getWorldData();
|
||||
@@ -681,6 +836,77 @@ public final class ModdedDimensionManager {
|
||||
return bound;
|
||||
}
|
||||
|
||||
private static RuntimePack persistentRuntimePack(
|
||||
ModdedGenerationHistoryStorage.ActivePack active,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
return new RuntimePack(
|
||||
active.packRoot(),
|
||||
active.dimensionKey(),
|
||||
active.dimensionContract().dimensionTypeKey(),
|
||||
generationMode
|
||||
);
|
||||
}
|
||||
|
||||
private static RuntimePack transientStudioPack(String pack, String dimensionKey) {
|
||||
ModdedStartup.requirePackForWorldCreation(pack);
|
||||
Path packRoot = ModdedWorldEngines.resolvePack(pack, dimensionKey)
|
||||
.toPath()
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
PackValidationRegistry.requireLoadable(packRoot);
|
||||
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
|
||||
try {
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey, false);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Transient Iris Studio pack does not contain dimension '"
|
||||
+ dimensionKey + "'.");
|
||||
}
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
return new RuntimePack(
|
||||
packRoot,
|
||||
dimensionKey,
|
||||
ModdedWorldgenIds.dimensionTypeRef(pack, dimensionKey),
|
||||
ModdedGenerationMode.TRANSIENT_STUDIO
|
||||
);
|
||||
}
|
||||
|
||||
private record RuntimePack(
|
||||
Path packRoot,
|
||||
String dimensionKey,
|
||||
String dimensionTypeKey,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
private RuntimePack {
|
||||
packRoot = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
|
||||
dimensionKey = Objects.requireNonNull(dimensionKey, "dimensionKey");
|
||||
dimensionTypeKey = Objects.requireNonNull(dimensionTypeKey, "dimensionTypeKey");
|
||||
generationMode = Objects.requireNonNull(generationMode, "generationMode");
|
||||
}
|
||||
}
|
||||
|
||||
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
|
||||
}
|
||||
|
||||
public record UpdateResult(
|
||||
boolean pending,
|
||||
boolean restartRequired,
|
||||
long activeActivationId,
|
||||
OptionalLong pendingActivationId
|
||||
) {
|
||||
public UpdateResult {
|
||||
pendingActivationId = Objects.requireNonNull(
|
||||
pendingActivationId,
|
||||
"pendingActivationId"
|
||||
);
|
||||
if (pending != pendingActivationId.isPresent()) {
|
||||
throw new IllegalArgumentException("Pending update status must match the pending activation ID.");
|
||||
}
|
||||
if (restartRequired != pending) {
|
||||
throw new IllegalArgumentException("A pending generation activation requires a restart.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -28,6 +28,7 @@ import net.minecraft.resources.Identifier;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class ModdedDimensionMetadata {
|
||||
private ModdedDimensionMetadata() {
|
||||
@@ -45,7 +46,10 @@ final class ModdedDimensionMetadata {
|
||||
|
||||
static Set<String> collectConfiguredBiomeKeys(IrisDimension dimension, IrisData data) {
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>(
|
||||
collectConfiguredBiomeKeys(dimension.getReachableBiomes(() -> data), dimension.getLoadKey()));
|
||||
collectConfiguredBiomeKeys(
|
||||
dimension.getReachableBiomes(() -> data),
|
||||
customBiome -> data.customBiomeResourceKey(dimension, customBiome)
|
||||
));
|
||||
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
|
||||
if (region == null) {
|
||||
continue;
|
||||
@@ -60,9 +64,11 @@ final class ModdedDimensionMetadata {
|
||||
return Set.copyOf(keys);
|
||||
}
|
||||
|
||||
static Set<String> collectConfiguredBiomeKeys(Iterable<IrisBiome> biomes, String dimensionLoadKey) {
|
||||
static Set<String> collectConfiguredBiomeKeys(
|
||||
Iterable<IrisBiome> biomes,
|
||||
Function<IrisBiomeCustom, String> customBiomeKey
|
||||
) {
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
String namespace = dimensionLoadKey.toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : biomes) {
|
||||
if (irisBiome == null) {
|
||||
continue;
|
||||
@@ -75,7 +81,7 @@ final class ModdedDimensionMetadata {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
keys.add(namespace + ":" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
keys.add(customBiomeKey.apply(customBiome).toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
return Set.copyOf(keys);
|
||||
|
||||
+9
-1
@@ -53,6 +53,10 @@ public final class ModdedDimensionRegistryStore {
|
||||
return contents(file).dimensions();
|
||||
}
|
||||
|
||||
static List<PersistentDimension> loadWorldRoot(Path worldRoot) {
|
||||
return load(storeFile(worldRoot));
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot-safe load: a corrupt registry must not abort server start. The broken file is renamed aside so the
|
||||
* next write starts clean, and every id we can still recognise in the raw text is reported as lost.
|
||||
@@ -232,7 +236,11 @@ public final class ModdedDimensionRegistryStore {
|
||||
}
|
||||
|
||||
private static Path storeFile(MinecraftServer server) {
|
||||
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
|
||||
return storeFile(server.getWorldPath(LevelResource.ROOT));
|
||||
}
|
||||
|
||||
private static Path storeFile(Path worldRoot) {
|
||||
return worldRoot.resolve("iris").resolve(FILE_NAME);
|
||||
}
|
||||
|
||||
public record PersistentDimension(String id, String pack, String dimension, long seed) {
|
||||
|
||||
+3
-1
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.IrisEngineMantle;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryPaths;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.Level;
|
||||
@@ -38,7 +39,8 @@ public final class ModdedDimensionStorage {
|
||||
"region",
|
||||
"entities",
|
||||
"poi",
|
||||
IrisEngineMantle.STORAGE_FOLDER_NAME
|
||||
IrisEngineMantle.STORAGE_FOLDER_NAME,
|
||||
GenerationHistoryPaths.IRIS_DIRECTORY_NAME
|
||||
);
|
||||
|
||||
private ModdedDimensionStorage() {
|
||||
|
||||
+365
-91
@@ -18,7 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.core.IrisDatapackCompiler;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.localization.IrisLanguage;
|
||||
import art.arcane.iris.core.localization.RuntimeUiMessages;
|
||||
@@ -27,11 +27,25 @@ import art.arcane.iris.core.nms.datapack.IDataFixer;
|
||||
import art.arcane.iris.core.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.core.pack.PackValidationResult;
|
||||
import art.arcane.iris.core.pack.PackValidator;
|
||||
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||
import art.arcane.iris.engine.history.GenerationHistory;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryPaths;
|
||||
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||
import art.arcane.iris.engine.history.GenerationRegistryContract.PhysicalResourceKey;
|
||||
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.iris.engine.object.IrisCustomBiomeAliasResolver;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionType;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.packs.PackLocationInfo;
|
||||
import net.minecraft.server.packs.PackSelectionConfig;
|
||||
import net.minecraft.server.packs.PackType;
|
||||
@@ -39,16 +53,16 @@ import net.minecraft.server.packs.PathPackResources;
|
||||
import net.minecraft.server.packs.repository.Pack;
|
||||
import net.minecraft.server.packs.repository.PackSource;
|
||||
import net.minecraft.server.packs.repository.RepositorySource;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
@@ -56,9 +70,12 @@ import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
@@ -68,14 +85,10 @@ public final class ModdedForcedDatapack {
|
||||
private static final String PACK_ID = "iris_worldgen";
|
||||
private static final String PACK_FOLDER = "iris";
|
||||
private static final String HASH_FILE_NAME = "packs.hash";
|
||||
// Bump this whenever the emitted datapack content changes for reasons the pack-directory hash cannot see.
|
||||
// v2: custom biomes now inherit their vanilla derivative's biome tags, so every already-published pack has
|
||||
// to regenerate once.
|
||||
private static final String HASH_SALT = "iris-forced-datapack-v2";
|
||||
private static final String HASH_SALT = "iris-forced-datapack-v3-content-addressed-biomes";
|
||||
private static final long PACKS_HASH_TTL_NANOS = 2_000_000_000L;
|
||||
private static final Object LOCK = new Object();
|
||||
private static final AtomicBoolean LOADED = new AtomicBoolean(false);
|
||||
private static final AtomicBoolean STALE_SERVE_LOGGED = new AtomicBoolean(false);
|
||||
private static volatile PublishedState published;
|
||||
private static volatile HashMemo packsHashMemo;
|
||||
|
||||
@@ -120,11 +133,9 @@ public final class ModdedForcedDatapack {
|
||||
reason = "no published pack";
|
||||
} else if (!hash.isEmpty() && !state.packsHash().equals(hash)) {
|
||||
reason = "stale cache (hash changed)";
|
||||
} else if (hash.isEmpty()) {
|
||||
reason = "current registry inputs could not be verified";
|
||||
} else {
|
||||
if (hash.isEmpty() && STALE_SERVE_LOGGED.compareAndSet(false, true)) {
|
||||
ModdedIrisLog.warn("Iris cannot hash the installed packs; serving the last generated forced datapack from {} unverified",
|
||||
state.directory());
|
||||
}
|
||||
try {
|
||||
return requireReadablePack(state.directory());
|
||||
} catch (RuntimeException unreadable) {
|
||||
@@ -164,17 +175,7 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
|
||||
private static Pack buildPack() {
|
||||
try {
|
||||
return requireReadablePack(regenerate());
|
||||
} catch (RuntimeException | Error generationFailure) {
|
||||
Path lastKnownGood = packDirectory();
|
||||
if (Files.isRegularFile(lastKnownGood.resolve("pack.mcmeta"))) {
|
||||
ModdedIrisLog.error("Iris kept the last known-good generated datapack after regeneration failed",
|
||||
generationFailure);
|
||||
return requireReadablePack(lastKnownGood);
|
||||
}
|
||||
throw generationFailure;
|
||||
}
|
||||
return requireReadablePack(regenerate());
|
||||
}
|
||||
|
||||
private static Pack requireReadablePack(Path directory) {
|
||||
@@ -264,7 +265,6 @@ public final class ModdedForcedDatapack {
|
||||
// Publish the hash this run was built from as the memo too: a memo captured before staging would
|
||||
// otherwise mismatch what was just published and send the next serve straight back into buildPack.
|
||||
packsHashMemo = new HashMemo(packsHash, System.nanoTime());
|
||||
STALE_SERVE_LOGGED.set(false);
|
||||
return packDirectory();
|
||||
} catch (IOException | RuntimeException | Error failure) {
|
||||
try {
|
||||
@@ -336,10 +336,6 @@ public final class ModdedForcedDatapack {
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content hash over the installed packs: relative path, size and mtime of every regular file, plus the
|
||||
* pack format and loader the generated datapack is shaped for.
|
||||
*/
|
||||
private static String packsHash() throws IOException {
|
||||
MessageDigest digest;
|
||||
try {
|
||||
@@ -349,39 +345,13 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
digest.update((HASH_SALT + '|' + ModdedEngineBootstrap.loader().platformName()
|
||||
+ '|' + DataVersion.getLatest().getPackFormat() + '\n').getBytes(StandardCharsets.UTF_8));
|
||||
Path root = packsRoot();
|
||||
if (Files.isDirectory(root)) {
|
||||
List<String> entries = new ArrayList<>();
|
||||
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) {
|
||||
return !directory.equals(root)
|
||||
&& PackDirectoryResolver.isHiddenName(directory.getFileName().toString())
|
||||
? FileVisitResult.SKIP_SUBTREE
|
||||
: FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
|
||||
if (attributes.isRegularFile()) {
|
||||
entries.add(root.relativize(file).toString().replace('\\', '/')
|
||||
+ '|' + attributes.size() + '|' + attributes.lastModifiedTime().toMillis());
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException failure) {
|
||||
entries.add(root.relativize(file).toString().replace('\\', '/') + "|unreadable");
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
entries.sort(Comparator.naturalOrder());
|
||||
for (String entry : entries) {
|
||||
digest.update(entry.getBytes(StandardCharsets.UTF_8));
|
||||
digest.update((byte) '\n');
|
||||
}
|
||||
}
|
||||
String inputFingerprint = IrisDatapackCompiler.computeInputFingerprint(
|
||||
compilationPackRoots(),
|
||||
List.of(),
|
||||
DataVersion.getLatest().get(),
|
||||
false
|
||||
);
|
||||
digest.update(inputFingerprint.getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
|
||||
@@ -391,10 +361,11 @@ public final class ModdedForcedDatapack {
|
||||
|
||||
int packCount = 0;
|
||||
KList<String> presetIds = new KList<>();
|
||||
File root = packsRoot().toFile();
|
||||
List<File> packs = PackDirectoryResolver.listVisiblePackDirectoriesOrThrow(root);
|
||||
for (File pack : packs) {
|
||||
if (stagePack(pack, fixer, stagingDirectory, seenBiomes, presetIds)) {
|
||||
Path worldRoot = worldRootOrNull();
|
||||
Set<PackSelection> legacySelections = unadoptedSelections(worldRoot);
|
||||
for (File pack : compilationPackRoots(worldRoot)) {
|
||||
PackStageContext context = packStageContext(pack.toPath(), legacySelections);
|
||||
if (stagePack(context, fixer, stagingDirectory, seenBiomes, presetIds)) {
|
||||
packCount++;
|
||||
}
|
||||
}
|
||||
@@ -415,19 +386,28 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean stagePack(File sourcePack, IDataFixer fixer, Path stagingDirectory,
|
||||
private static boolean stagePack(PackStageContext context, IDataFixer fixer, Path stagingDirectory,
|
||||
Map<String, KSet<String>> seenBiomes,
|
||||
KList<String> presetIds) throws IOException {
|
||||
File sourcePack = context.packRoot().toFile();
|
||||
PackValidationResult validation;
|
||||
try {
|
||||
validation = PackValidator.validateForDatapackBootstrap(sourcePack);
|
||||
} catch (Throwable validationFailure) {
|
||||
if (context.required()) {
|
||||
throw requiredPackFailure(context, "validation", validationFailure);
|
||||
}
|
||||
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because validation failed",
|
||||
sourcePack.getName(), validationFailure);
|
||||
rethrowIfUnrecoverable(validationFailure);
|
||||
return false;
|
||||
}
|
||||
if (!validation.isLoadable()) {
|
||||
if (context.required()) {
|
||||
throw new IOException("Retained Iris generation pack at " + context.packRoot()
|
||||
+ " has " + validation.getBlockingErrors().size()
|
||||
+ " blocking validation error(s): " + validation.getBlockingErrors().getFirst());
|
||||
}
|
||||
ModdedIrisLog.error("Iris excluded pack '{}' from Create World: {} blocking validation error(s); first error: {}",
|
||||
sourcePack.getName(), validation.getBlockingErrors().size(),
|
||||
validation.getBlockingErrors().getFirst());
|
||||
@@ -442,8 +422,11 @@ public final class ModdedForcedDatapack {
|
||||
packFolders.add(packStagingDirectory.toFile());
|
||||
boolean installed;
|
||||
try {
|
||||
installed = installPack(sourcePack, fixer, packFolders, packBiomes, packPresetIds);
|
||||
installed = installPack(context, fixer, packFolders, packBiomes, packPresetIds);
|
||||
} catch (Throwable installationFailure) {
|
||||
if (context.required()) {
|
||||
throw requiredPackFailure(context, "datapack serialization", installationFailure);
|
||||
}
|
||||
ModdedIrisLog.error("Iris excluded pack '{}' from Create World because datapack serialization failed",
|
||||
sourcePack.getName(), installationFailure);
|
||||
rethrowIfUnrecoverable(installationFailure);
|
||||
@@ -482,7 +465,7 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
}
|
||||
|
||||
private static void mergeDirectory(Path sourceDirectory, Path destinationDirectory) throws IOException {
|
||||
static void mergeDirectory(Path sourceDirectory, Path destinationDirectory) throws IOException {
|
||||
List<Path> entries = new ArrayList<>();
|
||||
try (Stream<Path> walk = Files.walk(sourceDirectory)) {
|
||||
walk.sorted(Comparator.comparingInt(Path::getNameCount)).forEach(entries::add);
|
||||
@@ -491,9 +474,14 @@ public final class ModdedForcedDatapack {
|
||||
Path relative = sourceDirectory.relativize(source);
|
||||
Path destination = destinationDirectory.resolve(relative);
|
||||
if (Files.isDirectory(source)) {
|
||||
if (Files.exists(destination) && !Files.isDirectory(destination)) {
|
||||
throw new IOException("Conflicting Iris datapack output at " + destination + ".");
|
||||
}
|
||||
Files.createDirectories(destination);
|
||||
} else if (!Files.exists(destination)) {
|
||||
Files.copy(source, destination);
|
||||
} else if (!Files.isRegularFile(destination) || Files.mismatch(source, destination) != -1L) {
|
||||
throw new IOException("Conflicting Iris datapack output at " + destination + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -506,10 +494,11 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders,
|
||||
private static boolean installPack(PackStageContext context, IDataFixer fixer, KList<File> folders,
|
||||
Map<String, KSet<String>> seenBiomes,
|
||||
KList<String> presetIds) throws IOException {
|
||||
String packName = packFolder.getName();
|
||||
File packFolder = context.packRoot().toFile();
|
||||
String packName = context.packName();
|
||||
File dimensionsDirectory = new File(packFolder, "dimensions");
|
||||
if (!dimensionsDirectory.isDirectory()) {
|
||||
return false;
|
||||
@@ -523,25 +512,55 @@ public final class ModdedForcedDatapack {
|
||||
return false;
|
||||
}
|
||||
Arrays.sort(dimensionFiles, Comparator.comparing(File::getName));
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
|
||||
+ dimensionKey + "' did not load while building the forced datapack");
|
||||
IrisData data = IrisData.openDatapackCompiler(packFolder);
|
||||
try {
|
||||
if (context.retainedContract() != null) {
|
||||
data.bindGenerationRegistryContract(context.retainedContract());
|
||||
}
|
||||
String biomePathPrefix = ModdedWorldgenIds.biomePathPrefix(packName, dimensionKey);
|
||||
dimension.installBiomes(fixer, () -> data, folders, "irisworldgen", biomePathPrefix,
|
||||
biomesForNamespace(seenBiomes, biomePathPrefix));
|
||||
dimension.installBiomes(fixer, () -> data, folders,
|
||||
biomesForNamespace(seenBiomes, dimension.getLoadKey()));
|
||||
writeDimensionType(folders, fixer, dimension, packName, dimensionKey);
|
||||
String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey);
|
||||
writeWorldPreset(folders, packName, dimensionKey, presetRef);
|
||||
presetIds.add(presetRef);
|
||||
int installedDimensions = 0;
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
String dimensionKey = dimensionFile.getName().substring(
|
||||
0,
|
||||
dimensionFile.getName().length() - ".json".length()
|
||||
);
|
||||
if (context.dimensionKey() != null && !context.dimensionKey().equals(dimensionKey)) {
|
||||
continue;
|
||||
}
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Iris pack '" + packName + "' dimension '"
|
||||
+ dimensionKey + "' did not load while building the forced datapack");
|
||||
}
|
||||
dimension.installBiomes(
|
||||
fixer,
|
||||
() -> data,
|
||||
folders,
|
||||
biomesForNamespace(seenBiomes, "iris"),
|
||||
context
|
||||
);
|
||||
writeDimensionType(
|
||||
folders,
|
||||
fixer,
|
||||
dimension,
|
||||
context.dimensionTypeRef(dimensionKey),
|
||||
context.emitLegacyDimensionTypeAlias(),
|
||||
context
|
||||
);
|
||||
if (context.emitPresets()) {
|
||||
String presetRef = ModdedWorldgenIds.presetRef(packName, dimensionKey);
|
||||
writeWorldPreset(folders, packName, dimensionKey, presetRef);
|
||||
presetIds.add(presetRef);
|
||||
}
|
||||
installedDimensions++;
|
||||
}
|
||||
if (context.required() && installedDimensions == 0) {
|
||||
throw new IOException("Retained Iris generation pack at " + context.packRoot()
|
||||
+ " does not contain dimension '" + context.dimensionKey() + "'.");
|
||||
}
|
||||
return installedDimensions > 0;
|
||||
} finally {
|
||||
data.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static KSet<String> biomesForNamespace(Map<String, KSet<String>> biomes, String namespace) {
|
||||
@@ -631,15 +650,41 @@ public final class ModdedForcedDatapack {
|
||||
|
||||
static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension,
|
||||
String pack, String packDimensionKey) throws IOException {
|
||||
writeDimensionType(
|
||||
folders,
|
||||
fixer,
|
||||
dimension,
|
||||
ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey),
|
||||
true,
|
||||
IrisCustomBiomeAliasResolver.none()
|
||||
);
|
||||
}
|
||||
|
||||
private static void writeDimensionType(
|
||||
KList<File> folders,
|
||||
IDataFixer fixer,
|
||||
IrisDimension dimension,
|
||||
String typeRef,
|
||||
boolean emitLegacyAlias,
|
||||
IrisCustomBiomeAliasResolver sourceResolver
|
||||
) throws IOException {
|
||||
IrisDimensionType type = dimension.getDimensionType();
|
||||
String json = type.toJson(fixer);
|
||||
String typeRef = ModdedWorldgenIds.dimensionTypeRef(pack, packDimensionKey);
|
||||
String json = Objects.requireNonNull(sourceResolver, "sourceResolver").generatedSource(
|
||||
GenerationRegistryContractFactory.DIMENSION_TYPE_REGISTRY,
|
||||
typeRef,
|
||||
type.toJson(fixer),
|
||||
fixer
|
||||
);
|
||||
String typePath = typeRef.substring(typeRef.indexOf(':') + 1);
|
||||
String typeNamespace = typeRef.substring(0, typeRef.indexOf(':'));
|
||||
for (File datapackRoot : folders) {
|
||||
Path output = datapackRoot.toPath().resolve("data").resolve("irisworldgen")
|
||||
Path output = datapackRoot.toPath().resolve("data").resolve(typeNamespace)
|
||||
.resolve("dimension_type").resolve(typePath + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, json, StandardCharsets.UTF_8);
|
||||
if (!emitLegacyAlias) {
|
||||
continue;
|
||||
}
|
||||
// Load-bearing, not dead: worlds created before the scoped pack path reference
|
||||
// irisworldgen:<dimensionTypeKey> in their level.dat, and ModdedWorldEngines accepts that legacy
|
||||
// key when validating the runtime dimension contract. Removing this emission unloads those worlds.
|
||||
@@ -731,10 +776,239 @@ public final class ModdedForcedDatapack {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<File> compilationPackRoots() throws IOException {
|
||||
return compilationPackRoots(worldRootOrNull());
|
||||
}
|
||||
|
||||
private static List<File> compilationPackRoots(Path worldRoot) throws IOException {
|
||||
Path dataDirectory = packsRoot().getParent();
|
||||
Path historyRoot = worldRoot == null
|
||||
? dataDirectory.resolve(".iris-world-unavailable")
|
||||
: worldRoot;
|
||||
return IrisDatapackCompiler.collectPackRoots(dataDirectory, historyRoot);
|
||||
}
|
||||
|
||||
private static Path worldRootOrNull() throws IOException {
|
||||
MinecraftServer server = ModdedEngineBootstrap.currentServer();
|
||||
if (server != null) {
|
||||
return server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||
}
|
||||
if (ModdedEngineBootstrap.loader().clientEnvironment()) {
|
||||
return null;
|
||||
}
|
||||
return MainWorldService.configuredWorldRootIfPresent();
|
||||
}
|
||||
|
||||
private static Set<PackSelection> unadoptedSelections(Path worldRoot) throws IOException {
|
||||
if (worldRoot == null) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<PackSelection> selections = new LinkedHashSet<>();
|
||||
for (ModdedDimensionRegistryStore.PersistentDimension dimension
|
||||
: ModdedDimensionRegistryStore.loadWorldRoot(worldRoot)) {
|
||||
Identifier identifier = Identifier.tryParse(dimension.id());
|
||||
if (identifier == null) {
|
||||
throw new IOException("Invalid persistent Iris dimension ID '" + dimension.id() + "'.");
|
||||
}
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, identifier);
|
||||
Path dimensionRoot = DimensionType.getStorageFolder(key, worldRoot).toAbsolutePath().normalize();
|
||||
Path generationRoot = GenerationHistoryPaths.forDimension(dimensionRoot).generationRoot();
|
||||
if (!Files.exists(generationRoot)) {
|
||||
selections.add(new PackSelection(dimension.pack(), dimension.dimension()));
|
||||
}
|
||||
}
|
||||
return Set.copyOf(selections);
|
||||
}
|
||||
|
||||
private static PackStageContext packStageContext(
|
||||
Path packRoot,
|
||||
Set<PackSelection> unadoptedSelections
|
||||
) throws IOException {
|
||||
Path normalizedRoot = packRoot.toAbsolutePath().normalize();
|
||||
RetainedPack retained = retainedPack(normalizedRoot);
|
||||
if (retained != null) {
|
||||
ModdedWorldgenIds.ScopedDimension identity = ModdedWorldgenIds
|
||||
.scopedDimensionType(retained.epoch().dimensionContract().dimensionTypeKey())
|
||||
.orElseThrow(() -> new IOException("Retained Iris dimension type key is not scoped: "
|
||||
+ retained.epoch().dimensionContract().dimensionTypeKey()));
|
||||
if (!identity.dimension().equals(retained.epoch().dimensionContract().dimensionKey())) {
|
||||
throw new IOException("Retained Iris dimension type identity does not match its epoch contract.");
|
||||
}
|
||||
return PackStageContext.retained(normalizedRoot, identity.pack(), retained.epoch());
|
||||
}
|
||||
|
||||
String packName = normalizedRoot.getFileName().toString();
|
||||
Set<String> legacyDimensions = new LinkedHashSet<>();
|
||||
for (PackSelection selection : unadoptedSelections) {
|
||||
if (selection.pack().equals(packName)) {
|
||||
legacyDimensions.add(selection.dimension());
|
||||
}
|
||||
}
|
||||
return PackStageContext.installed(normalizedRoot, packName, legacyDimensions);
|
||||
}
|
||||
|
||||
private static RetainedPack retainedPack(Path packRoot) throws IOException {
|
||||
Path epochRoot = packRoot.getParent();
|
||||
Path epochsRoot = epochRoot == null ? null : epochRoot.getParent();
|
||||
Path generationRoot = epochsRoot == null ? null : epochsRoot.getParent();
|
||||
Path irisRoot = generationRoot == null ? null : generationRoot.getParent();
|
||||
Path dimensionRoot = irisRoot == null ? null : irisRoot.getParent();
|
||||
if (epochRoot == null
|
||||
|| epochsRoot == null
|
||||
|| generationRoot == null
|
||||
|| irisRoot == null
|
||||
|| dimensionRoot == null
|
||||
|| !"pack".equals(packRoot.getFileName().toString())
|
||||
|| !GenerationHistoryPaths.EPOCHS_DIRECTORY_NAME.equals(epochsRoot.getFileName().toString())
|
||||
|| !GenerationHistoryPaths.GENERATION_DIRECTORY_NAME.equals(generationRoot.getFileName().toString())
|
||||
|| !GenerationHistoryPaths.IRIS_DIRECTORY_NAME.equals(irisRoot.getFileName().toString())) {
|
||||
return null;
|
||||
}
|
||||
GenerationHistory history = GenerationHistory.open(dimensionRoot);
|
||||
String epochId = epochRoot.getFileName().toString();
|
||||
GenerationEpoch epoch = history.manifest().epoch(epochId).orElseThrow(
|
||||
() -> new IOException("Retained Iris pack is not referenced by generation history: " + packRoot)
|
||||
);
|
||||
if (!history.paths().packRoot(epochId).toAbsolutePath().normalize().equals(packRoot)) {
|
||||
throw new IOException("Retained Iris pack path does not match generation history: " + packRoot);
|
||||
}
|
||||
return new RetainedPack(history, epoch);
|
||||
}
|
||||
|
||||
private static IOException requiredPackFailure(
|
||||
PackStageContext context,
|
||||
String operation,
|
||||
Throwable failure
|
||||
) {
|
||||
if (failure instanceof IOException ioFailure) {
|
||||
return ioFailure;
|
||||
}
|
||||
return new IOException("Retained Iris generation pack at " + context.packRoot()
|
||||
+ " failed " + operation + ".", failure);
|
||||
}
|
||||
|
||||
private static Path packsRoot() {
|
||||
return IrisPlatforms.get().packsFolderNoCreate().toPath();
|
||||
}
|
||||
|
||||
private record PackSelection(String pack, String dimension) {
|
||||
}
|
||||
|
||||
private record RetainedPack(GenerationHistory history, GenerationEpoch epoch) {
|
||||
}
|
||||
|
||||
private record PackStageContext(
|
||||
Path packRoot,
|
||||
String packName,
|
||||
String dimensionKey,
|
||||
String retainedDimensionTypeRef,
|
||||
boolean emitPresets,
|
||||
boolean emitLegacyDimensionTypeAlias,
|
||||
boolean required,
|
||||
Set<String> legacyDimensions,
|
||||
GenerationRegistryContract retainedContract
|
||||
) implements IrisCustomBiomeAliasResolver {
|
||||
private static PackStageContext installed(
|
||||
Path packRoot,
|
||||
String packName,
|
||||
Set<String> legacyDimensions
|
||||
) {
|
||||
return new PackStageContext(
|
||||
packRoot,
|
||||
packName,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Set.copyOf(legacyDimensions),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private static PackStageContext retained(
|
||||
Path packRoot,
|
||||
String packName,
|
||||
GenerationEpoch epoch
|
||||
) {
|
||||
return new PackStageContext(
|
||||
packRoot,
|
||||
packName,
|
||||
epoch.dimensionContract().dimensionKey(),
|
||||
epoch.dimensionContract().dimensionTypeKey(),
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
Set.of(),
|
||||
epoch.registryContract()
|
||||
);
|
||||
}
|
||||
|
||||
private String dimensionTypeRef(String installedDimensionKey) {
|
||||
return retainedDimensionTypeRef == null
|
||||
? ModdedWorldgenIds.dimensionTypeRef(packName, installedDimensionKey)
|
||||
: retainedDimensionTypeRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String physicalResourceKey(
|
||||
IrisDimension dimension,
|
||||
IrisBiome biome,
|
||||
IrisBiomeCustom customBiome,
|
||||
String currentPhysicalResourceKey
|
||||
) throws IOException {
|
||||
if (retainedContract == null) {
|
||||
return currentPhysicalResourceKey;
|
||||
}
|
||||
return dimension.getLoader().customBiomeResourceKey(dimension, customBiome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases(
|
||||
IrisDimension dimension,
|
||||
IrisBiome biome,
|
||||
IrisBiomeCustom customBiome,
|
||||
String physicalResourceKey
|
||||
) {
|
||||
List<String> candidates = ModdedWorldgenIds.legacyBiomeRefs(
|
||||
packName,
|
||||
dimension.getLoadKey(),
|
||||
customBiome.getId()
|
||||
);
|
||||
if (legacyDimensions.contains(dimension.getLoadKey())) {
|
||||
return candidates;
|
||||
}
|
||||
if (retainedContract == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> aliases = new ArrayList<>(candidates.size());
|
||||
for (String candidate : candidates) {
|
||||
PhysicalResourceKey key = new PhysicalResourceKey(
|
||||
GenerationRegistryContractFactory.BIOME_REGISTRY,
|
||||
candidate
|
||||
);
|
||||
if (retainedContract.definitions().containsKey(key)) {
|
||||
aliases.add(candidate);
|
||||
}
|
||||
}
|
||||
return List.copyOf(aliases);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generatedSource(
|
||||
String registryKey,
|
||||
String resourceKey,
|
||||
String currentSource,
|
||||
IDataFixer fixer
|
||||
) throws IOException {
|
||||
if (retainedContract == null) {
|
||||
return currentSource;
|
||||
}
|
||||
PhysicalResourceKey key = new PhysicalResourceKey(registryKey, resourceKey);
|
||||
return GenerationRegistryContractFactory.requireGeneratedSource(retainedContract, key, fixer);
|
||||
}
|
||||
}
|
||||
|
||||
private record PublishedState(Path directory, String packsHash) {
|
||||
}
|
||||
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+34
-26
@@ -57,6 +57,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
@@ -77,13 +78,11 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
*/
|
||||
final class ModdedImportedFeatureStage {
|
||||
private static final String CYCLE_MARKER = "Feature order cycle found";
|
||||
private static final long NO_GENERATION = Long.MIN_VALUE;
|
||||
|
||||
private final IrisModdedBiomeSource biomeSource;
|
||||
private final ReentrantLock buildLock = new ReentrantLock();
|
||||
private final ConcurrentHashMap<Integer, FeatureTable> featureTables = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> inertGenerations = new ConcurrentHashMap<>();
|
||||
private volatile IrisModdedChunkGenerator generator;
|
||||
private volatile FeatureTable featureTable;
|
||||
private volatile long inertGeneration = NO_GENERATION;
|
||||
|
||||
ModdedImportedFeatureStage(IrisModdedBiomeSource biomeSource) {
|
||||
this.biomeSource = biomeSource;
|
||||
@@ -98,12 +97,12 @@ final class ModdedImportedFeatureStage {
|
||||
* pack can never serve another.
|
||||
*/
|
||||
void invalidate() {
|
||||
featureTable = null;
|
||||
inertGeneration = NO_GENERATION;
|
||||
featureTables.clear();
|
||||
inertGenerations.clear();
|
||||
}
|
||||
|
||||
boolean active() {
|
||||
return featureTable != null;
|
||||
return !featureTables.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +114,8 @@ final class ModdedImportedFeatureStage {
|
||||
* <p>With {@code importedFeatures} disabled there is no table and this is vanilla's default getter.
|
||||
*/
|
||||
BiomeGenerationSettings generationSettings(Holder<Biome> biome) {
|
||||
FeatureTable table = featureTable;
|
||||
Engine engine = generator == null ? null : generator.engineOrNull();
|
||||
FeatureTable table = engine == null ? null : featureTables.get(engine.getCacheID());
|
||||
if (table == null) {
|
||||
return biome.value().getGenerationSettings();
|
||||
}
|
||||
@@ -146,8 +146,9 @@ final class ModdedImportedFeatureStage {
|
||||
if (engine == null || engine.isClosed() || engine.isClosing()) {
|
||||
return;
|
||||
}
|
||||
int runtimeIdentity = engine.getCacheID();
|
||||
long generation = biomeSource.packGeneration();
|
||||
if (settled(generation)) {
|
||||
if (settled(runtimeIdentity, generation)) {
|
||||
return;
|
||||
}
|
||||
if (waitForBuild) {
|
||||
@@ -156,35 +157,35 @@ final class ModdedImportedFeatureStage {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (settled(generation)) {
|
||||
if (settled(runtimeIdentity, generation)) {
|
||||
return;
|
||||
}
|
||||
build(engine, generation);
|
||||
build(engine, runtimeIdentity, generation);
|
||||
} finally {
|
||||
buildLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean settled(long generation) {
|
||||
FeatureTable current = featureTable;
|
||||
private boolean settled(int runtimeIdentity, long generation) {
|
||||
FeatureTable current = featureTables.get(runtimeIdentity);
|
||||
if (current != null && current.generation() == generation) {
|
||||
return true;
|
||||
}
|
||||
return inertGeneration == generation;
|
||||
return Long.valueOf(generation).equals(inertGenerations.get(runtimeIdentity));
|
||||
}
|
||||
|
||||
private void build(Engine engine, long generation) {
|
||||
private void build(Engine engine, int runtimeIdentity, long generation) {
|
||||
IrisImportedFeatureControl control;
|
||||
try {
|
||||
control = NativeFeatureGenerationPolicy.control(engine);
|
||||
} catch (RuntimeException error) {
|
||||
ModdedIrisLog.error("Iris could not read importedFeatures for this dimension; features off: {}",
|
||||
error.toString());
|
||||
markInert(generation);
|
||||
markInert(runtimeIdentity, generation);
|
||||
return;
|
||||
}
|
||||
if (!control.shouldGenerateFeatures()) {
|
||||
markInert(generation);
|
||||
markInert(runtimeIdentity, generation);
|
||||
return;
|
||||
}
|
||||
FeatureTable built;
|
||||
@@ -194,15 +195,15 @@ final class ModdedImportedFeatureStage {
|
||||
} catch (Throwable error) {
|
||||
ModdedIrisLog.error("Iris importedFeatures is off for {}: feature table construction failed: {}",
|
||||
dimensionKey(engine), error.toString());
|
||||
markInert(generation);
|
||||
markInert(runtimeIdentity, generation);
|
||||
return;
|
||||
}
|
||||
if (built == null) {
|
||||
markInert(generation);
|
||||
markInert(runtimeIdentity, generation);
|
||||
return;
|
||||
}
|
||||
featureTable = built;
|
||||
inertGeneration = NO_GENERATION;
|
||||
featureTables.put(runtimeIdentity, built);
|
||||
inertGenerations.remove(runtimeIdentity);
|
||||
// Arm the worldcheck log watch here, before any chunk decorates: arming from the first pass instead
|
||||
// missed every far-chunk write the first chunk made. No-op unless -Diris.worldcheck is set.
|
||||
WorldCheckFeaturePlacement.arm();
|
||||
@@ -211,9 +212,9 @@ final class ModdedImportedFeatureStage {
|
||||
built.derivatives().size());
|
||||
}
|
||||
|
||||
private void markInert(long generation) {
|
||||
featureTable = null;
|
||||
inertGeneration = generation;
|
||||
private void markInert(int runtimeIdentity, long generation) {
|
||||
featureTables.remove(runtimeIdentity);
|
||||
inertGenerations.put(runtimeIdentity, generation);
|
||||
}
|
||||
|
||||
private FeatureTable buildTable(Engine engine, IrisImportedFeatureControl control, long generation) {
|
||||
@@ -300,7 +301,8 @@ final class ModdedImportedFeatureStage {
|
||||
* control is disabled or the table degraded.
|
||||
*/
|
||||
void run(WorldGenLevel level, ChunkAccess chunk, Engine engine) {
|
||||
FeatureTable table = featureTable;
|
||||
int runtimeIdentity = engine.getCacheID();
|
||||
FeatureTable table = featureTables.get(runtimeIdentity);
|
||||
if (table == null) {
|
||||
WorldCheckFeaturePlacement.recordFeaturesOff();
|
||||
return;
|
||||
@@ -308,7 +310,8 @@ final class ModdedImportedFeatureStage {
|
||||
if (table.generation() != biomeSource.packGeneration()) {
|
||||
// A repoint landed between the prepare above and this chunk. Refuse stale content outright; the
|
||||
// next chunk's prepare rebuilds against the new pack.
|
||||
invalidate();
|
||||
featureTables.remove(runtimeIdentity, table);
|
||||
inertGenerations.remove(runtimeIdentity);
|
||||
return;
|
||||
}
|
||||
IrisModdedChunkGenerator owner = generator;
|
||||
@@ -349,6 +352,11 @@ final class ModdedImportedFeatureStage {
|
||||
WorldCheckFeaturePlacement.recordPlacementPass();
|
||||
}
|
||||
|
||||
void evictRuntime(int runtimeIdentity) {
|
||||
featureTables.remove(runtimeIdentity);
|
||||
inertGenerations.remove(runtimeIdentity);
|
||||
}
|
||||
|
||||
private void placeStep(WorldGenLevel level, FeatureTable table, FeatureSorter.StepFeatureData stepData,
|
||||
Registry<PlacedFeature> featureRegistry, Set<Holder<Biome>> chunkBiomes,
|
||||
IrisModdedChunkGenerator owner, WorldgenRandom random, long decorationSeed,
|
||||
|
||||
+27
-5
@@ -22,6 +22,7 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipRecord;
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||
import art.arcane.iris.engine.framework.NativeStructureStartPlan;
|
||||
import art.arcane.iris.engine.object.IrisMaterialPalette;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
@@ -230,17 +231,30 @@ final class ModdedNativeStructureStage {
|
||||
if (!start.isValid() || previousStarts.get(structure) == start) {
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
recordWorldCheckStructureShift(
|
||||
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
|
||||
continue;
|
||||
}
|
||||
Identifier id = registry.getKey(structure);
|
||||
String structureId = id == null ? null : id.toString();
|
||||
if (structureId == null) {
|
||||
throw NativeStructureGenerationException.failure(
|
||||
"resolution", null, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
BoundingBox footprint = start.getBoundingBox();
|
||||
if (!current.getComplex().allowsNewGenerationFootprint(
|
||||
footprint.minX(),
|
||||
footprint.minZ(),
|
||||
footprint.maxX(),
|
||||
footprint.maxZ())) {
|
||||
chunk.setStartForStructure(structure, StructureStart.INVALID_START);
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
NativeStructureOwnershipStore.discard(
|
||||
current, structureId, chunkPos.x(), chunkPos.z());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (configuredStarts.containsKey(structure)) {
|
||||
recordWorldCheckStructureShift(
|
||||
configuredStarts.get(structure).source().getStructure(), start.getChunkPos(), 0);
|
||||
continue;
|
||||
}
|
||||
boolean undergroundStep = NativeStructureVegetationClearer.isUndergroundStep(structure.step());
|
||||
IrisNativeStructureDecision decision;
|
||||
try {
|
||||
@@ -323,6 +337,14 @@ final class ModdedNativeStructureStage {
|
||||
List<StructureStart> starts = structureManager.startsForStructure(sectionPos, structure);
|
||||
List<NativePlacement> resolvedPlacements = new ArrayList<>(starts.size());
|
||||
for (StructureStart start : starts) {
|
||||
BoundingBox footprint = start.getBoundingBox();
|
||||
if (!current.getComplex().allowsNewGenerationFootprint(
|
||||
footprint.minX(),
|
||||
footprint.minZ(),
|
||||
footprint.maxX(),
|
||||
footprint.maxZ())) {
|
||||
continue;
|
||||
}
|
||||
NativeStructureOwnershipRecord ownership =
|
||||
NativeStructureOwnershipRecovery.resolve(
|
||||
current, world.getLevel(), structureId, structure, start);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.BuildConstants;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.modded.api.ModdedDataType;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -25,6 +26,7 @@ import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformEntityType;
|
||||
import art.arcane.iris.spi.PlatformGenerationRegistry;
|
||||
import art.arcane.iris.spi.PlatformItem;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.volmlib.util.data.UnresolvedKeyLog;
|
||||
@@ -59,6 +61,19 @@ public final class ModdedRegistries implements PlatformRegistries {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformGenerationRegistry generationRegistry() {
|
||||
MinecraftServer instance = server.get();
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException("Minecraft server is not ready for generation registry capture.");
|
||||
}
|
||||
return new ModdedGenerationRegistry(
|
||||
server,
|
||||
"modded-generation-registry-v1",
|
||||
"modded-generated-registry-json-v1|minecraft:" + BuildConstants.MINECRAFT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState block(String key) {
|
||||
return ModdedBlockResolution.get(key);
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ public final class ModdedRuntimeRegistry {
|
||||
if (!seen.add(biomeId)) {
|
||||
continue;
|
||||
}
|
||||
String biomeRef = ModdedWorldgenIds.biomeRef(pack, dimension.getLoadKey(), biomeId);
|
||||
String biomeRef = data.customBiomeResourceKey(dimension, customBiome);
|
||||
ResourceKey<Biome> biomeKey = ResourceKey.create(Registries.BIOME, Identifier.parse(biomeRef));
|
||||
if (registry.get(biomeKey).isEmpty()) {
|
||||
missing.add(biomeRef);
|
||||
|
||||
+37
-21
@@ -31,8 +31,8 @@ import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.MobSpawnSettings;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -41,32 +41,30 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
*/
|
||||
final class ModdedSpawnTableMerger {
|
||||
private final IrisModdedChunkGenerator generator;
|
||||
private final ConcurrentHashMap<Biome, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<SpawnBiomeKey, Holder<Biome>> vanillaSpawnBiomes = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<SpawnTableKey, WeightedList<MobSpawnSettings.SpawnerData>> mergedSpawnTables = new ConcurrentHashMap<>();
|
||||
private volatile boolean vanillaSpawnBiomesInitialized;
|
||||
private final Set<Integer> initializedRuntimeIdentities = ConcurrentHashMap.newKeySet();
|
||||
|
||||
ModdedSpawnTableMerger(IrisModdedChunkGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
void initializeVanillaSpawnBiomes(Registry<Biome> registry) {
|
||||
// Volatile fast path BEFORE the monitor: this runs per mob category per spawn attempt
|
||||
// on the server thread, and the generator monitor is contended by binds/hotloads.
|
||||
if (vanillaSpawnBiomesInitialized) {
|
||||
Engine current = generator.engineOrNull();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
int runtimeIdentity = current.getCacheID();
|
||||
if (initializedRuntimeIdentities.contains(runtimeIdentity)) {
|
||||
return;
|
||||
}
|
||||
synchronized (generator) {
|
||||
if (vanillaSpawnBiomesInitialized) {
|
||||
return;
|
||||
}
|
||||
Engine current = generator.engineOrNull();
|
||||
if (current == null) {
|
||||
if (initializedRuntimeIdentities.contains(runtimeIdentity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (GenerationSessionLease lease = generator.requireGenerationLease(current, "modded_spawn_biomes");
|
||||
IrisContext.Scope ignored = IrisContext.open(current, lease.sessionId(), null)) {
|
||||
String namespace = current.getDimension().getLoadKey().toLowerCase(Locale.ROOT);
|
||||
for (IrisBiome irisBiome : current.getDimension().getReachableBiomes(current)) {
|
||||
if (irisBiome == null || !irisBiome.isCustom()) {
|
||||
continue;
|
||||
@@ -76,29 +74,44 @@ final class ModdedSpawnTableMerger {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiomeCustom customBiome : irisBiome.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, namespace + ":" + customBiome.getId());
|
||||
String resourceKey = current.getData().customBiomeResourceKey(
|
||||
current.getDimension(),
|
||||
customBiome
|
||||
);
|
||||
Holder<Biome> customHolder = resolveBiomeHolder(registry, resourceKey);
|
||||
if (customHolder != null) {
|
||||
vanillaSpawnBiomes.putIfAbsent(customHolder.value(), vanillaHolder);
|
||||
vanillaSpawnBiomes.putIfAbsent(
|
||||
new SpawnBiomeKey(runtimeIdentity, customHolder.value()),
|
||||
vanillaHolder
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
vanillaSpawnBiomesInitialized = true;
|
||||
initializedRuntimeIdentities.add(runtimeIdentity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Holder<Biome> vanillaSpawnBiome(Biome biome) {
|
||||
return vanillaSpawnBiomes.get(biome);
|
||||
Holder<Biome> vanillaSpawnBiome(Engine engine, Biome biome) {
|
||||
return vanillaSpawnBiomes.get(new SpawnBiomeKey(engine.getCacheID(), biome));
|
||||
}
|
||||
|
||||
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
|
||||
Biome biome, MobCategory category,
|
||||
Engine engine,
|
||||
Biome biome,
|
||||
MobCategory category,
|
||||
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
|
||||
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
|
||||
SpawnTableKey key = new SpawnTableKey(biome, category);
|
||||
SpawnTableKey key = new SpawnTableKey(engine.getCacheID(), biome, category);
|
||||
return mergedSpawnTables.computeIfAbsent(key, ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns));
|
||||
}
|
||||
|
||||
void evictRuntime(int runtimeIdentity) {
|
||||
vanillaSpawnBiomes.keySet().removeIf(key -> key.runtimeIdentity() == runtimeIdentity);
|
||||
mergedSpawnTables.keySet().removeIf(key -> key.runtimeIdentity() == runtimeIdentity);
|
||||
initializedRuntimeIdentities.remove(runtimeIdentity);
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return null;
|
||||
@@ -115,10 +128,13 @@ final class ModdedSpawnTableMerger {
|
||||
synchronized (generator) {
|
||||
vanillaSpawnBiomes.clear();
|
||||
mergedSpawnTables.clear();
|
||||
vanillaSpawnBiomesInitialized = false;
|
||||
initializedRuntimeIdentities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private record SpawnTableKey(Biome biome, MobCategory category) {
|
||||
private record SpawnBiomeKey(int runtimeIdentity, Biome biome) {
|
||||
}
|
||||
|
||||
private record SpawnTableKey(int runtimeIdentity, Biome biome, MobCategory category) {
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,13 @@ public final class ModdedStartup {
|
||||
index++;
|
||||
long dimensionStartedAt = System.currentTimeMillis();
|
||||
try {
|
||||
ModdedDimensionManager.create(server, dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
|
||||
ModdedDimensionManager.restorePersistent(
|
||||
server,
|
||||
dimension.id(),
|
||||
dimension.pack(),
|
||||
dimension.dimension(),
|
||||
dimension.seed()
|
||||
);
|
||||
injected++;
|
||||
ModdedIrisLog.info("Iris re-injected {}/{} '{}' (pack={} dim={}) in {}ms",
|
||||
index, dimensions.size(), dimension.id(), dimension.pack(), dimension.dimension(),
|
||||
|
||||
+249
-49
@@ -18,21 +18,30 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||
import art.arcane.iris.engine.IrisEngine;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.history.GenerationActivation;
|
||||
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||
import art.arcane.iris.engine.history.GenerationHistory;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisDimensionRuntimeContract;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.modded.command.ModdedGuiHost;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
@@ -45,13 +54,25 @@ public final class ModdedWorldEngines {
|
||||
private ModdedWorldEngines() {
|
||||
}
|
||||
|
||||
public static Engine get(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||
public static Engine get(
|
||||
ServerLevel level,
|
||||
String pack,
|
||||
String dimensionKey,
|
||||
long seedOverride,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
Engine existing = ENGINES.get(level);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
return ENGINES.computeIfAbsent(level,
|
||||
(ServerLevel l) -> createAndApplyWorldBoundary(l, pack, dimensionKey, seedOverride));
|
||||
(ServerLevel l) -> createAndApplyWorldBoundary(
|
||||
l,
|
||||
pack,
|
||||
dimensionKey,
|
||||
seedOverride,
|
||||
generationMode
|
||||
));
|
||||
}
|
||||
|
||||
public static Collection<Engine> activeEngines() {
|
||||
@@ -81,8 +102,14 @@ public final class ModdedWorldEngines {
|
||||
ModdedIrisLog.info("Iris engine evicted for {}", level.dimension().identifier());
|
||||
}
|
||||
|
||||
static Engine prepareReplacement(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||
return create(level, pack, dimensionKey, seedOverride);
|
||||
static Engine prepareReplacement(
|
||||
ServerLevel level,
|
||||
String pack,
|
||||
String dimensionKey,
|
||||
long seedOverride,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
return create(level, pack, dimensionKey, seedOverride, generationMode);
|
||||
}
|
||||
|
||||
static void installReplacement(ServerLevel level, Engine replacement) {
|
||||
@@ -103,9 +130,14 @@ public final class ModdedWorldEngines {
|
||||
ModdedGuiHost.unbind(engine);
|
||||
}
|
||||
|
||||
private static Engine createAndApplyWorldBoundary(ServerLevel level, String pack,
|
||||
String dimensionKey, long seedOverride) {
|
||||
Engine engine = create(level, pack, dimensionKey, seedOverride);
|
||||
private static Engine createAndApplyWorldBoundary(
|
||||
ServerLevel level,
|
||||
String pack,
|
||||
String dimensionKey,
|
||||
long seedOverride,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
Engine engine = create(level, pack, dimensionKey, seedOverride, generationMode);
|
||||
try {
|
||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||
return engine;
|
||||
@@ -126,22 +158,178 @@ public final class ModdedWorldEngines {
|
||||
}
|
||||
}
|
||||
|
||||
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||
private static Engine create(
|
||||
ServerLevel level,
|
||||
String pack,
|
||||
String dimensionKey,
|
||||
long seedOverride,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
ModdedEngineBootstrap.bind();
|
||||
File packDir = resolvePack(pack, dimensionKey);
|
||||
PackValidationRegistry.requireLoadable(pack);
|
||||
IrisData data = IrisData.openRuntime(packDir);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
ModdedIrisLog.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
|
||||
pack, packDir.getAbsolutePath(), dimensionKey, dimensionKey);
|
||||
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + packDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
|
||||
validateDimensionContract(pack, dimensionKey, dimension, level);
|
||||
File worldFolder = DimensionType.getStorageFolder(level.dimension(), level.getServer().getWorldPath(LevelResource.ROOT)).toFile();
|
||||
IrisWorld world = IrisWorld.builder()
|
||||
ModdedGenerationMode requiredMode = Objects.requireNonNull(generationMode, "generationMode");
|
||||
if (requiredMode == ModdedGenerationMode.TRANSIENT_STUDIO) {
|
||||
return createTransientStudioEngine(level, pack, dimensionKey, seed);
|
||||
}
|
||||
ModdedGenerationHistoryStorage.ActivePack active = ModdedGenerationHistoryStorage.openOrAdopt(
|
||||
level,
|
||||
pack,
|
||||
dimensionKey,
|
||||
seed
|
||||
);
|
||||
try {
|
||||
return createHistoryEngine(level, seed, active);
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris generation history could not initialize runtime routing for '"
|
||||
+ level.dimension().identifier() + "'.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static Engine createHistoryEngine(
|
||||
ServerLevel level,
|
||||
long seed,
|
||||
ModdedGenerationHistoryStorage.ActivePack opened
|
||||
) throws IOException {
|
||||
GenerationHistory history = opened.history();
|
||||
long openedActivationId = history.activeActivation().activationId();
|
||||
IrisEngine candidate = buildEngine(level, seed, opened);
|
||||
boolean ready = false;
|
||||
try {
|
||||
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
|
||||
history,
|
||||
IrisBoundarySignatureSampler.INSTANCE,
|
||||
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
||||
);
|
||||
router.preloadActiveRuntimes();
|
||||
if (history.activeActivation().activationId() != openedActivationId) {
|
||||
close(candidate);
|
||||
ModdedGenerationHistoryStorage.ActivePack promoted =
|
||||
ModdedGenerationHistoryStorage.resolveActive(history);
|
||||
candidate = buildEngine(level, seed, promoted);
|
||||
router = GenerationHistoryRuntimeRouter.attach(
|
||||
candidate,
|
||||
history,
|
||||
IrisBoundarySignatureSampler.INSTANCE
|
||||
);
|
||||
router.preloadActiveRuntimes();
|
||||
}
|
||||
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
|
||||
ready = true;
|
||||
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
|
||||
level.dimension().identifier(),
|
||||
candidate.getData().getDataFolder().getAbsolutePath(),
|
||||
candidate.getDimension().getLoadKey(),
|
||||
seed,
|
||||
candidate.getDimension().getMinHeight(),
|
||||
candidate.getDimension().getMaxHeight(),
|
||||
history.activeActivation().activationId());
|
||||
return candidate;
|
||||
} finally {
|
||||
if (!ready) {
|
||||
close(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisEngine buildEngine(
|
||||
ServerLevel level,
|
||||
long seed,
|
||||
ModdedGenerationHistoryStorage.ActivePack active
|
||||
) throws IOException {
|
||||
File packDir = active.packRoot().toFile();
|
||||
PackValidationRegistry.requireLoadable(active.packRoot());
|
||||
IrisData data = IrisData.openRuntime(packDir);
|
||||
boolean runtimeOwnsData = false;
|
||||
try {
|
||||
IrisDimension dimension = data.getDimensionLoader().load(active.dimensionKey());
|
||||
if (dimension == null) {
|
||||
ModdedIrisLog.error("Iris immutable generation pack at {} does not contain dimension '{}' (expected dimensions/{}.json).",
|
||||
packDir.getAbsolutePath(), active.dimensionKey(), active.dimensionKey());
|
||||
throw new IllegalStateException("Iris dimension '" + active.dimensionKey()
|
||||
+ "' missing from immutable pack " + packDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
validateDimensionContract(active.dimensionContract(), dimension, level);
|
||||
File worldFolder = DimensionType.getStorageFolder(
|
||||
level.dimension(),
|
||||
level.getServer().getWorldPath(LevelResource.ROOT)
|
||||
).toFile();
|
||||
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
|
||||
GenerationHistory history = active.history();
|
||||
GenerationActivation activation = history.activeActivation();
|
||||
GenerationEpoch epoch = history.activeEpoch();
|
||||
TransitionGenerationPlan transitionPlan = activation.isInitial()
|
||||
? null
|
||||
: history.transitionPlan(activation.activationId());
|
||||
IrisEngine engine = new IrisEngine(
|
||||
new EngineTarget(world, dimension, data),
|
||||
IrisEngine.InitializationMode.RUNTIME,
|
||||
history.paths().activationMantleRoot(activation.activationId()),
|
||||
epoch.kernelVersion(),
|
||||
transitionPlan);
|
||||
runtimeOwnsData = true;
|
||||
return engine;
|
||||
} finally {
|
||||
if (!runtimeOwnsData) {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisEngine createTransientStudioEngine(
|
||||
ServerLevel level,
|
||||
String pack,
|
||||
String dimensionKey,
|
||||
long seed
|
||||
) {
|
||||
File packDir = resolvePack(pack, dimensionKey);
|
||||
PackValidationRegistry.requireLoadable(packDir.toPath());
|
||||
IrisData data = IrisData.openRuntime(packDir);
|
||||
boolean runtimeOwnsData = false;
|
||||
boolean ready = false;
|
||||
IrisEngine engine = null;
|
||||
try {
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
throw new IllegalStateException("Transient Iris Studio pack does not contain dimension '"
|
||||
+ dimensionKey + "'.");
|
||||
}
|
||||
File worldFolder = DimensionType.getStorageFolder(
|
||||
level.dimension(),
|
||||
level.getServer().getWorldPath(LevelResource.ROOT)
|
||||
).toFile();
|
||||
IrisWorld world = buildWorld(level, seed, dimension, worldFolder);
|
||||
engine = new IrisEngine(
|
||||
new EngineTarget(world, dimension, data),
|
||||
IrisEngine.InitializationMode.STUDIO
|
||||
);
|
||||
runtimeOwnsData = true;
|
||||
requireReady(engine, level, ModdedGenerationMode.TRANSIENT_STUDIO);
|
||||
ready = true;
|
||||
ModdedIrisLog.info("Iris transient Studio engine up for {}: pack={} dim={} seed={} height={}..{}",
|
||||
level.dimension().identifier(),
|
||||
packDir.getAbsolutePath(),
|
||||
dimension.getLoadKey(),
|
||||
seed,
|
||||
dimension.getMinHeight(),
|
||||
dimension.getMaxHeight());
|
||||
return engine;
|
||||
} finally {
|
||||
if (!ready && engine != null) {
|
||||
close(engine);
|
||||
} else if (!runtimeOwnsData) {
|
||||
data.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisWorld buildWorld(
|
||||
ServerLevel level,
|
||||
long seed,
|
||||
IrisDimension dimension,
|
||||
File worldFolder
|
||||
) {
|
||||
return IrisWorld.builder()
|
||||
.platformIdentity(level.dimension().identifier().toString())
|
||||
.name(level.dimension().identifier().toString().replace(':', '_'))
|
||||
.seed(seed)
|
||||
@@ -150,40 +338,52 @@ public final class ModdedWorldEngines {
|
||||
.maxHeight(dimension.getMaxHeight())
|
||||
.platformWorld(new ModdedPlatformWorld(level))
|
||||
.build();
|
||||
Engine engine = new IrisEngine(
|
||||
new EngineTarget(world, dimension, data),
|
||||
IrisEngine.InitializationMode.RUNTIME);
|
||||
if (engine.isClosed() || engine.getComplex() == null) {
|
||||
IllegalStateException failure = new IllegalStateException("Iris engine for "
|
||||
+ level.dimension().identifier() + " did not initialize a ready biome complex");
|
||||
try {
|
||||
close(engine);
|
||||
} catch (Throwable cleanupError) {
|
||||
failure.addSuppressed(cleanupError);
|
||||
}
|
||||
throw failure;
|
||||
}
|
||||
|
||||
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
|
||||
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static void validateDimensionContract(String pack, String dimensionKey,
|
||||
IrisDimension dimension, ServerLevel level) {
|
||||
private static void requireReady(
|
||||
IrisEngine engine,
|
||||
ServerLevel level,
|
||||
ModdedGenerationMode generationMode
|
||||
) {
|
||||
if (engine.isClosed() || engine.getComplex() == null) {
|
||||
throw new IllegalStateException("Iris engine for " + level.dimension().identifier()
|
||||
+ " did not initialize a ready runtime");
|
||||
}
|
||||
if (generationMode.historyRequired()
|
||||
&& engine.getGenerationHistoryRuntimeRouter().isEmpty()) {
|
||||
throw new IllegalStateException("Persistent Iris engine for " + level.dimension().identifier()
|
||||
+ " did not initialize a generation-history runtime router");
|
||||
}
|
||||
if (!generationMode.historyRequired()
|
||||
&& (!engine.isStudio() || engine.getGenerationHistoryRuntimeRouter().isPresent())) {
|
||||
throw new IllegalStateException("Transient Iris Studio engine for "
|
||||
+ level.dimension().identifier() + " has an invalid history mode");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateDimensionContract(
|
||||
GenerationEpoch.DimensionContract recorded,
|
||||
IrisDimension dimension,
|
||||
ServerLevel level
|
||||
) {
|
||||
GenerationEpoch.DimensionContract loaded = GenerationEpochContractFactory.create(
|
||||
dimension,
|
||||
dimension.getLoadKey(),
|
||||
recorded.dimensionTypeKey()
|
||||
);
|
||||
if (!recorded.equals(loaded)) {
|
||||
throw new IllegalStateException("Immutable Iris dimension contract changed for '"
|
||||
+ level.dimension().identifier() + "'.");
|
||||
}
|
||||
DimensionType actualType = level.dimensionType();
|
||||
String actualTypeKey = level.dimensionTypeRegistration().unwrapKey()
|
||||
.map(key -> key.identifier().toString())
|
||||
.orElse("<unregistered>");
|
||||
String legacyTypeKey = "irisworldgen:" + dimension.getDimensionTypeKey();
|
||||
String expectedTypeKey = legacyTypeKey.equals(actualTypeKey)
|
||||
? legacyTypeKey
|
||||
: ModdedWorldgenIds.dimensionTypeRef(pack, dimensionKey);
|
||||
IrisDimensionRuntimeContract expected = new IrisDimensionRuntimeContract(
|
||||
expectedTypeKey,
|
||||
dimension.getMinHeight(),
|
||||
dimension.getMaxHeight() - dimension.getMinHeight(),
|
||||
dimension.getLogicalHeight());
|
||||
recorded.dimensionTypeKey(),
|
||||
recorded.minHeight(),
|
||||
recorded.height(),
|
||||
recorded.logicalHeight());
|
||||
IrisDimensionRuntimeContract actual = new IrisDimensionRuntimeContract(
|
||||
actualTypeKey,
|
||||
actualType.minY(),
|
||||
|
||||
@@ -3,7 +3,9 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class ModdedWorldgenIds {
|
||||
private static final String NAMESPACE = "irisworldgen";
|
||||
@@ -29,8 +31,33 @@ public final class ModdedWorldgenIds {
|
||||
}
|
||||
|
||||
public static String biomeRef(Engine engine, String biome) {
|
||||
return biomeRef(engine.getData().getDataFolder().getName(),
|
||||
engine.getDimension().getLoadKey(), biome);
|
||||
return engine.getData().customBiomeResourceKey(engine.getDimension(), biome);
|
||||
}
|
||||
|
||||
public static List<String> legacyBiomeRefs(String pack, String dimension, String biome) {
|
||||
String scoped = biomeRef(pack, dimension, biome);
|
||||
String unscoped = dimension.toLowerCase(Locale.ROOT) + ":" + biome.toLowerCase(Locale.ROOT);
|
||||
return scoped.equals(unscoped) ? List.of(scoped) : List.of(scoped, unscoped);
|
||||
}
|
||||
|
||||
static Optional<ScopedDimension> scopedDimensionType(String dimensionTypeRef) {
|
||||
String prefix = NAMESPACE + ":";
|
||||
if (dimensionTypeRef == null || !dimensionTypeRef.startsWith(prefix)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String[] parts = dimensionTypeRef.substring(prefix.length()).split("/");
|
||||
if (parts.length != 5
|
||||
|| !"packs".equals(parts[0])
|
||||
|| !"dimensions".equals(parts[2])
|
||||
|| !"dimension_type".equals(parts[4])) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String pack = decode(parts[1]);
|
||||
String dimension = decode(parts[3]);
|
||||
if (pack == null || dimension == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new ScopedDimension(pack, dimension));
|
||||
}
|
||||
|
||||
public static String displayName(String presetPath) {
|
||||
@@ -100,4 +127,7 @@ public final class ModdedWorldgenIds {
|
||||
}
|
||||
return Character.toUpperCase(normalized.charAt(0)) + normalized.substring(1);
|
||||
}
|
||||
|
||||
record ScopedDimension(String pack, String dimension) {
|
||||
}
|
||||
}
|
||||
+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.StructureReachability;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.engine.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
import art.arcane.iris.engine.object.NativeStructureGenerationStatus;
|
||||
@@ -109,7 +110,7 @@ final class ModdedCommandSuggestions {
|
||||
}
|
||||
|
||||
static Set<String> reachableBiomeKeys(Engine engine) {
|
||||
return reachableBiomeKeys(engine.getAllBiomes());
|
||||
return reachableBiomeKeys(GenerationFindCatalog.biomes(engine));
|
||||
}
|
||||
|
||||
static Set<String> reachableBiomeKeys(Iterable<IrisBiome> biomes) {
|
||||
@@ -123,7 +124,7 @@ final class ModdedCommandSuggestions {
|
||||
}
|
||||
|
||||
static boolean isReachableBiome(Engine engine, String biomeKey) {
|
||||
return isReachableBiome(engine.getAllBiomes(), biomeKey);
|
||||
return GenerationFindCatalog.biome(engine, biomeKey) != null;
|
||||
}
|
||||
|
||||
static boolean isReachableBiome(Iterable<IrisBiome> biomes, String biomeKey) {
|
||||
@@ -135,7 +136,8 @@ final class ModdedCommandSuggestions {
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder);
|
||||
return SharedSuggestionProvider.suggest(GenerationFindCatalog.regions(engine).stream()
|
||||
.map(region -> region.getLoadKey()), builder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("region keys", context.getSource(), e);
|
||||
@@ -148,7 +150,7 @@ final class ModdedCommandSuggestions {
|
||||
try {
|
||||
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
|
||||
if (engine != null) {
|
||||
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
|
||||
return SharedSuggestionProvider.suggest(GenerationFindCatalog.objectKeys(engine), builder);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
warnTabFailure("object keys", context.getSource(), e);
|
||||
@@ -191,6 +193,7 @@ final class ModdedCommandSuggestions {
|
||||
Collection<String> unregisteredIrisKeys = eligibleUnregisteredEditableKeys(
|
||||
irisKeys, registeredKeys,
|
||||
(String candidate) -> IrisStructureLocator.hasNativePlacement(engine, candidate));
|
||||
nativeKeys.addAll(GenerationFindCatalog.retainedStructureKeys(engine));
|
||||
return SharedSuggestionProvider.suggest(
|
||||
combineStructureKeys(unregisteredIrisKeys, nativeKeys), builder);
|
||||
} catch (Throwable e) {
|
||||
|
||||
+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.StructureReachability;
|
||||
import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||
import art.arcane.iris.engine.history.GenerationSemanticQueries;
|
||||
import art.arcane.iris.engine.history.GenerationFindCatalog;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureQuery;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisNativeStructureDecision;
|
||||
@@ -86,15 +87,11 @@ final class ModdedLocateCommands {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_8));
|
||||
return 0;
|
||||
}
|
||||
IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim());
|
||||
IrisBiome biome = GenerationFindCatalog.biome(engine, key);
|
||||
if (biome == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_BIOME_2, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
if (!ModdedCommandSuggestions.isReachableBiome(engine, biome.getLoadKey())) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", biome.getLoadKey())));
|
||||
return 0;
|
||||
}
|
||||
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
|
||||
return 1;
|
||||
}
|
||||
@@ -111,15 +108,11 @@ final class ModdedLocateCommands {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_9));
|
||||
return 0;
|
||||
}
|
||||
IrisRegion region = engine.getData().getRegionLoader().load(key.trim());
|
||||
IrisRegion region = GenerationFindCatalog.region(engine, key);
|
||||
if (region == null) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNKNOWN_REGION_2, MessageArgument.untrusted("key", key)));
|
||||
return 0;
|
||||
}
|
||||
if (!engine.getDimension().getRegions().contains(region.getLoadKey())) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_DEFINED_DIMENSION, MessageArgument.untrusted("value", region.getLoadKey())));
|
||||
return 0;
|
||||
}
|
||||
locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
|
||||
return 1;
|
||||
}
|
||||
@@ -132,7 +125,7 @@ final class ModdedLocateCommands {
|
||||
return 0;
|
||||
}
|
||||
String key = keyRaw.trim();
|
||||
if (!engine.hasObjectPlacement(key)) {
|
||||
if (!GenerationFindCatalog.hasObjectPlacement(engine, key)) {
|
||||
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_IS_NOT_CONFIGURED_ANY_REGION_BIOME_OBJECT_PLACEMENTS_OBJECT_KEYS, MessageArgument.untrusted("key", key), MessageArgument.untrusted("value", engine.getData().getObjectLoader().getPossibleKeys().length)));
|
||||
return 0;
|
||||
}
|
||||
@@ -158,10 +151,6 @@ final class ModdedLocateCommands {
|
||||
return 0;
|
||||
}
|
||||
IrisHydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
||||
if (runtime == null) {
|
||||
IrisModdedCommands.fail(source, "Hydrology is not active in this dimension.");
|
||||
return 0;
|
||||
}
|
||||
HydrologyFeatureQuery query;
|
||||
try {
|
||||
query = HydrologyFeatureQuery.parse(type);
|
||||
@@ -171,15 +160,19 @@ final class ModdedLocateCommands {
|
||||
}
|
||||
int originX = player.blockPosition().getX();
|
||||
int originZ = player.blockPosition().getZ();
|
||||
int requestedDistance = Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
||||
int requestedDistance = runtime == null
|
||||
? 8192
|
||||
: Math.min(8192, runtime.settings().routing().tileSize() * 15);
|
||||
int maximumDistance = runtime == null
|
||||
? requestedDistance
|
||||
: runtime.maximumFeatureSearchDistance(originX, originZ, requestedDistance);
|
||||
MinecraftServer server = source.getServer();
|
||||
IrisModdedCommands.ok(source, "Searching accepted hydrology plans for " + type + "...");
|
||||
MultiBurst.burst.completeValueAsync(() -> runtime.nearestFeature(
|
||||
query.types(), query.profileKey(), originX, originZ, maximumDistance,
|
||||
MultiBurst.burst.completeValueAsync(() -> GenerationSemanticQueries.nearestRiver(
|
||||
engine, query, originX, originZ, maximumDistance,
|
||||
(int visited) -> server.execute(() -> IrisModdedCommands.ok(source,
|
||||
"Searched " + visited + " hydrology tiles for " + type + "..."))).orElse(null))
|
||||
.whenComplete((HydrologyFeatureRef feature, Throwable error) -> server.execute(() -> {
|
||||
.whenComplete((GenerationSemanticQueries.RiverResult feature, Throwable error) -> server.execute(() -> {
|
||||
if (error != null) {
|
||||
ModdedIrisLog.error("Hydrology locate failed for {}", type, error);
|
||||
IrisModdedCommands.fail(source,
|
||||
@@ -192,13 +185,12 @@ final class ModdedLocateCommands {
|
||||
+ maximumDistance + " blocks.");
|
||||
return;
|
||||
}
|
||||
int worldY = feature.y() + engine.getDimension().getMinHeight() + 2;
|
||||
teleportToStructure(
|
||||
source,
|
||||
level,
|
||||
player,
|
||||
feature.x(),
|
||||
worldY,
|
||||
feature.y() + 2,
|
||||
feature.z(),
|
||||
type + " hydrology feature"
|
||||
);
|
||||
@@ -225,8 +217,9 @@ final class ModdedLocateCommands {
|
||||
}
|
||||
Optional<NativeStructureTarget> resolved = resolveNativeStructure(source, level, engine, key);
|
||||
if (resolved.isEmpty()) {
|
||||
if (!IrisStructureLocator.hasNativePlacement(engine, key)
|
||||
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key)) {
|
||||
if ((!IrisStructureLocator.hasNativePlacement(engine, key)
|
||||
&& IrisStructureLocator.hasLocatableEditablePlacement(engine, key))
|
||||
|| GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)) {
|
||||
locateIrisStructure(source, level, engine, player, key);
|
||||
return 1;
|
||||
}
|
||||
@@ -247,6 +240,10 @@ final class ModdedLocateCommands {
|
||||
if (!ModdedCommandSuggestions.isEligibleRegisteredStructure(
|
||||
decision, nativePlacement, locatableNativePlacement, locatableEditableReplacement,
|
||||
reachable, nativeGenerationEnabled)) {
|
||||
if (GenerationFindCatalog.hasRetainedStructurePlacement(engine, key)) {
|
||||
locateIrisStructure(source, level, engine, player, key);
|
||||
return 1;
|
||||
}
|
||||
IrisModdedCommands.fail(source, registeredStructureUnavailableMessage(
|
||||
target.key(), target.availability(), decision,
|
||||
nativePlacement, locatableNativePlacement,
|
||||
@@ -312,7 +309,7 @@ final class ModdedLocateCommands {
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
IrisStructureLocator.LocateResult result =
|
||||
IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
|
||||
GenerationSemanticQueries.nearestStructure(engine, key, blockX, blockZ, 1024);
|
||||
if (result.status() == IrisStructureLocator.LocateStatus.SEARCH_LIMIT_REACHED) {
|
||||
server.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_UNABLE_LOCATE_IRIS_PLACED_STRUCTURE_DENSITY_SEARCH_SAFETY_LIMIT_WAS, MessageArgument.untrusted("key", key))));
|
||||
return;
|
||||
|
||||
+1
-1
@@ -488,7 +488,7 @@ public final class ModdedStudioCommands {
|
||||
ModdedDimensionManager.Handle handle = null;
|
||||
try {
|
||||
replaceExistingStudio(server, owner, dimensionId);
|
||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
|
||||
handle = ModdedDimensionManager.createTransientStudio(server, dimensionId, pack, pack, seed);
|
||||
STUDIOS.put(owner, dimensionId);
|
||||
if (owner.equals(CONSOLE_OWNER)) {
|
||||
completeConsoleOpen(source, dimensionId, pack, seed, handle);
|
||||
|
||||
+56
@@ -78,6 +78,7 @@ public final class ModdedWorldCommands {
|
||||
|
||||
root.then(enableTree("enable"));
|
||||
root.then(enableTree("create"));
|
||||
root.then(updateTree());
|
||||
root.then(replaceOverworldTree());
|
||||
root.then(mainWorldTree());
|
||||
|
||||
@@ -110,6 +111,20 @@ public final class ModdedWorldCommands {
|
||||
.executes(ModdedCommandTree.localized((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), false))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> updateTree() {
|
||||
return Commands.literal("update")
|
||||
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
|
||||
.then(Commands.argument("pack", StringArgumentType.string())
|
||||
.suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes(ModdedCommandTree.localized(
|
||||
(CommandContext<CommandSourceStack> context) -> update(
|
||||
context.getSource(),
|
||||
dimensionArgument(context),
|
||||
StringArgumentType.getString(context, "pack")
|
||||
)
|
||||
))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> deleteTree(String name) {
|
||||
return Commands.literal(name)
|
||||
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
|
||||
@@ -197,6 +212,47 @@ public final class ModdedWorldCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int update(CommandSourceStack source, String targetDimension, String packRaw) {
|
||||
String dimensionId;
|
||||
try {
|
||||
dimensionId = normalizeDimensionId(targetDimension);
|
||||
} catch (IllegalArgumentException failure) {
|
||||
IrisModdedCommands.fail(source, failure.getMessage());
|
||||
return 0;
|
||||
}
|
||||
String[] packRef = parsePackRef(packRaw);
|
||||
String pack = packRef[0];
|
||||
String packDimension = packRef[1];
|
||||
if (!validPackRef(source, pack, packDimension)
|
||||
|| blockIfPackBroken(source, dimensionId, pack)
|
||||
|| !loadPackDimension(source, pack, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
ModdedDimensionManager.UpdateResult result = ModdedDimensionManager.stagePersistentUpdate(
|
||||
source.getServer(),
|
||||
dimensionId,
|
||||
pack,
|
||||
packDimension
|
||||
);
|
||||
if (result.restartRequired()) {
|
||||
IrisModdedCommands.ok(source, "Iris world update staged for " + dimensionId
|
||||
+ " from " + pack + ":" + packDimension
|
||||
+ ". Restart the server to activate it; the running world remains on its current pack.");
|
||||
} else {
|
||||
IrisModdedCommands.ok(source, "Iris world " + dimensionId
|
||||
+ " already uses this immutable pack; no restart is required.");
|
||||
}
|
||||
return 1;
|
||||
} catch (Throwable failure) {
|
||||
ModdedIrisLog.error("Iris world update staging failed for {} (pack={} dim={})",
|
||||
dimensionId, pack, packDimension, failure);
|
||||
IrisModdedCommands.fail(source, "Iris world update failed for " + dimensionId
|
||||
+ ": " + IrisLanguage.errorDetail(failure));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int replaceOverworld(CommandSourceStack source, String packRaw, String seedRaw) {
|
||||
MinecraftServer server = source.getServer();
|
||||
Long seed = parseSeed(source, seedRaw);
|
||||
|
||||
+4
-2
@@ -17,6 +17,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -126,10 +127,11 @@ public class IrisModdedStructureParityTest {
|
||||
.setInferredType(InferredType.SEA);
|
||||
|
||||
Set<String> keys = ModdedDimensionMetadata.collectConfiguredBiomeKeys(
|
||||
List.of(ocean, custom, shore, unsafeSea), "OverWorld");
|
||||
List.of(ocean, custom, shore, unsafeSea),
|
||||
customBiome -> "iris:biomes/" + customBiome.getId().toLowerCase(Locale.ROOT));
|
||||
|
||||
assertEquals(Set.of("minecraft:deep_ocean", "minecraft:forest", "minecraft:beach",
|
||||
"minecraft:the_void", "overworld:aurora"), keys);
|
||||
"minecraft:the_void", "iris:biomes/aurora"), keys);
|
||||
assertFalse(keys.contains("minecraft:desert"));
|
||||
assertFalse(keys.contains("minecraft:plains"));
|
||||
}
|
||||
|
||||
+31
@@ -52,6 +52,19 @@ public class ModdedForcedDatapackTest {
|
||||
assertEquals("iris:overworld", ModdedWorldgenIds.generatorIdentity("overworld"));
|
||||
assertEquals("iris:other/overworld",
|
||||
ModdedWorldgenIds.generatorIdentity("other:overworld"));
|
||||
assertEquals(
|
||||
new ModdedWorldgenIds.ScopedDimension("overworld", "overworld"),
|
||||
ModdedWorldgenIds.scopedDimensionType(
|
||||
ModdedWorldgenIds.dimensionTypeRef("overworld", "overworld")
|
||||
).orElseThrow()
|
||||
);
|
||||
assertEquals(
|
||||
List.of(
|
||||
ModdedWorldgenIds.biomeRef("overworld", "overworld", "mist"),
|
||||
"overworld:mist"
|
||||
),
|
||||
ModdedWorldgenIds.legacyBiomeRefs("overworld", "overworld", "mist")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -148,6 +161,24 @@ public class ModdedForcedDatapackTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsConflictingRegistryDefinitionsDuringMerge() throws IOException {
|
||||
Path root = Files.createTempDirectory("iris-forced-pack-conflict");
|
||||
try {
|
||||
Path source = Files.createDirectories(root.resolve("source/data/iris/worldgen/biome"));
|
||||
Path destination = Files.createDirectories(root.resolve("destination/data/iris/worldgen/biome"));
|
||||
Files.writeString(source.resolve("shared.json"), "{\"temperature\":1}", StandardCharsets.UTF_8);
|
||||
Files.writeString(destination.resolve("shared.json"), "{\"temperature\":2}", StandardCharsets.UTF_8);
|
||||
|
||||
assertThrows(
|
||||
IOException.class,
|
||||
() -> ModdedForcedDatapack.mergeDirectory(root.resolve("source"), root.resolve("destination"))
|
||||
);
|
||||
} finally {
|
||||
deleteTree(root);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> directoryEntries(Path root) throws IOException {
|
||||
try (Stream<Path> entries = Files.list(root)) {
|
||||
return entries.map((Path path) -> path.getFileName().toString())
|
||||
|
||||
+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("e.isExpectedTeardown()"));
|
||||
assertTrue(source.contains("Iris structure biome key lookup was rejected during an engine transition"));
|
||||
assertTrue(source.contains("source.clearCache()"));
|
||||
assertTrue(source.contains("int runtimeIdentity = engine.getCacheID();"));
|
||||
assertTrue(source.contains("cache.get(runtimeIdentity, key)"));
|
||||
assertTrue(source.contains("cache.put(runtimeIdentity, key, resolved)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -203,7 +205,7 @@ public class ModdedGenerationLeaseContractTest {
|
||||
String jobSource = source("art/arcane/iris/modded/command/ModdedPregenJob.java");
|
||||
String start = method(jobSource, "public static boolean start(");
|
||||
int taskConstruction = start.indexOf("PregenTask task = PregenTask.builder()");
|
||||
int profileMutation = start.indexOf("PregenPerformanceProfile.apply(engine);");
|
||||
int profileMutation = start.indexOf("PregenPerformanceProfile.apply(engine)");
|
||||
|
||||
assertTrue(taskConstruction >= 0);
|
||||
assertTrue(profileMutation > taskConstruction);
|
||||
|
||||
+36
-21
@@ -14,15 +14,16 @@ public class ModdedLifecycleFailureContractTest {
|
||||
private static final String SOURCE_ROOT_PROPERTY = "iris.moddedCommonSources";
|
||||
|
||||
@Test
|
||||
public void existingDimensionRepointBindsBeforeHandlePublication() throws IOException {
|
||||
public void existingDimensionStagesWithoutReplacingTheLiveEngine() throws IOException {
|
||||
String managerSource = source("ModdedDimensionManager.java");
|
||||
String create = method(managerSource, "public static Handle create(");
|
||||
String create = method(managerSource, "private static Handle create(");
|
||||
int existingStart = requiredIndex(create, "if (existing != null");
|
||||
int presentStart = requiredIndex(create, "if (serverAccess.hasLevel(server, key))", existingStart + 1);
|
||||
int injectionStart = requiredIndex(create, "try {", presentStart);
|
||||
|
||||
assertRepointBeforePublication(create.substring(existingStart, presentStart));
|
||||
assertRepointBeforePublication(create.substring(presentStart, injectionStart));
|
||||
assertFalse(create.substring(existingStart, presentStart).contains("repointAndBind("));
|
||||
assertFalse(create.substring(presentStart, injectionStart).contains("repointAndBind("));
|
||||
assertBefore(create, "ModdedGenerationHistoryStorage.createOrStage(", "if (existing != null");
|
||||
|
||||
String generatorSource = source("IrisModdedChunkGenerator.java");
|
||||
String repointAndBind = method(generatorSource, "void repointAndBind(");
|
||||
@@ -40,6 +41,31 @@ public class ModdedLifecycleFailureContractTest {
|
||||
assertFalse(installReplacement.contains("catch ("));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistentPackUpdatesUseAnExplicitRestartBoundPath() throws IOException {
|
||||
String managerSource = source("ModdedDimensionManager.java");
|
||||
String createPersistent = method(managerSource, "public static Handle createPersistent(");
|
||||
String update = method(managerSource, "public static UpdateResult stagePersistentUpdate(");
|
||||
|
||||
assertBefore(createPersistent, "if (previous != null)", "ModdedDimensionRegistryStore.put(");
|
||||
assertTrue(createPersistent.contains("Use the Iris world update command"));
|
||||
assertTrue(update.contains("ModdedGenerationHistoryStorage.createOrStage("));
|
||||
assertTrue(update.contains("active.history().pendingActivation()"));
|
||||
assertTrue(update.contains("restartRequired()"));
|
||||
assertFalse(update.contains("inject("));
|
||||
assertFalse(update.contains("repointAndBind("));
|
||||
assertBefore(update, "ModdedDimensionRegistryStore.put(server, updated);",
|
||||
"ModdedGenerationHistoryStorage.createOrStage(");
|
||||
assertTrue(update.contains("ModdedDimensionRegistryStore.put(server, previous);"));
|
||||
|
||||
String commandsSource = source("command/ModdedWorldCommands.java");
|
||||
assertTrue(commandsSource.contains("root.then(updateTree());"));
|
||||
String command = method(commandsSource, "private static int update(");
|
||||
assertTrue(command.contains("ModdedDimensionManager.stagePersistentUpdate("));
|
||||
assertTrue(command.contains("Restart the server to activate it"));
|
||||
assertTrue(command.contains("running world remains on its current pack"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void injectionBindsBeforeRegistrationAndRollsBackEveryFailure() throws IOException {
|
||||
String source = source("ModdedDimensionManager.java");
|
||||
@@ -110,18 +136,13 @@ public class ModdedLifecycleFailureContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void packDimensionLoadingNeverSuppressesTheFailureAsNull() throws IOException {
|
||||
String source = source("ModdedDimensionManager.java");
|
||||
String loading = method(source, "private static IrisDimension loadPackDimension(");
|
||||
public void persistentRestoreOpensHistoryBeforeConsultingTheGlobalPack() throws IOException {
|
||||
String source = source("ModdedGenerationHistoryStorage.java");
|
||||
String restore = method(source, "static GenerationHistory restoreOrAdopt(");
|
||||
|
||||
assertFalse(loading.contains("return null;"));
|
||||
assertFalse(loading.contains("e.toString()"));
|
||||
int catchIndex = loading.indexOf("catch (");
|
||||
if (catchIndex >= 0) {
|
||||
String failure = blockAt(loading, requiredIndex(loading, "{", catchIndex));
|
||||
assertTrue(failure.contains("throw "));
|
||||
assertTrue(failure.contains(", e);") || failure.contains("throw e;"));
|
||||
}
|
||||
assertBefore(restore, "GenerationHistory.openIfPresent(", "sourceSupplier\").get();");
|
||||
assertTrue(restore.contains("if (existing.isPresent())"));
|
||||
assertBefore(restore, "requireRegistryDefinitions(existing.get()", "return existing.get();");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -249,12 +270,6 @@ public class ModdedLifecycleFailureContractTest {
|
||||
assertBefore(failure, "rollback.restore(failure);", "throw ");
|
||||
}
|
||||
|
||||
private static void assertRepointBeforePublication(String branch) {
|
||||
assertBefore(branch, "repointAndBind(", "new Handle(");
|
||||
assertBefore(branch, "repointAndBind(", "HANDLES.put(");
|
||||
assertBefore(branch, "repointAndBind(", "return ");
|
||||
}
|
||||
|
||||
private static void assertBefore(String source, String first, String second) {
|
||||
int firstIndex = requiredIndex(source, first);
|
||||
int secondIndex = requiredIndex(source, second);
|
||||
|
||||
+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("));
|
||||
assertFalse(open.contains("orTimeout("));
|
||||
assertFalse(open.contains("deadlineNanos"));
|
||||
assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.create(");
|
||||
assertBefore(execute, "ModdedDimensionManager.create(", "ModdedDimensionManager.teleportAsync(");
|
||||
assertBefore(execute, "replaceExistingStudio(", "ModdedDimensionManager.createTransientStudio(");
|
||||
assertBefore(execute, "ModdedDimensionManager.createTransientStudio(", "ModdedDimensionManager.teleportAsync(");
|
||||
assertFalse(execute.contains("deadlineNanos"));
|
||||
assertFalse(source.contains("player.teleportTo(studio"));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user