This commit is contained in:
Brian Neumann-Fopiano
2026-08-15 21:32:00 -04:00
parent ef2e1b8f58
commit fc0fdf4ce4
81 changed files with 5109 additions and 511 deletions
@@ -0,0 +1,130 @@
package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
import art.arcane.iris.util.common.scheduling.J;
import io.papermc.paper.world.saveddata.PaperLevelOverrides;
import io.papermc.paper.world.saveddata.PaperWorldMetadata;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.storage.PrimaryLevelData;
import net.minecraft.world.level.storage.SavedDataStorage;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.CraftServer;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
final class CurrentPaperWorldDataWriter {
private static final long SNAPSHOT_TIMEOUT_SECONDS = 30L;
private CurrentPaperWorldDataWriter() {
}
static void write(
Path sourceWorldDirectory,
Path targetWorldDirectory,
long seed
) throws IOException {
CraftServer craftServer = (CraftServer) Bukkit.getServer();
MinecraftServer server = craftServer.getHandle().getServer();
PaperLevelOverrides levelOverrides = captureLevelOverrides(craftServer, server);
Path targetWorld = targetWorldDirectory.toAbsolutePath().normalize();
UUID metadataUuid = UUID.randomUUID();
WorldReplacementSeed.copyWithAuthoritativeSeed(sourceWorldDirectory, targetWorld, seed);
try (SavedDataStorage savedDataStorage = new SavedDataStorage(
targetWorld.resolve("data"),
server.getFixerUpper(),
server.registryAccess()
)) {
savedDataStorage.set(PaperWorldMetadata.TYPE, new PaperWorldMetadata(metadataUuid));
savedDataStorage.set(PaperLevelOverrides.TYPE, levelOverrides);
}
List<Path> requiredDataFiles = List.of(
targetWorld.resolve("data/minecraft/world_gen_settings.dat"),
targetWorld.resolve("data/paper/metadata.dat"),
targetWorld.resolve("data/paper/level_overrides.dat")
);
for (Path requiredDataFile : requiredDataFiles) {
if (!Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Current Paper world data was not written: " + requiredDataFile);
}
}
long writtenSeed = WorldReplacementSeed.readAuthoritativeSeed(targetWorld);
if (writtenSeed != seed) {
throw new IOException("Current Paper world data did not retain the requested seed.");
}
try (SavedDataStorage verificationStorage = new SavedDataStorage(
targetWorld.resolve("data"),
server.getFixerUpper(),
server.registryAccess()
)) {
PaperWorldMetadata metadata = verificationStorage.get(PaperWorldMetadata.TYPE);
if (metadata == null || !metadataUuid.equals(metadata.uuid())) {
throw new IOException("Current Paper world metadata could not be verified.");
}
PaperLevelOverrides overrides = verificationStorage.get(PaperLevelOverrides.TYPE);
if (overrides == null || overrides.isInitialized()) {
throw new IOException("Current Paper level overrides could not be verified.");
}
}
}
private static PaperLevelOverrides captureLevelOverrides(
CraftServer craftServer,
MinecraftServer server
) throws IOException {
if (craftServer.isGlobalTickThread()) {
return createLevelOverrides(craftServer, server);
}
if (J.isFolia() && J.isPrimaryThread()) {
throw new IOException("Current Paper world data cannot be staged from a Folia region tick thread.");
}
CompletableFuture<PaperLevelOverrides> captured = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
captured.complete(createLevelOverrides(craftServer, server));
} catch (Throwable failure) {
captured.completeExceptionally(failure);
}
});
if (!scheduled) {
throw new IOException("Could not schedule the current Paper level-data snapshot on the global thread.");
}
try {
return captured.get(SNAPSHOT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while capturing current Paper level data.", failure);
} catch (ExecutionException failure) {
throw new IOException("Could not capture current Paper level data.", failure.getCause());
} catch (TimeoutException failure) {
throw new IOException("Timed out while capturing current Paper level data.", failure);
}
}
private static PaperLevelOverrides createLevelOverrides(
CraftServer craftServer,
MinecraftServer server
) throws IOException {
if (!craftServer.isGlobalTickThread()) {
throw new IOException("Current Paper level data must be captured on the global tick thread.");
}
if (!(server.getWorldData().overworldData() instanceof PrimaryLevelData primaryLevelData)) {
throw new IOException("Paper primary level data is unavailable for current world data staging.");
}
return PaperLevelOverrides.createFromLiveLevelData(primaryLevelData);
}
}
@@ -7,7 +7,6 @@ import net.minecraft.core.Holder;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.Vec3i;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
@@ -41,16 +40,31 @@ final class DatapackStructureStateFilter {
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
IrisImportedStructureControl importedStructures
) {
return filter(
structureSets,
scopeIndex,
declaredSources,
importedStructures,
keyIndex(structureSets));
}
static Selection filter(
List<Holder<StructureSet>> structureSets,
DatapackStructureScopeIndex scopeIndex,
Set<String> declaredSources,
IrisImportedStructureControl importedStructures,
StructureSetKeyIndex keyIndex
) {
if (scopeIndex.isEmpty()) {
return new Selection(
scaleFrequencyOverrides(structureSets, importedStructures),
scaleFrequencyOverrides(structureSets, importedStructures, keyIndex),
0,
0);
}
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder);
String key = structureSetKey(holder, keyIndex);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
@@ -61,7 +75,7 @@ final class DatapackStructureStateFilter {
int retainedManagedSets = 0;
int excludedManagedSets = 0;
for (Holder<StructureSet> holder : structureSets) {
String setKey = structureSetKey(holder);
String setKey = structureSetKey(holder, keyIndex);
boolean managedSet = setKey != null && scopeIndex.isManagedStructureSet(setKey);
Holder<StructureSet> scopedHolder = scopeHolder(
holder,
@@ -69,7 +83,8 @@ final class DatapackStructureStateFilter {
declaredSources,
holdersByKey,
scopedByIdentity,
visiting);
visiting,
keyIndex);
if (scopedHolder == null) {
if (managedSet) {
excludedManagedSets++;
@@ -82,21 +97,29 @@ final class DatapackStructureStateFilter {
filteredSets.add(scopedHolder);
}
return new Selection(
scaleFrequencyOverrides(List.copyOf(filteredSets), importedStructures),
scaleFrequencyOverrides(
List.copyOf(filteredSets), importedStructures, keyIndex),
retainedManagedSets,
excludedManagedSets);
}
static String structureSetKey(Holder<StructureSet> holder) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isPresent()) {
return key.get().identifier().toString();
}
if (holder.value().placement()
instanceof ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement keyedPlacement) {
return keyedPlacement.key.identifier().toString();
}
return null;
return structureSetKey(holder, keyIndex(List.of(holder)));
}
static StructureSetKeyIndex keyIndex(
Iterable<? extends Holder<StructureSet>> registeredStructureSets
) {
return StructureSetKeyIndex.create(registeredStructureSets);
}
private static String structureSetKey(
Holder<StructureSet> holder,
StructureSetKeyIndex keyIndex
) {
return keyIndex.resolve(holder)
.map(key -> key.identifier().toString())
.orElse(null);
}
private static List<StructureSet.StructureSelectionEntry> filterEntries(
@@ -122,7 +145,8 @@ final class DatapackStructureStateFilter {
Set<String> declaredSources,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
Set<Holder<StructureSet>> visiting
Set<Holder<StructureSet>> visiting,
StructureSetKeyIndex keyIndex
) {
if (scopedByIdentity.containsKey(holder)) {
return scopedByIdentity.get(holder);
@@ -131,7 +155,7 @@ final class DatapackStructureStateFilter {
return null;
}
try {
String setKey = structureSetKey(holder);
String setKey = structureSetKey(holder, keyIndex);
if (setKey != null && scopeIndex.isManagedStructureSet(setKey)
&& !scopeIndex.allowsStructureSet(setKey, declaredSources)) {
scopedByIdentity.put(holder, null);
@@ -150,14 +174,15 @@ final class DatapackStructureStateFilter {
declaredSources,
holdersByKey,
scopedByIdentity,
visiting);
visiting,
keyIndex);
if (entries.size() == originalSet.structures().size()
&& placement == originalSet.placement()) {
scopedByIdentity.put(holder, holder);
return holder;
}
Holder<StructureSet> scoped = replacementHolder(
holder, new StructureSet(entries, placement));
holder, new StructureSet(entries, placement), keyIndex);
scopedByIdentity.put(holder, scoped);
return scoped;
} finally {
@@ -184,7 +209,8 @@ final class DatapackStructureStateFilter {
Set<String> declaredSources,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scopedByIdentity,
Set<Holder<StructureSet>> visiting
Set<Holder<StructureSet>> visiting,
StructureSetKeyIndex keyIndex
) {
Optional<StructurePlacement.ExclusionZone> currentZone = exclusionZone(placement);
if (currentZone.isEmpty()) {
@@ -192,7 +218,7 @@ final class DatapackStructureStateFilter {
}
Holder<StructureSet> target = currentZone.get().otherSet();
String targetKey = structureSetKey(target);
String targetKey = structureSetKey(target, keyIndex);
if (targetKey != null) {
target = holdersByKey.getOrDefault(targetKey, target);
}
@@ -202,7 +228,8 @@ final class DatapackStructureStateFilter {
declaredSources,
holdersByKey,
scopedByIdentity,
visiting);
visiting,
keyIndex);
Optional<StructurePlacement.ExclusionZone> scopedZone = Optional.empty();
if (scopedTarget != null) {
scopedZone = Optional.of(new StructurePlacement.ExclusionZone(
@@ -217,14 +244,15 @@ final class DatapackStructureStateFilter {
private static List<Holder<StructureSet>> scaleFrequencyOverrides(
List<Holder<StructureSet>> structureSets,
IrisImportedStructureControl importedStructures
IrisImportedStructureControl importedStructures,
StructureSetKeyIndex keyIndex
) {
if (!importedStructures.hasFrequencyOverrides()) {
return structureSets;
}
Map<String, Holder<StructureSet>> holdersByKey = new HashMap<>();
for (Holder<StructureSet> holder : structureSets) {
String key = structureSetKey(holder);
String key = structureSetKey(holder, keyIndex);
if (key != null) {
holdersByKey.putIfAbsent(key, holder);
}
@@ -234,7 +262,11 @@ final class DatapackStructureStateFilter {
boolean changed = false;
for (Holder<StructureSet> holder : structureSets) {
Holder<StructureSet> scaled = scaleFrequencyHolder(
holder, importedStructures, holdersByKey, scaledByIdentity);
holder,
importedStructures,
holdersByKey,
scaledByIdentity,
keyIndex);
scaledSets.add(scaled);
changed |= scaled != holder;
}
@@ -245,17 +277,19 @@ final class DatapackStructureStateFilter {
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey,
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity
Map<Holder<StructureSet>, Holder<StructureSet>> scaledByIdentity,
StructureSetKeyIndex keyIndex
) {
if (scaledByIdentity.containsKey(holder)) {
return scaledByIdentity.get(holder);
}
if (!dependsOnFrequencyOverride(holder, importedStructures, holdersByKey)) {
if (!dependsOnFrequencyOverride(
holder, importedStructures, holdersByKey, keyIndex)) {
scaledByIdentity.put(holder, holder);
return holder;
}
ResourceKey<StructureSet> holderKey = structureSetResourceKey(holder).orElseThrow(() ->
ResourceKey<StructureSet> holderKey = keyIndex.resolve(holder).orElseThrow(() ->
new IllegalStateException(
"An affected native structure-set exclusion graph has an unkeyed holder"));
ReboundStructureSetHolder scaledHolder = new ReboundStructureSetHolder(holderKey);
@@ -267,16 +301,21 @@ final class DatapackStructureStateFilter {
Optional<StructurePlacement.ExclusionZone> scaledZone = originalZone;
if (originalZone.isPresent()) {
Holder<StructureSet> target = canonicalHolder(
originalZone.get().otherSet(), holdersByKey);
originalZone.get().otherSet(), holdersByKey, keyIndex);
Holder<StructureSet> scaledTarget = scaleFrequencyHolder(
target, importedStructures, holdersByKey, scaledByIdentity);
target,
importedStructures,
holdersByKey,
scaledByIdentity,
keyIndex);
if (scaledTarget != originalZone.get().otherSet()) {
scaledZone = Optional.of(new StructurePlacement.ExclusionZone(
scaledTarget, originalZone.get().chunkCount()));
}
}
double multiplier = importedStructures.frequencyMultiplier(structureSetKey(holder));
double multiplier = importedStructures.frequencyMultiplier(
structureSetKey(holder, keyIndex));
StructurePlacement scaledPlacement = multiplier == 1D && scaledZone.equals(originalZone)
? originalPlacement
: copyPlacement(originalPlacement, scaledZone, multiplier);
@@ -287,12 +326,14 @@ final class DatapackStructureStateFilter {
private static boolean dependsOnFrequencyOverride(
Holder<StructureSet> holder,
IrisImportedStructureControl importedStructures,
Map<String, Holder<StructureSet>> holdersByKey
Map<String, Holder<StructureSet>> holdersByKey,
StructureSetKeyIndex keyIndex
) {
Set<Holder<StructureSet>> visited = Collections.newSetFromMap(new IdentityHashMap<>());
Holder<StructureSet> current = holder;
while (visited.add(current)) {
if (importedStructures.frequencyMultiplier(structureSetKey(current)) != 1D) {
if (importedStructures.frequencyMultiplier(
structureSetKey(current, keyIndex)) != 1D) {
return true;
}
Optional<StructurePlacement.ExclusionZone> zone =
@@ -300,24 +341,26 @@ final class DatapackStructureStateFilter {
if (zone.isEmpty()) {
return false;
}
current = canonicalHolder(zone.get().otherSet(), holdersByKey);
current = canonicalHolder(zone.get().otherSet(), holdersByKey, keyIndex);
}
return false;
}
private static Holder<StructureSet> canonicalHolder(
Holder<StructureSet> holder,
Map<String, Holder<StructureSet>> holdersByKey
Map<String, Holder<StructureSet>> holdersByKey,
StructureSetKeyIndex keyIndex
) {
String key = structureSetKey(holder);
String key = structureSetKey(holder, keyIndex);
return key == null ? holder : holdersByKey.getOrDefault(key, holder);
}
private static Holder<StructureSet> replacementHolder(
Holder<StructureSet> original,
StructureSet replacement
StructureSet replacement,
StructureSetKeyIndex keyIndex
) {
Optional<ResourceKey<StructureSet>> key = structureSetResourceKey(original);
Optional<ResourceKey<StructureSet>> key = keyIndex.resolve(original);
if (key.isEmpty()) {
return Holder.direct(replacement);
}
@@ -326,20 +369,6 @@ final class DatapackStructureStateFilter {
return holder;
}
private static Optional<ResourceKey<StructureSet>> structureSetResourceKey(
Holder<StructureSet> holder
) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isPresent()) {
return key;
}
if (holder.value().placement()
instanceof ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement keyedPlacement) {
return Optional.of(keyedPlacement.key);
}
return Optional.empty();
}
private static StructurePlacement copyPlacement(
StructurePlacement placement,
Optional<StructurePlacement.ExclusionZone> exclusionZone,
@@ -357,26 +386,7 @@ final class DatapackStructureStateFilter {
int salt = (int) declaredFieldValue(
StructurePlacement.class, placement, int.class);
if (placement.getClass()
== ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement.class) {
ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement keyedPlacement =
(ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement) placement;
NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread(
frequency, keyedPlacement.spacing(), keyedPlacement.separation(), frequencyMultiplier);
return new ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement(
keyedPlacement.key,
locateOffset,
frequencyReductionMethod,
scale.frequency(),
salt,
exclusionZone,
scale.spacing(),
keyedPlacement.separation(),
keyedPlacement.spreadType());
}
if (placement.getClass() == RandomSpreadStructurePlacement.class) {
RandomSpreadStructurePlacement randomSpread =
(RandomSpreadStructurePlacement) placement;
if (placement instanceof RandomSpreadStructurePlacement randomSpread) {
NativeStructureFrequencyScale scale = NativeStructureFrequencyScale.randomSpread(
frequency, randomSpread.spacing(), randomSpread.separation(), frequencyMultiplier);
return new RandomSpreadStructurePlacement(
@@ -439,6 +449,46 @@ final class DatapackStructureStateFilter {
}
}
static final class StructureSetKeyIndex {
private final Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
keysByEntries;
private StructureSetKeyIndex(
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>>
keysByEntries
) {
this.keysByEntries = keysByEntries;
}
private static StructureSetKeyIndex create(
Iterable<? extends Holder<StructureSet>> registeredStructureSets
) {
Map<List<StructureSet.StructureSelectionEntry>, ResourceKey<StructureSet>> keys =
new IdentityHashMap<>();
for (Holder<StructureSet> holder : registeredStructureSets) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isEmpty()) {
continue;
}
List<StructureSet.StructureSelectionEntry> entries = holder.value().structures();
ResourceKey<StructureSet> previous = keys.put(entries, key.get());
if (previous != null && !previous.equals(key.get())) {
throw new IllegalStateException(
"Multiple registered structure sets share the same entry list");
}
}
return new StructureSetKeyIndex(keys);
}
private Optional<ResourceKey<StructureSet>> resolve(Holder<StructureSet> holder) {
Optional<ResourceKey<StructureSet>> key = holder.unwrapKey();
if (key.isPresent()) {
return key;
}
return Optional.ofNullable(keysByEntries.get(holder.value().structures()));
}
}
record Selection(
List<Holder<StructureSet>> structureSets,
int retainedManagedSets,
@@ -15,7 +15,6 @@ import art.arcane.iris.core.nms.container.BiomeColor;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.nms.datapack.DataVersion;
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.engine.data.chunk.TerrainChunk;
import art.arcane.iris.engine.framework.Engine;
@@ -55,8 +54,6 @@ import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import art.arcane.iris.util.common.scheduling.J;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.shorts.ShortList;
import io.papermc.paper.world.saveddata.PaperLevelOverrides;
import io.papermc.paper.world.saveddata.PaperWorldMetadata;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
@@ -119,14 +116,14 @@ import net.minecraft.world.level.levelgen.XoroshiroRandomSource;
import net.minecraft.world.level.levelgen.structure.StructureCheck;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure;
import net.minecraft.world.level.storage.PrimaryLevelData;
import net.minecraft.world.level.storage.SavedDataStorage;
import net.minecraft.world.level.levelgen.feature.AbstractHugeMushroomFeature;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import net.minecraft.world.level.levelgen.feature.FallenTreeFeature;
import net.minecraft.world.level.levelgen.feature.Feature;
import net.minecraft.world.level.levelgen.feature.TreeFeature;
import net.minecraft.world.level.storage.LevelStorageSource;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import org.bukkit.Bukkit;
@@ -162,8 +159,6 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
@@ -173,23 +168,18 @@ import java.util.Map;
import java.util.Optional;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class NMSBinding implements INMSBinding {
private static final long CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS = 30L;
private final KMap<Biome, Object> baseBiomeCache = new KMap<>();
private volatile DataVersion dataVersion;
private final BlockData AIR = Material.AIR.createBlockData();
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
private final AtomicBoolean injected = new AtomicBoolean();
private volatile ResettableClassFileTransformer levelStorageAccessTransformer;
private volatile ResettableClassFileTransformer serverLevelTransformer;
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
@@ -1249,8 +1239,15 @@ public class NMSBinding implements INMSBinding {
ChunkGeneratorStructureState currentState = level.getChunkSource().getGeneratorState();
net.minecraft.world.level.chunk.ChunkGenerator generator = level.getChunkSource().getGenerator();
ChunkGeneratorStructureState scopedState = createStructureState(level, generator, currentState);
Registry<StructureSet> structureSetRegistry =
level.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET);
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
scopedState.possibleStructureSets(), scopeIndex, declaredSources, importedStructures);
scopedState.possibleStructureSets(),
scopeIndex,
declaredSources,
importedStructures,
DatapackStructureStateFilter.keyIndex(
structureSetRegistry.listElements().toList()));
Field possibleSetsField = getField(scopedState.getClass(), List.class);
possibleSetsField.setAccessible(true);
@@ -1328,7 +1325,7 @@ public class NMSBinding implements INMSBinding {
level.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
currentState.randomState(),
currentState.getLevelSeed(),
currentState.conf);
level.spigotConfig);
}
private void initializeAndPublishStructureState(
@@ -1542,6 +1539,18 @@ public class NMSBinding implements INMSBinding {
}
try {
IrisLogging.info("Injecting Bukkit");
levelStorageAccessTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(LevelStorageAccessAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArguments(4))
.and(ElementMatchers.takesArgument(0, LevelStorageSource.class))
.and(ElementMatchers.takesArgument(1, String.class))
.and(ElementMatchers.takesArgument(2, Path.class))
.and(ElementMatchers.takesArgument(3, ResourceKey.class)))))
.installOn(Agent.getInstrumentation());
serverLevelTransformer = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
@@ -1549,8 +1558,7 @@ public class NMSBinding implements INMSBinding {
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
.and(ElementMatchers.takesArgument(5, LevelStem.class))
.and(ElementMatchers.takesArgument(12, ChunkGenerator.class)))))
.and(ElementMatchers.takesArgument(5, LevelStem.class)))))
.installOn(Agent.getInstrumentation());
ByteBuddy buddy = new ByteBuddy();
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
@@ -1565,12 +1573,17 @@ public class NMSBinding implements INMSBinding {
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to inject Bukkit");
e.printStackTrace();
// The ServerLevel transformer may already be installed when the ChunkAccess
// redefine throws; remove it or a retry would stack a second one and orphan
// this one permanently.
ResettableClassFileTransformer partial = serverLevelTransformer;
ResettableClassFileTransformer partialServerLevel = serverLevelTransformer;
ResettableClassFileTransformer partialStorageAccess = levelStorageAccessTransformer;
serverLevelTransformer = null;
if (partial != null) {
levelStorageAccessTransformer = null;
for (ResettableClassFileTransformer partial : new ResettableClassFileTransformer[]{
partialServerLevel,
partialStorageAccess
}) {
if (partial == null) {
continue;
}
try {
partial.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable ignored) {
@@ -1586,26 +1599,36 @@ public class NMSBinding implements INMSBinding {
if (!injected.get())
return;
try {
Agent.getInstrumentation().retransformClasses(ServerLevel.class);
Agent.getInstrumentation().retransformClasses(
LevelStorageSource.LevelStorageAccess.class,
ServerLevel.class
);
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to re-apply ServerLevel injection");
IrisLogging.error(C.RED + "Failed to re-apply Bukkit world lifecycle injection");
}
}
@Override
public void uninjectBukkit() {
synchronized (injected) {
ResettableClassFileTransformer transformer = serverLevelTransformer;
ResettableClassFileTransformer activeServerLevel = serverLevelTransformer;
ResettableClassFileTransformer activeStorageAccess = levelStorageAccessTransformer;
serverLevelTransformer = null;
levelStorageAccessTransformer = null;
injected.set(false);
if (transformer == null) {
return;
}
try {
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to remove ServerLevel injection");
e.printStackTrace();
for (ResettableClassFileTransformer transformer : new ResettableClassFileTransformer[]{
activeServerLevel,
activeStorageAccess
}) {
if (transformer == null) {
continue;
}
try {
transformer.reset(Agent.getInstrumentation(), AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
} catch (Throwable e) {
IrisLogging.error(C.RED + "Failed to remove Bukkit world lifecycle injection");
e.printStackTrace();
}
}
}
}
@@ -1616,103 +1639,7 @@ public class NMSBinding implements INMSBinding {
Path targetWorldDirectory,
long seed
) throws IOException {
CraftServer craftServer = (CraftServer) Bukkit.getServer();
MinecraftServer server = craftServer.getHandle().getServer();
PaperLevelOverrides levelOverrides = captureCurrentPaperLevelOverrides(craftServer, server);
Path targetWorld = targetWorldDirectory.toAbsolutePath().normalize();
UUID metadataUuid = UUID.randomUUID();
WorldReplacementSeed.copyWithAuthoritativeSeed(sourceWorldDirectory, targetWorld, seed);
try (SavedDataStorage savedDataStorage = new SavedDataStorage(
targetWorld.resolve("data"),
server.getFixerUpper(),
server.registryAccess()
)) {
savedDataStorage.set(PaperWorldMetadata.TYPE, new PaperWorldMetadata(metadataUuid));
savedDataStorage.set(
PaperLevelOverrides.TYPE,
levelOverrides
);
}
List<Path> requiredDataFiles = List.of(
targetWorld.resolve("data/minecraft/world_gen_settings.dat"),
targetWorld.resolve("data/paper/metadata.dat"),
targetWorld.resolve("data/paper/level_overrides.dat")
);
for (Path requiredDataFile : requiredDataFiles) {
if (!Files.isRegularFile(requiredDataFile, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Current Paper world data was not written: " + requiredDataFile);
}
}
long writtenSeed = WorldReplacementSeed.readAuthoritativeSeed(targetWorld);
if (writtenSeed != seed) {
throw new IOException("Current Paper world data did not retain the requested seed.");
}
try (SavedDataStorage verificationStorage = new SavedDataStorage(
targetWorld.resolve("data"),
server.getFixerUpper(),
server.registryAccess()
)) {
PaperWorldMetadata metadata = verificationStorage.get(PaperWorldMetadata.TYPE);
if (metadata == null || !metadataUuid.equals(metadata.uuid())) {
throw new IOException("Current Paper world metadata could not be verified.");
}
PaperLevelOverrides overrides = verificationStorage.get(PaperLevelOverrides.TYPE);
if (overrides == null || overrides.isInitialized()) {
throw new IOException("Current Paper level overrides could not be verified.");
}
}
}
private PaperLevelOverrides captureCurrentPaperLevelOverrides(
CraftServer craftServer,
MinecraftServer server
) throws IOException {
if (craftServer.isGlobalTickThread()) {
return createCurrentPaperLevelOverrides(craftServer, server);
}
if (J.isFolia() && J.isPrimaryThread()) {
throw new IOException("Current Paper world data cannot be staged from a Folia region tick thread.");
}
CompletableFuture<PaperLevelOverrides> captured = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
captured.complete(createCurrentPaperLevelOverrides(craftServer, server));
} catch (Throwable failure) {
captured.completeExceptionally(failure);
}
});
if (!scheduled) {
throw new IOException("Could not schedule the current Paper level-data snapshot on the global thread.");
}
try {
return captured.get(CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while capturing current Paper level data.", failure);
} catch (ExecutionException failure) {
throw new IOException("Could not capture current Paper level data.", failure.getCause());
} catch (TimeoutException failure) {
throw new IOException("Timed out while capturing current Paper level data.", failure);
}
}
private PaperLevelOverrides createCurrentPaperLevelOverrides(
CraftServer craftServer,
MinecraftServer server
) throws IOException {
if (!craftServer.isGlobalTickThread()) {
throw new IOException("Current Paper level data must be captured on the global tick thread.");
}
if (!(server.getWorldData().overworldData() instanceof PrimaryLevelData primaryLevelData)) {
throw new IOException("Paper primary level data is unavailable for current world data staging.");
}
return PaperLevelOverrides.createFromLiveLevelData(primaryLevelData);
CurrentPaperWorldDataWriter.write(sourceWorldDirectory, targetWorldDirectory, seed);
}
@Override
@@ -1777,13 +1704,76 @@ public class NMSBinding implements INMSBinding {
}
}
private static class LevelStorageAccessAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(1) String levelId,
@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType
) {
if (levelId == null || levelId.isBlank()) {
return;
}
ClassLoader pluginClassLoader;
Class<?> generatorType;
Class<?> stagingType;
try {
org.bukkit.plugin.Plugin irisPlugin = Bukkit.getPluginManager().getPlugin("Iris");
if (irisPlugin == null) {
return;
}
pluginClassLoader = irisPlugin.getClass().getClassLoader();
generatorType = Class.forName("art.arcane.iris.engine.platform.PlatformChunkGenerator", true, pluginClassLoader);
stagingType = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, pluginClassLoader);
} catch (Throwable ignored) {
return;
}
Object generator;
try {
generator = stagingType
.getDeclaredMethod("peekStemGenerator", String.class)
.invoke(null, levelId);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to inspect the staged world generator",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
if (generator == null || !generatorType.isInstance(generator)) {
return;
}
try {
Object target = generatorType.getMethod("getTarget").invoke(generator);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);
} catch (Throwable e) {
throw new RuntimeException("Iris failed to bind the staged world storage identity",
e instanceof InvocationTargetException ex ? ex.getCause() : e);
}
}
}
private static class ServerLevelAdvice {
@Advice.OnMethodEnter
static void enter(
@Advice.Argument(0) MinecraftServer server,
@Advice.Argument(4) ResourceKey<Level> dimensionKey,
@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey,
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
@Advice.Argument(12) ChunkGenerator constructorGenerator
@Advice.AllArguments Object[] constructorArguments
) {
if (dimensionKey == null)
return;
@@ -1818,6 +1808,13 @@ public class NMSBinding implements INMSBinding {
// the vanilla stem corrupts generation.
ChunkGenerator gen = null;
try {
ChunkGenerator constructorGenerator = null;
for (Object argument : constructorArguments) {
if (argument instanceof ChunkGenerator candidate) {
constructorGenerator = candidate;
break;
}
}
Object generator = generatorType.isInstance(constructorGenerator) ? constructorGenerator : null;
if (generator == null) {
generator = stagingType
@@ -1855,10 +1852,27 @@ public class NMSBinding implements INMSBinding {
if (stemMethod == null) {
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
}
Object target = generatorType.getMethod("getTarget").invoke(gen);
if (target == null) {
throw new IllegalStateException("Iris generator has no engine target.");
}
Object world = target.getClass().getMethod("getWorld").invoke(target);
if (world == null) {
throw new IllegalStateException("Iris generator target has no world identity.");
}
Object rawIdentity = world.getClass().getMethod("identity").invoke(world);
String worldIdentity = rawIdentity == null ? "" : rawIdentity.toString().trim();
Identifier worldIdentifier = Identifier.parse(worldIdentity);
if (!"iris".equals(worldIdentifier.getNamespace())
&& !"minecraft".equals(worldIdentifier.getNamespace())) {
throw new IllegalStateException(
"Iris generator has an unmanaged world identity: " + worldIdentity);
}
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
}
dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);
levelStem = runtimeStem;
} catch (Throwable e) {
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
@@ -215,6 +215,14 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
}
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
@@ -3,29 +3,54 @@ package art.arcane.iris.core.nms.v26_2_R1;
import art.arcane.iris.core.nms.INMSBinding;
import org.junit.Test;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NMSBindingCurrentPaperWorldDataContractTest {
@Test
public void stagesAllCurrentPaperWorldDataFromLiveServerState() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
public void bindingDelegatesWithoutLinkingPaperSavedDataClasses() throws Exception {
String bindingSource = Files.readString(bindingSourcePath());
String writer = section(
source,
bindingSource,
"public void writeCurrentPaperWorldData(",
"public KMap<Material, List<BlockProperty>> getBlockProperties()"
"public boolean awaitServerShutdownBoundary("
);
assertTrue(writer.contains("CurrentPaperWorldDataWriter.write("));
assertFalse(bindingSource.contains("PaperWorldMetadata"));
assertFalse(bindingSource.contains("PaperLevelOverrides"));
assertFalse(bindingSource.contains("io.papermc.paper.world.saveddata"));
InputStream classResource = NMSBindingCurrentPaperWorldDataContractTest.class
.getResourceAsStream("NMSBinding.class");
assertNotNull(classResource);
try (InputStream input = classResource) {
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
assertFalse(classFile.contains("PaperWorldMetadata"));
assertFalse(classFile.contains("PaperLevelOverrides"));
assertFalse(classFile.contains("io/papermc/paper/world/saveddata"));
}
}
@Test
public void stagesAllCurrentPaperWorldDataFromLiveServerState() throws Exception {
String writer = Files.readString(writerSourcePath());
assertTrue(writer.contains("WorldReplacementSeed.copyWithAuthoritativeSeed("));
assertTrue(writer.contains("UUID metadataUuid = UUID.randomUUID()"));
assertTrue(writer.contains("new PaperWorldMetadata(metadataUuid)"));
assertTrue(writer.contains("captureCurrentPaperLevelOverrides(craftServer, server)"));
assertTrue(writer.contains("captureLevelOverrides(craftServer, server)"));
assertTrue(writer.indexOf("captureLevelOverrides(craftServer, server)")
< writer.indexOf("WorldReplacementSeed.copyWithAuthoritativeSeed("));
assertTrue(writer.contains("new SavedDataStorage("));
assertTrue(writer.contains("server.getFixerUpper()"));
assertTrue(writer.contains("server.registryAccess()"));
@@ -44,23 +69,23 @@ public class NMSBindingCurrentPaperWorldDataContractTest {
@Test
public void capturesOnlyLiveLevelOverridesOnTheGlobalThread() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nmsBindingSource")));
String source = Files.readString(writerSourcePath());
String capture = section(
source,
"private PaperLevelOverrides captureCurrentPaperLevelOverrides(",
"private PaperLevelOverrides createCurrentPaperLevelOverrides("
"private static PaperLevelOverrides captureLevelOverrides(",
"private static PaperLevelOverrides createLevelOverrides("
);
String create = section(
source,
"private PaperLevelOverrides createCurrentPaperLevelOverrides(",
"public KMap<Material, List<BlockProperty>> getBlockProperties()"
"private static PaperLevelOverrides createLevelOverrides(",
"\n }\n}"
);
assertTrue(capture.contains("craftServer.isGlobalTickThread()"));
assertTrue(capture.contains("J.isFolia() && J.isPrimaryThread()"));
assertTrue(capture.contains("J.runGlobal("));
assertTrue(capture.contains("createCurrentPaperLevelOverrides(craftServer, server)"));
assertTrue(capture.contains("captured.get(CURRENT_WORLD_DATA_SNAPSHOT_TIMEOUT_SECONDS"));
assertTrue(capture.contains("createLevelOverrides(craftServer, server)"));
assertTrue(capture.contains("captured.get(SNAPSHOT_TIMEOUT_SECONDS"));
assertTrue(capture.contains("Thread.currentThread().interrupt()"));
assertTrue(create.contains("if (!craftServer.isGlobalTickThread())"));
assertTrue(create.indexOf("if (!craftServer.isGlobalTickThread())")
@@ -89,6 +114,14 @@ public class NMSBindingCurrentPaperWorldDataContractTest {
assertTrue(error.getMessage().contains("does not support current Paper world data staging"));
}
private static Path bindingSourcePath() {
return Path.of(System.getProperty("iris.nmsBindingSource"));
}
private static Path writerSourcePath() {
return bindingSourcePath().resolveSibling("CurrentPaperWorldDataWriter.java");
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
@@ -27,6 +27,8 @@ import org.junit.Test;
import org.junit.BeforeClass;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@@ -38,8 +40,8 @@ import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class NMSBindingDatapackStructureScopeTest {
@@ -108,28 +110,56 @@ public class NMSBindingDatapackStructureScopeTest {
}
@Test
public void spigotDirectHolderRetainsItsStructureSetKey() {
public void spigotDirectHolderRecoversItsRegistryKeyFromSharedEntries() {
ResourceKey<StructureSet> key = ResourceKey.create(
Registries.STRUCTURE_SET,
Identifier.parse("minecraft:villages"));
ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement placement =
new ChunkGeneratorStructureState.KeyedRandomSpreadStructurePlacement(
key,
Vec3i.ZERO,
StructurePlacement.FrequencyReductionMethod.DEFAULT,
1.0F,
10387312,
Optional.empty(),
34,
8,
RandomSpreadType.LINEAR);
StructureSet structureSet = new StructureSet(
List.of(new StructureSet.StructureSelectionEntry(
structureHolder("minecraft:village_plains"), 1)),
placement);
Identifier.parse("managed:illager_barracks"));
List<StructureSet.StructureSelectionEntry> entries = List.of(
new StructureSet.StructureSelectionEntry(
structureHolder("minecraft:pillager_outpost"), 1));
Holder<StructureSet> registered = new KeyedHolder<>(key, new StructureSet(
entries,
new RandomSpreadStructurePlacement(
34, 8, RandomSpreadType.LINEAR, 10387312)));
Holder<StructureSet> spigotDirect = Holder.direct(new StructureSet(
entries,
new RandomSpreadStructurePlacement(
34, 8, RandomSpreadType.LINEAR, 14357620)));
DatapackStructureScopeIndex scopeIndex = index(
List.of(), List.of("managed:illager_barracks"));
DatapackStructureStateFilter.StructureSetKeyIndex keyIndex =
DatapackStructureStateFilter.keyIndex(List.of(registered));
assertEquals("minecraft:villages",
DatapackStructureStateFilter.structureSetKey(Holder.direct(structureSet)));
DatapackStructureStateFilter.Selection excluded =
DatapackStructureStateFilter.filter(
List.of(spigotDirect),
scopeIndex,
Set.of(),
new IrisImportedStructureControl(),
keyIndex);
DatapackStructureStateFilter.Selection retained =
DatapackStructureStateFilter.filter(
List.of(spigotDirect),
scopeIndex,
scopeIndex.declaredSources(List.of(SOURCE)),
new IrisImportedStructureControl(),
keyIndex);
assertEquals(0, excluded.structureSets().size());
assertEquals(1, excluded.excludedManagedSets());
assertSame(spigotDirect, retained.structureSets().getFirst());
assertEquals(1, retained.retainedManagedSets());
}
@Test
public void structureScopeDoesNotLinkPaperOnlyPlacementClasses() throws IOException {
InputStream classResource = NMSBindingDatapackStructureScopeTest.class
.getResourceAsStream("DatapackStructureStateFilter.class");
assertNotNull(classResource);
try (InputStream input = classResource) {
String classFile = new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
assertFalse(classFile.contains("KeyedRandomSpreadStructurePlacement"));
}
}
@Test
@@ -271,7 +301,7 @@ public class NMSBindingDatapackStructureScopeTest {
}
@Test
public void affectedRandomSpreadSubclassFailsInsteadOfLosingSubtypeBehavior() {
public void randomSpreadSubtypeUsesTheCanonicalPlacementContract() {
Holder<StructureSet> custom = structureSetHolder(
"example:custom",
new CustomRandomSpreadPlacement(),
@@ -283,8 +313,12 @@ public class NMSBindingDatapackStructureScopeTest {
IrisImportedStructureControl control = new IrisImportedStructureControl()
.setFrequencyOverrides(overrides);
assertThrows(IllegalStateException.class, () -> DatapackStructureStateFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control));
DatapackStructureStateFilter.Selection selection = DatapackStructureStateFilter.filter(
List.of(custom), index(List.of(), List.of()), Set.of(), control);
StructurePlacement placement = selection.structureSets().getFirst().value().placement();
assertEquals(RandomSpreadStructurePlacement.class, placement.getClass());
assertEquals(26, ((RandomSpreadStructurePlacement) placement).spacing());
}
@Test
@@ -395,6 +429,21 @@ public class NMSBindingDatapackStructureScopeTest {
assertTrue(structureRetarget > identityGate);
}
@Test
public void structureStateRecreationUsesTheWorldOwnedSpigotConfiguration() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java"));
int methodStart = source.indexOf("private ChunkGeneratorStructureState createStructureState(");
int methodEnd = source.indexOf(
"\n private void initializeAndPublishStructureState(", methodStart);
assertTrue(methodStart >= 0);
assertTrue(methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("level.spigotConfig"));
assertFalse(method.contains("currentState.conf"));
}
private static DatapackStructureScopeIndex index(
List<String> structureKeys,
List<String> structureSetKeys
@@ -0,0 +1,103 @@
package art.arcane.iris.core.nms.v26_2_R1;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class NMSBindingServerLevelAdviceContractTest {
@Test
public void serverLevelAdviceSupportsCurrentSpigotAndPaperConstructorLayouts() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource")));
String transformer = section(
source,
"public boolean injectBukkit()",
"public void ensureServerLevelInjection()");
String advice = section(
source,
"private static class ServerLevelAdvice",
"\n }\n}");
assertTrue(transformer.contains("takesArgument(0, MinecraftServer.class)"));
assertTrue(transformer.contains("takesArgument(5, LevelStem.class)"));
assertFalse(transformer.contains("takesArgument(12, ChunkGenerator.class)"));
assertFalse(transformer.contains("takesArgument(13, ChunkGenerator.class)"));
assertTrue(advice.contains("@Advice.AllArguments Object[] constructorArguments"));
assertTrue(advice.contains("argument instanceof ChunkGenerator candidate"));
assertFalse(advice.contains("@Advice.Argument(12)"));
assertFalse(advice.contains("@Advice.Argument(13)"));
}
@Test
public void currentSpigotStorageAccessReceivesManagedIdentityBeforeConstruction() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource")));
String transformer = section(
source,
"public boolean injectBukkit()",
"public void ensureServerLevelInjection()");
String advice = section(
source,
"private static class LevelStorageAccessAdvice",
"private static class ServerLevelAdvice");
assertTrue(transformer.contains(
".type(ElementMatchers.is(LevelStorageSource.LevelStorageAccess.class))"));
assertTrue(transformer.contains("Advice.to(LevelStorageAccessAdvice.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArguments(4)"));
assertTrue(transformer.contains(
"ElementMatchers.takesArgument(0, LevelStorageSource.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(1, String.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(2, Path.class)"));
assertTrue(transformer.contains("ElementMatchers.takesArgument(3, ResourceKey.class)"));
assertTrue(advice.contains("@Advice.Argument(1) String levelId"));
assertTrue(advice.contains(
"@Advice.Argument(value = 3, readOnly = false) ResourceKey<LevelStem> dimensionType"));
assertTrue(advice.contains("getDeclaredMethod(\"peekStemGenerator\", String.class)"));
assertTrue(advice.contains(".invoke(null, levelId)"));
assertTrue(advice.contains(
"dimensionType = ResourceKey.create(Registries.LEVEL_STEM, worldIdentifier);"));
assertFalse(advice.contains("dimensionRoot("));
assertFalse(advice.contains("Files.copy"));
}
@Test
public void ownedWorldPublishesItsManagedIdentityAndRuntimeStemTogether() throws Exception {
String source = Files.readString(
Path.of(System.getProperty("iris.nmsBindingSource")));
String advice = section(
source,
"private static class ServerLevelAdvice",
"\n }\n}");
assertTrue(advice.contains(
"@Advice.Argument(value = 4, readOnly = false) ResourceKey<Level> dimensionKey"));
assertTrue(advice.contains("getMethod(\"getTarget\")"));
assertTrue(advice.contains("getMethod(\"getWorld\")"));
assertTrue(advice.contains("getMethod(\"identity\")"));
assertTrue(advice.contains("\"iris\".equals(worldIdentifier.getNamespace())"));
assertTrue(advice.contains("\"minecraft\".equals(worldIdentifier.getNamespace())"));
int resolvedStem = advice.indexOf("Object resolvedStem = stemMethod.invoke(");
int validatedStem = advice.indexOf("resolvedStem instanceof LevelStem runtimeStem");
int identityPublication = advice.indexOf(
"dimensionKey = ResourceKey.create(Registries.DIMENSION, worldIdentifier);");
int stemPublication = advice.indexOf("levelStem = runtimeStem;");
assertTrue(resolvedStem >= 0);
assertTrue(validatedStem > resolvedStem);
assertTrue(identityPublication > validatedStem);
assertTrue(stemPublication > identityPublication);
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue("Missing source section starting with " + startMarker, start >= 0);
assertTrue("Missing source section ending with " + endMarker, end > start);
return source.substring(start, end);
}
}
@@ -135,6 +135,18 @@ public class NativeStructureWorldgenAccessTest {
assertEquals(1, recording.blockTicks.count());
}
@Test
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
RecordingDelegate recording = new RecordingDelegate();
NativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
assertNull(access.getChunk(generationCenter.getWorldPosition()));
assertEquals(1, recording.statuslessChunkReads);
assertEquals(0, recording.statusChunkReads);
}
@Test
public void entityQueriesRejectDisjointAreasAndClampOverlaps() {
RecordingDelegate recording = new RecordingDelegate();
@@ -187,6 +199,8 @@ public class NativeStructureWorldgenAccessTest {
private int terrainReads;
private int heightReads;
private int chunkReads;
private int statuslessChunkReads;
private int statusChunkReads;
private int mutations;
private int events;
@@ -240,7 +254,16 @@ public class NativeStructureWorldgenAccessTest {
terrainReads++;
return Blocks.WATER.defaultBlockState().getFluidState();
}
if (name.equals("getChunk") || name.equals("getChunkIfLoadedImmediately")) {
if (name.equals("getChunk")) {
chunkReads++;
if (arguments.length == 2) {
statuslessChunkReads++;
} else {
statusChunkReads++;
}
return null;
}
if (name.equals("getChunkIfLoadedImmediately")) {
chunkReads++;
return null;
}
+1 -1
View File
@@ -3,7 +3,7 @@ def mainClass = 'art.arcane.iris.Iris'
def bootstrapperClass = 'art.arcane.iris.IrisBootstrap'
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
.orElse('com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969')
.orElse('com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522')
.get()
dependencies {
@@ -756,6 +756,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
IrisStartupValidation.begin();
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
Bukkit.getPluginManager().registerEvents(pendingWorldReplacements, this);
pendingWorldReplacements.registerPlatformEntryListener();
boolean enabled = enable();
if (!enabled) {
return;
@@ -22,13 +22,13 @@ import art.arcane.iris.Iris;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration;
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.platform.bukkit.BukkitEnvironment;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.WorldCreator;
@@ -698,16 +698,17 @@ public final class BukkitWorldReconciler {
@Override
public CompletableFuture<World> createWorld(NamespacedKey worldKey, String dimension, Long seed) {
try {
String worldName = IrisWorldStorage.logicalName(worldKey);
Iris.info("Loading World: %s | Generator: %s", worldName, dimension);
ChunkGenerator generator = plugin.getDefaultWorldGenerator(worldName, dimension);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(worldName, dimension);
String logicalWorldName = IrisWorldStorage.logicalName(worldKey);
String configuredWorldName = configuredWorldName(worldKey);
Iris.info("Loading World: %s | Generator: %s", logicalWorldName, dimension);
ChunkGenerator generator = plugin.getDefaultWorldGenerator(configuredWorldName, dimension);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
if (generator == null || irisDimension == null) {
throw new IllegalStateException("Could not resolve the Iris generator or dimension \"" + dimension + "\".");
}
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + worldName + " using Iris:" + dimension + "...");
WorldCreator creator = WorldCreatorCompat.ofKey(worldKey)
Iris.info(C.LIGHT_PURPLE + "Preparing Spawn for " + logicalWorldName + " using Iris:" + dimension + "...");
WorldCreator creator = WorldCreatorCompat.ofPersistentKey(worldKey)
.generator(generator)
.environment(BukkitEnvironment.from(irisDimension.getEnvironment()));
if (seed != null) {
@@ -726,7 +727,7 @@ public final class BukkitWorldReconciler {
@Override
public DimensionResolution resolveDimension(NamespacedKey worldKey) {
File dimensionsDirectory = new File(IrisWorldStorage.packRoot(worldKey), "dimensions");
File dimensionsDirectory = new File(snapshotRoot(worldKey), "dimensions");
if (!dimensionsDirectory.isDirectory()) {
return DimensionResolution.failed(new IllegalStateException("The world has no Iris dimensions directory."));
}
@@ -764,19 +765,30 @@ public final class BukkitWorldReconciler {
@Override
public void requireDimensionLoadable(NamespacedKey worldKey, String dimension) {
File snapshotRoot = IrisWorldStorage.packRoot(worldKey);
boolean snapshotPresent = snapshotRoot.isDirectory();
if (snapshotPresent) {
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
}
String worldName = IrisWorldStorage.logicalName(worldKey);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(worldName, dimension);
File snapshotRoot = snapshotRoot(worldKey);
IrisWorldGeneratorResolver.requireSnapshotLoadable(snapshotRoot);
String configuredWorldName = configuredWorldName(worldKey);
IrisDimension irisDimension = IrisWorldGeneratorResolver.loadDimension(configuredWorldName, dimension);
if (irisDimension == null) {
throw new IllegalStateException("Could not resolve the Iris dimension \"" + dimension + "\".");
}
if (!snapshotPresent) {
PackValidationRegistry.requireLoadable(irisDimension.getLoader().getDataFolder().getName());
}
private File snapshotRoot(NamespacedKey worldKey) {
File levelRoot = IrisWorldStorage.levelRoot();
File dimensionRoot = IrisWorldStorage.requireFrozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
configuredWorldName(worldKey),
worldKey
);
File expectedRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey);
if (!dimensionRoot.toPath().toAbsolutePath().normalize()
.equals(expectedRoot.toPath().toAbsolutePath().normalize())) {
throw new IllegalStateException("Iris world storage does not match the current platform layout for "
+ worldKey + ".");
}
return IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
}
}
}
@@ -1,6 +1,5 @@
package art.arcane.iris.core;
import net.kyori.adventure.text.Component;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -11,6 +10,6 @@ public final class IrisStartupAdmissionListener implements Listener {
public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
IrisStartupValidation.denialReason().ifPresent(reason -> event.disallow(
AsyncPlayerPreLoginEvent.Result.KICK_OTHER,
Component.text(reason + " Check the server console, correct the reported Iris state, and restart.")));
reason + " Check the server console, correct the reported Iris state, and restart."));
}
}
@@ -29,12 +29,12 @@ 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.spi.IrisPlatforms;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import lombok.NonNull;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.ChunkGenerator;
@@ -231,9 +231,18 @@ public final class IrisWorldGeneratorResolver {
@Nullable
public static IrisDimension loadDimension(@NonNull String worldName, @NonNull String id) {
NamespacedKey worldKey = configuredWorldKey(worldName, IrisWorldStorage.levelRoot().getName());
File pack = IrisWorldStorage.packRoot(worldKey);
IrisDimension dimension = pack.isDirectory() ? IrisData.get(pack).getDimensionLoader().load(id) : null;
File levelRoot = IrisWorldStorage.levelRoot();
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName());
String configuredWorldName = IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName());
File pack = IrisWorldStorage.frozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
configuredWorldName,
worldKey
)
.map(IrisWorldStorage::requireFrozenPackRoot)
.orElse(null);
IrisDimension dimension = pack == null ? null : IrisData.get(pack).getDimensionLoader().load(id);
if (dimension == null) dimension = IrisData.loadAnyDimension(id, null);
if (dimension == null) {
File packsRoot = IrisPlatforms.get().packsFolderNoCreate();
@@ -279,57 +288,63 @@ public final class IrisWorldGeneratorResolver {
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
IrisDimension dim = loadDimension(worldName, id);
if (dim == null) {
throw new RuntimeException("Can't find dimension " + id + "!");
}
NamespacedKey worldKey = configuredWorldKey(worldName, IrisWorldStorage.levelRoot().getName());
File snapshotRoot = IrisWorldStorage.packRoot(worldKey);
File dimensionPackRoot = dim.getLoader().getDataFolder();
String packName = dimensionPackRoot.getName();
try {
if (snapshotRoot.toPath().toAbsolutePath().normalize()
.equals(dimensionPackRoot.toPath().toAbsolutePath().normalize())) {
requireSnapshotLoadable(snapshotRoot);
} else {
PackValidationRegistry.requireLoadable(packName);
}
return resolveFrozenWorldGenerator(worldName, id);
} catch (RuntimeException failure) {
Iris.reportError("Refusing to load configured Iris world '" + worldName
+ "' because its frozen world-local pack snapshot could not be used.", failure);
Bukkit.shutdown();
throw failure;
}
}
private ChunkGenerator resolveFrozenWorldGenerator(String worldName, String id) {
File levelRoot = IrisWorldStorage.levelRoot();
NamespacedKey worldKey = configuredWorldKey(worldName, levelRoot.getName());
File dimensionRoot = IrisWorldStorage.requireFrozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
worldName,
worldKey
);
File expectedDimensionRoot = WorldCreatorCompat.persistentDimensionRoot(worldKey);
if (!dimensionRoot.toPath().toAbsolutePath().normalize()
.equals(expectedDimensionRoot.toPath().toAbsolutePath().normalize())) {
throw new IllegalStateException("Frozen Iris world storage does not match the current platform layout for "
+ worldKey + ".");
}
File snapshotRoot = IrisWorldStorage.requireFrozenPackRoot(dimensionRoot);
try {
requireSnapshotLoadable(snapshotRoot);
} catch (BrokenPackException exception) {
Iris.error("Refusing to create world '" + worldName + "' using broken pack '" + packName + "':");
Iris.error("Refusing to create world '" + worldName + "' using broken snapshot at '"
+ snapshotRoot + "':");
for (String reason : exception.getReasons()) {
Iris.error(" - " + reason);
}
throw exception;
}
Iris.debug("Assuming IrisDimension: " + dim.getName());
IrisDimension dimension = IrisData.get(snapshotRoot).getDimensionLoader().load(id, false);
if (dimension == null) {
throw new IllegalStateException("Frozen Iris pack snapshot at " + snapshotRoot
+ " does not contain dimension " + id + ".");
}
IrisWorld w = IrisWorld.builder()
Iris.debug("Assuming IrisDimension: " + dimension.getName());
IrisWorld world = IrisWorld.builder()
.platformIdentity(worldKey.toString())
.name(worldName)
.seed(1337)
.worldFolder(IrisWorldStorage.dimensionRoot(worldKey))
.minHeight(dim.getMinHeight())
.maxHeight(dim.getMaxHeight())
.worldFolder(dimensionRoot)
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
.build();
Iris.debug("Generator Config: " + w.toString());
Iris.debug("Generator Config: " + world);
File ff = new File(w.worldFolder(), "iris/pack");
IrisDimension installedDimension = ff.isDirectory()
? IrisData.get(ff).getDimensionLoader().load(dim.getLoadKey(), false)
: null;
if (installedDimension == null) {
dim = Iris.service(StudioSVC.class).replaceIntoWorld(Iris.getSender(), dim, w.worldFolder());
if (dim == null) {
throw new IllegalStateException("Failed to install dimension pack for " + id);
}
} else {
dim = installedDimension;
}
requireSnapshotLoadable(ff);
return new BukkitChunkGenerator(w, false, ff, dim.getLoadKey());
return new BukkitChunkGenerator(world, false, snapshotRoot, dimension.getLoadKey());
}
private record FreshValidation(
@@ -0,0 +1,54 @@
package art.arcane.iris.core;
import io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent;
import net.kyori.adventure.text.Component;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import java.util.Objects;
import java.util.UUID;
public final class PaperWorldReplacementEntryListener implements Listener {
private final PendingWorldReplacementManager manager;
public PaperWorldReplacementEntryListener(PendingWorldReplacementManager manager) {
this.manager = Objects.requireNonNull(manager, "manager");
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event) {
UUID playerId = event.getConnection().getProfile().getId();
if (playerId == null) {
return;
}
try {
PendingWorldReplacementManager.ReplacementEntryRedirect redirect = manager.prepareReplacementEntry(
playerId,
event.getSpawnLocation(),
event.isNewPlayer()
);
if (redirect == null) {
return;
}
Location location = redirect.location();
event.setSpawnLocation(location);
if (redirect.acknowledgementRequired()) {
manager.expectReplacementEntryAcknowledgement(playerId, redirect.transactionId());
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
refuseUnsafeEntry(event, playerId, failure);
} catch (Throwable failure) {
refuseUnsafeEntry(event, playerId, failure);
}
}
private void refuseUnsafeEntry(AsyncPlayerSpawnLocationEvent event, UUID playerId, Throwable failure) {
manager.reportUnsafeEntry(playerId, failure);
event.getConnection().disconnect(Component.text(
"Iris could not verify a safe login location after the Overworld replacement. Retry after startup completes."
));
}
}
@@ -451,18 +451,51 @@ public final class PendingWorldDeleteQueue implements WorldDeletionQueue {
if (type == QueueEntryType.EXACT) {
NamespacedKey key = IrisWorldStorage.managedKeyFromName(storedName, levelRoot.getName());
Path path = IrisWorldStorage.requireSafeManagedDimensionRoot(levelRoot, key).toPath();
Path path = currentStorageRoot(levelRoot, key);
return List.of(new DeleteTarget(key, path));
}
ArrayList<DeleteTarget> targets = new ArrayList<>(3);
for (String familyWorldName : TransientWorldCleanupSupport.worldFamilyNames(storedName)) {
NamespacedKey key = IrisWorldStorage.managedKeyFromName(familyWorldName, levelRoot.getName());
Path path = IrisWorldStorage.requireSafeManagedDimensionRoot(levelRoot, key).toPath();
Path path = currentStorageRoot(levelRoot, key);
targets.add(new DeleteTarget(key, path));
}
return targets;
}
private static Path currentStorageRoot(File levelRoot, NamespacedKey key) {
File worldContainer = levelRoot.getAbsoluteFile().getParentFile();
if (worldContainer == null) {
throw new IllegalArgumentException("Selected level root has no world container: " + levelRoot);
}
String configuredWorldName = IrisWorldStorage.configuredWorldName(key, levelRoot.getName());
Path directRoot = IrisWorldStorage.requireSafeManagedDimensionRoot(levelRoot, key)
.toPath()
.toAbsolutePath()
.normalize();
Path dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
worldContainer,
levelRoot,
configuredWorldName,
key
).map(file -> file.toPath().toAbsolutePath().normalize()).orElse(directRoot);
if (dimensionRoot.equals(directRoot)) {
return directRoot;
}
Path configuredDimensionRoot = IrisWorldStorage.configuredDimensionRoot(
worldContainer,
levelRoot,
key
).toPath().toAbsolutePath().normalize();
if (!dimensionRoot.equals(configuredDimensionRoot)) {
throw new IllegalStateException("Iris world storage does not match the current platform layout.");
}
return IrisWorldStorage.configuredLevelRoot(worldContainer, levelRoot, key)
.toPath()
.toAbsolutePath()
.normalize();
}
}
private static Path requireSafeQuarantinePath(File levelRoot, String quarantineName) throws IOException {
@@ -8,6 +8,7 @@ import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSna
import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator;
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrap;
import art.arcane.iris.core.lifecycle.WorldReplacementBootstrapMarker;
import art.arcane.iris.core.lifecycle.WorldReplacementEntryGuard;
import art.arcane.iris.core.lifecycle.WorldReplacementFilesystem;
import art.arcane.iris.core.lifecycle.WorldReplacementFilesystem.ReplacementPaths;
import art.arcane.iris.core.lifecycle.WorldReplacementJournal;
@@ -15,24 +16,32 @@ import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Phase;
import art.arcane.iris.core.lifecycle.WorldReplacementJournal.Transaction;
import art.arcane.iris.core.lifecycle.WorldReplacementSeed;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.core.service.StudioSVC;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEnvironment;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.misc.ServerProperties;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.bukkit.WorldIdentity;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.world.WorldLoadEvent;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -40,23 +49,60 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
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;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public final class PendingWorldReplacementManager implements Listener {
private static final WorldSlotKey OVERWORLD_KEY = WorldSlotKey.minecraft("overworld");
private static final long SAFE_ENTRY_TIMEOUT_SECONDS = 30L;
private final Iris plugin;
private final Set<UUID> cleanupInFlight = new HashSet<>();
private final Set<UUID> verificationInFlight = new HashSet<>();
private final ConcurrentHashMap<UUID, UUID> pendingEntryAcknowledgements = new ConcurrentHashMap<>();
private volatile WorldReplacementEntryGuard.Entry overworldEntryGuard;
private volatile Path overworldEntryWorldDirectory;
private volatile CompletableFuture<Location> overworldSafeEntry;
private volatile UUID overworldEntryAwaitingVerification;
private volatile World overworldEntryWorld;
private volatile boolean paperEntryListenerRegistered;
public PendingWorldReplacementManager(Iris plugin) {
this.plugin = Objects.requireNonNull(plugin, "plugin");
}
public void registerPlatformEntryListener() {
ClassLoader loader = getClass().getClassLoader();
try {
Class.forName("io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent", false, loader);
} catch (ClassNotFoundException | LinkageError unavailable) {
return;
}
try {
Class<?> listenerType = Class.forName(
"art.arcane.iris.core.PaperWorldReplacementEntryListener",
true,
loader
);
Constructor<?> constructor = listenerType.getConstructor(PendingWorldReplacementManager.class);
Listener listener = (Listener) constructor.newInstance(this);
Bukkit.getPluginManager().registerEvents(listener, plugin);
paperEntryListenerRegistered = true;
} catch (InvocationTargetException failure) {
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
throw new IllegalStateException("Paper replacement entry listener registration failed.", cause);
} catch (ReflectiveOperationException | LinkageError failure) {
throw new IllegalStateException("Paper replacement entry listener is unavailable.", failure);
}
}
public NamespacedKey resolveRequestedWorldKey(String requestedName) {
NamespacedKey worldKey = IrisWorldStorage.replacementKeyFromName(
requestedName,
@@ -129,6 +175,9 @@ public final class PendingWorldReplacementManager implements Listener {
paths.stage(),
seedSelection
);
if (target.slotKind() == SlotKind.VANILLA_OVERWORLD) {
WorldReplacementEntryGuard.stage(target.levelRoot(), paths.stage(), transactionId);
}
File stagedPack = paths.stage().resolve("iris/pack").toFile();
IrisWorldGeneratorResolver.requireSnapshotLoadable(stagedPack);
String packFingerprint = WorldReplacementFilesystem.fingerprintPack(stagedPack.toPath());
@@ -205,6 +254,20 @@ public final class PendingWorldReplacementManager implements Listener {
public synchronized void processPendingStartupReplacements() {
ArrayList<String> failures = new ArrayList<>();
ArrayList<String> restartBoundaries = new ArrayList<>();
try {
loadOverworldEntryGuard();
} catch (Throwable failure) {
String message = "Iris could not load the Overworld replacement entry guard: " + detail(failure);
Iris.reportError(message, failure);
IrisStartupValidation.markPacksInvalid(List.of(message));
return;
}
if (overworldEntryGuard != null && !paperEntryListenerRegistered) {
String message = "A pending Overworld replacement requires the Paper safe-entry capability.";
Iris.error(message);
IrisStartupValidation.markPacksInvalid(List.of(message));
return;
}
List<Transaction> transactions;
try {
transactions = loadTransactions();
@@ -215,6 +278,9 @@ public final class PendingWorldReplacementManager implements Listener {
return;
}
for (Transaction transaction : transactions) {
if (OVERWORLD_KEY.equals(transaction.worldKey()) && transaction.phase() == Phase.PUBLISHED) {
overworldEntryAwaitingVerification = transaction.id();
}
try {
inspectStartupTransaction(transaction);
} catch (RestartBoundaryRequired boundary) {
@@ -229,6 +295,7 @@ public final class PendingWorldReplacementManager implements Listener {
Iris.reportError(message, failure);
}
}
scheduleLoadedOverworldEntryPreparation();
if (!failures.isEmpty()) {
IrisStartupValidation.markPacksInvalid(failures);
}
@@ -258,6 +325,245 @@ public final class PendingWorldReplacementManager implements Listener {
scheduleRuntimeCapture(transaction, 0);
}
}
scheduleLoadedOverworldEntryPreparation();
}
private void scheduleLoadedOverworldEntryPreparation() {
if (overworldEntryGuard == null) {
return;
}
try {
J.s(this::prepareLoadedOverworldEntry);
} catch (Throwable failure) {
failOverworldEntryPreparation(failure);
}
}
private void prepareLoadedOverworldEntry() {
World world;
try {
world = WorldIdentity.resolve(toNamespacedKey(OVERWORLD_KEY)).orElse(null);
} catch (Throwable failure) {
failOverworldEntryPreparation(failure);
return;
}
if (world != null) {
prepareOverworldEntry(world);
}
}
private void prepareOverworldEntry(World world) {
WorldReplacementEntryGuard.Entry guard = overworldEntryGuard;
if (guard == null
|| guard.transactionId().equals(overworldEntryAwaitingVerification)
|| !OVERWORLD_KEY.equals(toWorldSlotKey(WorldIdentity.key(world)))) {
return;
}
CompletableFuture<Location> targetFuture;
synchronized (this) {
if (overworldSafeEntry != null) {
return;
}
targetFuture = new CompletableFuture<>();
overworldSafeEntry = targetFuture;
overworldEntryWorld = world;
}
try {
PlatformChunkGenerator generator = IrisToolbelt.access(world);
if (!(generator instanceof BukkitChunkGenerator bukkitGenerator)) {
throw new IOException("The replaced Overworld does not have a Bukkit Iris generator.");
}
Location anchor = bukkitGenerator.getInitialSpawnLocation(world);
int chunkX = anchor.getBlockX() >> 4;
int chunkZ = anchor.getBlockZ() >> 4;
WorldRuntimeControlService runtime = WorldRuntimeControlService.get();
CompletableFuture<Chunk> chunkFuture = runtime.requestChunkAsync(world, chunkX, chunkZ, true);
if (chunkFuture == null) {
throw new IOException("The replacement spawn chunk request was not accepted.");
}
chunkFuture
.thenCompose(chunk -> runtime.resolveSafeEntry(world, anchor))
.thenCompose(this::applyOverworldSpawn)
.thenCompose(this::persistOverworldSpawn)
.whenComplete((safeEntry, failure) -> {
if (failure != null) {
targetFuture.completeExceptionally(failure);
Iris.reportError("Could not prepare a safe spawn for the replaced Overworld.", failure);
return;
}
targetFuture.complete(safeEntry.clone());
retireOverworldEntryIfComplete(guard.transactionId());
});
} catch (Throwable failure) {
targetFuture.completeExceptionally(failure);
Iris.reportError("Could not prepare a safe spawn for the replaced Overworld.", failure);
}
}
private CompletableFuture<Location> applyOverworldSpawn(Location safeEntry) {
Location requiredSafeEntry = Objects.requireNonNull(safeEntry, "safeEntry").clone();
World world = Objects.requireNonNull(requiredSafeEntry.getWorld(), "safeEntry.world");
CompletableFuture<Location> applied = new CompletableFuture<>();
int chunkX = requiredSafeEntry.getBlockX() >> 4;
int chunkZ = requiredSafeEntry.getBlockZ() >> 4;
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
if (!world.setSpawnLocation(requiredSafeEntry)) {
throw new IOException("The server rejected the replacement spawn location.");
}
applied.complete(requiredSafeEntry.clone());
} catch (Throwable failure) {
applied.completeExceptionally(failure);
}
});
if (!scheduled) {
applied.completeExceptionally(new IOException("Could not schedule the replacement spawn update."));
}
return applied;
}
private CompletableFuture<Location> persistOverworldSpawn(Location safeEntry) {
Location requiredSafeEntry = Objects.requireNonNull(safeEntry, "safeEntry").clone();
World world = Objects.requireNonNull(requiredSafeEntry.getWorld(), "safeEntry.world");
CompletableFuture<Location> persisted = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
world.save();
persisted.complete(requiredSafeEntry.clone());
} catch (Throwable failure) {
persisted.completeExceptionally(failure);
}
});
if (!scheduled) {
persisted.completeExceptionally(new IOException("Could not schedule replacement spawn persistence."));
}
return persisted;
}
private CompletableFuture<Boolean> inspectLoginCollision(Location location) {
Location requiredLocation = Objects.requireNonNull(location, "location").clone();
World world = Objects.requireNonNull(requiredLocation.getWorld(), "location.world");
int chunkX = requiredLocation.getBlockX() >> 4;
int chunkZ = requiredLocation.getBlockZ() >> 4;
WorldRuntimeControlService runtime = WorldRuntimeControlService.get();
CompletableFuture<Chunk> chunkFuture = runtime.requestChunkAsync(world, chunkX, chunkZ, true);
if (chunkFuture == null) {
return CompletableFuture.failedFuture(new IOException("The saved login chunk request was not accepted."));
}
return chunkFuture.thenCompose(chunk -> inspectLoadedLoginCollision(requiredLocation));
}
private CompletableFuture<Boolean> inspectLoadedLoginCollision(Location location) {
World world = Objects.requireNonNull(location.getWorld(), "location.world");
CompletableFuture<Boolean> inspected = new CompletableFuture<>();
int chunkX = location.getBlockX() >> 4;
int chunkZ = location.getBlockZ() >> 4;
boolean scheduled = J.runRegion(world, chunkX, chunkZ, () -> {
try {
int blockY = location.getBlockY();
if (blockY < world.getMinHeight() || blockY + 1 >= world.getMaxHeight()) {
inspected.complete(false);
return;
}
boolean feetPassable = world.getBlockAt(location.getBlockX(), blockY, location.getBlockZ())
.isPassable();
boolean headPassable = world.getBlockAt(location.getBlockX(), blockY + 1, location.getBlockZ())
.isPassable();
inspected.complete(feetPassable && headPassable);
} catch (Throwable failure) {
inspected.completeExceptionally(failure);
}
});
if (!scheduled) {
inspected.completeExceptionally(new IOException("Could not schedule the saved login collision check."));
}
return inspected;
}
private void completeOverworldEntry(UUID playerId, UUID transactionId) {
try {
J.a(() -> completeOverworldEntryAsync(playerId, transactionId));
} catch (Throwable failure) {
Iris.reportError("Could not record a completed Overworld replacement entry for " + playerId + ".", failure);
}
}
private void completeOverworldEntryAsync(UUID playerId, UUID transactionId) {
boolean retire = false;
try {
synchronized (this) {
WorldReplacementEntryGuard.Entry current = overworldEntryGuard;
Path worldDirectory = overworldEntryWorldDirectory;
if (current == null
|| worldDirectory == null
|| !current.transactionId().equals(transactionId)
|| !current.pendingPlayers().contains(playerId)) {
return;
}
Optional<WorldReplacementEntryGuard.Entry> updated = WorldReplacementEntryGuard.completePlayer(
worldDirectory,
transactionId,
playerId
);
overworldEntryGuard = updated.orElse(null);
retire = overworldEntryGuard != null && overworldEntryGuard.pendingPlayers().isEmpty();
}
} catch (Throwable failure) {
Iris.reportError("Could not record a completed Overworld replacement entry for " + playerId + ".", failure);
return;
}
if (retire) {
retireOverworldEntryIfCompleteAsync(transactionId);
}
}
private void retireOverworldEntryIfComplete(UUID transactionId) {
try {
J.a(() -> retireOverworldEntryIfCompleteAsync(transactionId));
} catch (Throwable failure) {
Iris.reportError("Could not schedule Overworld replacement entry marker retirement.", failure);
}
}
private synchronized void retireOverworldEntryIfCompleteAsync(UUID transactionId) {
WorldReplacementEntryGuard.Entry current = overworldEntryGuard;
Path worldDirectory = overworldEntryWorldDirectory;
CompletableFuture<Location> safeEntry = overworldSafeEntry;
if (current == null
|| worldDirectory == null
|| !current.transactionId().equals(transactionId)
|| !current.pendingPlayers().isEmpty()
|| safeEntry == null
|| !safeEntry.isDone()
|| safeEntry.isCompletedExceptionally()) {
return;
}
try {
if (!WorldReplacementEntryGuard.retireIfEmpty(worldDirectory, transactionId)) {
return;
}
overworldEntryGuard = null;
overworldEntryWorldDirectory = null;
overworldSafeEntry = null;
overworldEntryWorld = null;
pendingEntryAcknowledgements.entrySet().removeIf(entry -> entry.getValue().equals(transactionId));
} catch (Throwable failure) {
Iris.reportError("Could not retire the completed Overworld replacement entry marker.", failure);
}
}
void reportUnsafeEntry(UUID playerId, Throwable failure) {
Iris.reportError("Refused unsafe Overworld replacement entry for " + playerId + ".", failure);
}
private synchronized void failOverworldEntryPreparation(Throwable failure) {
CompletableFuture<Location> future = overworldSafeEntry;
if (future == null) {
future = new CompletableFuture<>();
overworldSafeEntry = future;
}
future.completeExceptionally(failure);
Iris.reportError("Could not prepare a safe spawn for the replaced Overworld.", failure);
}
@EventHandler(priority = EventPriority.MONITOR)
@@ -269,9 +575,81 @@ public final class PendingWorldReplacementManager implements Listener {
Iris.reportError("Failed to capture a loaded world identity for replacement verification.", failure);
return;
}
if (OVERWORLD_KEY.equals(worldKey)) {
prepareOverworldEntry(event.getWorld());
}
J.a(() -> discoverLoadedWorldTransaction(worldKey));
}
ReplacementEntryRedirect prepareReplacementEntry(UUID playerId, Location savedLocation, boolean newPlayer)
throws IOException, InterruptedException, ExecutionException, TimeoutException {
WorldReplacementEntryGuard.Entry guard = overworldEntryGuard;
if (guard == null || playerId == null) {
return null;
}
boolean pendingPlayer = guard.pendingPlayers().contains(playerId);
if (!pendingPlayer && !newPlayer) {
return null;
}
Location requiredSavedLocation = Objects.requireNonNull(savedLocation, "savedLocation").clone();
World replacementWorld = overworldEntryWorld;
if (replacementWorld == null) {
throw new IOException("The replacement Overworld is not ready.");
}
if (requiredSavedLocation.getWorld() != replacementWorld) {
if (pendingPlayer) {
completeOverworldEntry(playerId, guard.transactionId());
}
return null;
}
if (!newPlayer) {
boolean collisionSafe = inspectLoginCollision(requiredSavedLocation)
.get(SAFE_ENTRY_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (collisionSafe) {
completeOverworldEntry(playerId, guard.transactionId());
return null;
}
}
CompletableFuture<Location> safeEntry = overworldSafeEntry;
if (safeEntry == null) {
throw new IOException("The replacement safe spawn is not ready.");
}
Location prepared = safeEntry.get(SAFE_ENTRY_TIMEOUT_SECONDS, TimeUnit.SECONDS).clone();
prepared.setYaw(requiredSavedLocation.getYaw());
prepared.setPitch(requiredSavedLocation.getPitch());
return new ReplacementEntryRedirect(guard.transactionId(), prepared, pendingPlayer);
}
void expectReplacementEntryAcknowledgement(UUID playerId, UUID transactionId) throws IOException {
WorldReplacementEntryGuard.Entry guard = overworldEntryGuard;
if (guard == null
|| !guard.transactionId().equals(transactionId)
|| !guard.pendingPlayers().contains(playerId)) {
throw new IOException("The Overworld replacement entry receipt is no longer active.");
}
pendingEntryAcknowledgements.put(playerId, transactionId);
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
UUID playerId = event.getPlayer().getUniqueId();
UUID transactionId = pendingEntryAcknowledgements.remove(playerId);
if (transactionId == null) {
return;
}
boolean scheduled = J.runEntity(event.getPlayer(), () -> {
try {
event.getPlayer().saveData();
completeOverworldEntry(playerId, transactionId);
} catch (Throwable failure) {
Iris.reportError("Could not persist safe Overworld replacement entry for " + playerId + ".", failure);
}
});
if (!scheduled) {
Iris.error("Could not schedule safe Overworld replacement entry persistence for " + playerId + ".");
}
}
private void discoverLoadedWorldTransaction(WorldSlotKey worldKey) {
Transaction transaction;
try {
@@ -412,6 +790,10 @@ public final class PendingWorldReplacementManager implements Listener {
try {
writeTransaction(committed);
Iris.success("Committed Iris world replacement for " + transaction.worldKey() + ".");
if (OVERWORLD_KEY.equals(transaction.worldKey())) {
overworldEntryAwaitingVerification = null;
scheduleLoadedOverworldEntryPreparation();
}
scheduleCommittedCleanup(committed);
} catch (Throwable failure) {
Iris.reportError("The replacement for " + transaction.worldKey()
@@ -456,6 +838,9 @@ public final class PendingWorldReplacementManager implements Listener {
private void initiateRollback(Transaction transaction, Throwable failure) {
Iris.reportError("Iris world replacement verification failed for " + transaction.worldKey()
+ "; the retained world will be restored on restart.", failure);
if (OVERWORLD_KEY.equals(transaction.worldKey())) {
clearOverworldEntryGuard();
}
try {
Transaction rollback = transaction.withPhase(Phase.ROLLBACK_PENDING);
writeTransaction(rollback);
@@ -608,6 +993,24 @@ public final class PendingWorldReplacementManager implements Listener {
return plugin.getDataFolder().toPath().toAbsolutePath().normalize();
}
private synchronized void loadOverworldEntryGuard() throws IOException {
ExactWorldSlotPathPolicy.Target target = resolveTarget(OVERWORLD_KEY);
Optional<WorldReplacementEntryGuard.Entry> loaded = WorldReplacementEntryGuard.load(target.worldDirectory());
overworldEntryGuard = loaded.orElse(null);
overworldEntryWorldDirectory = loaded.isPresent() ? target.worldDirectory() : null;
overworldSafeEntry = null;
overworldEntryAwaitingVerification = null;
}
private synchronized void clearOverworldEntryGuard() {
overworldEntryGuard = null;
overworldEntryWorldDirectory = null;
overworldSafeEntry = null;
overworldEntryAwaitingVerification = null;
overworldEntryWorld = null;
pendingEntryAcknowledgements.clear();
}
private ExactWorldSlotPathPolicy.Target resolveTransactionTarget(Transaction transaction) throws IOException {
return WorldReplacementJournal.resolveTarget(transaction, IrisWorldStorage.levelRoot().toPath());
}
@@ -738,6 +1141,22 @@ public final class PendingWorldReplacementManager implements Listener {
private record VanillaLevelContext(boolean allowNether, boolean allowEnd) {
}
record ReplacementEntryRedirect(
UUID transactionId,
Location location,
boolean acknowledgementRequired
) {
ReplacementEntryRedirect {
Objects.requireNonNull(transactionId, "transactionId");
location = Objects.requireNonNull(location, "location").clone();
}
@Override
public Location location() {
return location.clone();
}
}
private static final class RestartBoundaryRequired extends IOException {
private RestartBoundaryRequired(String message) {
super(message);
@@ -801,8 +801,14 @@ public class CommandIris implements DirectorExecutor {
}
boolean doesWorldExist(String worldName) {
File worldDirectory = IrisWorldStorage.dimensionRoot(worldName);
return worldDirectory.exists() && worldDirectory.isDirectory();
NamespacedKey worldKey = IrisWorldStorage.managedKeyFromName(worldName);
File levelRoot = IrisWorldStorage.levelRoot();
return IrisWorldStorage.frozenDimensionRoot(
Bukkit.getWorldContainer(),
levelRoot,
IrisWorldStorage.configuredWorldName(worldKey, levelRoot.getName()),
worldKey
).isPresent();
}
public static class ManagedWorldNameHandler implements DirectorParameterHandler<String> {
@@ -93,6 +93,61 @@ public class IrisWorldGeneratorResolverTest {
);
}
@Test
public void configuredWorldResolutionUsesOnlyFrozenWorldLocalSnapshot() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java"));
int resolverStart = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator(");
int resolverEnd = source.indexOf("private record FreshValidation", resolverStart);
String resolver = source.substring(resolverStart, resolverEnd);
int dimensionRoot = resolver.indexOf("IrisWorldStorage.requireFrozenDimensionRoot(");
int currentPlatformRoot = resolver.indexOf("WorldCreatorCompat.persistentDimensionRoot(worldKey)");
int layoutRefusal = resolver.indexOf(
"Frozen Iris world storage does not match the current platform layout",
currentPlatformRoot
);
int snapshotRoot = resolver.indexOf("IrisWorldStorage.requireFrozenPackRoot(dimensionRoot)");
int validation = resolver.indexOf("requireSnapshotLoadable(snapshotRoot)");
int exactLoad = resolver.indexOf(
"IrisData.get(snapshotRoot).getDimensionLoader().load(id, false)"
);
int canonicalIdentity = resolver.indexOf(".platformIdentity(worldKey.toString())");
int resolvedStorage = resolver.indexOf(".worldFolder(dimensionRoot)");
assertTrue(dimensionRoot >= 0);
assertTrue(currentPlatformRoot > dimensionRoot);
assertTrue(layoutRefusal > currentPlatformRoot);
assertTrue(snapshotRoot > layoutRefusal);
assertTrue(validation > snapshotRoot);
assertTrue(exactLoad > validation);
assertTrue(canonicalIdentity > exactLoad);
assertTrue(resolvedStorage > canonicalIdentity);
assertFalse(resolver.contains("loadDimension("));
assertFalse(resolver.contains("loadAnyDimension("));
assertFalse(resolver.contains("replaceIntoWorld("));
assertFalse(resolver.contains("installIntoWorld("));
}
@Test
public void configuredWorldSnapshotFailureStopsStartupAndRethrows() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/IrisWorldGeneratorResolver.java"));
int resolverStart = source.indexOf("public ChunkGenerator resolveDefaultWorldGenerator(");
int resolverEnd = source.indexOf("private ChunkGenerator resolveFrozenWorldGenerator(", resolverStart);
String resolver = source.substring(resolverStart, resolverEnd);
int failureCapture = resolver.indexOf("catch (RuntimeException failure)");
int report = resolver.indexOf("Iris.reportError(", failureCapture);
int shutdown = resolver.indexOf("Bukkit.shutdown()", report);
int rethrow = resolver.indexOf("throw failure", shutdown);
assertTrue(failureCapture >= 0);
assertTrue(report > failureCapture);
assertTrue(shutdown > report);
assertTrue(rethrow > shutdown);
}
private static void writeValidPack(Path packRoot) throws Exception {
Files.createDirectories(packRoot.resolve("dimensions"));
Files.createDirectories(packRoot.resolve("regions"));
@@ -174,6 +174,19 @@ public class PendingWorldDeleteQueueTest {
), family);
}
@Test
public void exactLogicalEntryResolvesCurrentCraftBukkitLevelStorage() throws IOException {
Path worldContainer = temporaryFolder.newFolder("configured-delete-server").toPath();
File levelRoot = Files.createDirectory(worldContainer.resolve("world")).toFile();
Path configuredLevelRoot = worldContainer.resolve("world_iris_alpha");
Files.createDirectories(configuredLevelRoot.resolve("dimensions/iris/alpha"));
assertEquals(
List.of(configuredLevelRoot.toAbsolutePath().normalize()),
PendingWorldDeleteQueue.resolveQueueEntryPaths(levelRoot, "exact:alpha")
);
}
@Test
public void failedSafeDeletionSignalsQueueRetentionAndSucceedsOnRetry() throws IOException {
File levelRoot = temporaryFolder.newFolder("retry-world");
@@ -164,6 +164,84 @@ public class PendingWorldReplacementThreadAffinityTest {
assertFalse(irisSource.contains("J.a(pendingWorldReplacements::verifyLoadedPublishedWorlds)"));
}
@Test
public void paperLoginHookIsIsolatedFromAlwaysLoadedSpigotClasses() throws Exception {
String managerSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java"));
String irisSource = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java"));
String listenerSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java"));
String registration = method(managerSource, "public void registerPlatformEntryListener()");
assertFalse(managerSource.contains("import io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent"));
assertFalse(managerSource.contains("AsyncPlayerSpawnLocationEvent event"));
assertFalse(irisSource.contains("AsyncPlayerSpawnLocationEvent"));
assertTrue(registration.contains("Class.forName(\"io.papermc.paper.event.player.AsyncPlayerSpawnLocationEvent\""));
assertTrue(registration.contains("Class.forName("));
assertTrue(registration.contains("PaperWorldReplacementEntryListener"));
assertTrue(irisSource.contains("pendingWorldReplacements.registerPlatformEntryListener();"));
assertTrue(listenerSource.contains("onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event)"));
}
@Test
public void redirectedPlayerReceiptSurvivesUntilTheMaterializedPositionIsSaved() throws Exception {
String managerSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java"));
String listenerSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java"));
String preparation = method(
managerSource,
"ReplacementEntryRedirect prepareReplacementEntry(UUID playerId, Location savedLocation, boolean newPlayer)"
);
String acknowledgement = method(
managerSource,
"void expectReplacementEntryAcknowledgement(UUID playerId, UUID transactionId)"
);
String join = method(managerSource, "public void onPlayerJoin(PlayerJoinEvent event)");
String listener = method(
listenerSource,
"public void onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event)"
);
assertTrue(preparation.contains("return new ReplacementEntryRedirect(guard.transactionId(), prepared, pendingPlayer)"));
assertFalse(preparation.substring(preparation.indexOf("CompletableFuture<Location> safeEntry"))
.contains("completeOverworldEntry("));
assertBefore(listener, "event.setSpawnLocation(location)",
"manager.expectReplacementEntryAcknowledgement(playerId, redirect.transactionId())");
assertTrue(acknowledgement.contains("pendingEntryAcknowledgements.put(playerId, transactionId)"));
assertBefore(join, "event.getPlayer().saveData()", "completeOverworldEntry(playerId, transactionId)");
}
@Test
public void replacementSpawnIsPersistedBeforeFinalMarkerRetirement() throws Exception {
String managerSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java"));
String listenerSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java"));
String preparation = method(managerSource, "private void prepareOverworldEntry(World world)");
String persistence = method(managerSource, "private CompletableFuture<Location> persistOverworldSpawn(Location safeEntry)");
String retirement = method(
managerSource,
"private synchronized void retireOverworldEntryIfCompleteAsync(UUID transactionId)"
);
String listener = method(
listenerSource,
"public void onAsyncPlayerSpawnLocation(AsyncPlayerSpawnLocationEvent event)"
);
String generatorSource = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")));
assertBefore(preparation, ".thenCompose(this::applyOverworldSpawn)",
".thenCompose(this::persistOverworldSpawn)");
assertBefore(preparation, "targetFuture.complete(safeEntry.clone())",
"retireOverworldEntryIfComplete(guard.transactionId())");
assertTrue(persistence.contains("world.save()"));
assertTrue(retirement.contains("!current.pendingPlayers().isEmpty()"));
assertTrue(retirement.contains("!safeEntry.isDone()"));
assertTrue(retirement.contains("safeEntry.isCompletedExceptionally()"));
assertTrue(listener.contains("event.isNewPlayer()"));
assertTrue(generatorSource.contains("world.getHighestBlockYAt(initialSpawn) + 1"));
}
private static PendingWorldReplacementManager.PublishedWorldRuntimeState runtimeState(
WorldSlotKey worldKey,
long seed,
+1 -1
View File
@@ -31,7 +31,7 @@ file('../../gradle.properties').withInputStream { InputStream stream -> rootProp
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522'))
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}
+2 -2
View File
@@ -30,8 +30,8 @@ Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.0.4'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969'))
String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.1.1'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522'))
Closure<String> loaderDisplayVersion = { String loaderVersion ->
String coordinatePrefix = "${minecraftVersion}-"
if (loaderVersion.startsWith(coordinatePrefix)) {
@@ -360,9 +360,8 @@ public final class NativeStructureVolumeIndex {
private static void warnOnce(String structureId, Throwable error) {
if (WARNED_RESOLUTION_FAILURES.add(structureId)) {
IrisLogging.warn("Native structure volume resolution failed for '" + structureId
+ "'; objects will not be vetoed against it: "
+ error.getClass().getSimpleName() + ":" + error.getMessage());
IrisLogging.reportError("Native structure volume resolution failed for '" + structureId
+ "'; objects will not be vetoed against it.", error);
}
}
@@ -53,6 +53,10 @@ public final class ModdedMixinAudit {
new ExpectedMixin("MobAwarenessMixin", "entity",
"net.minecraft.world.entity.Mob", "iris$tickUnawareMob",
false, ModdedMixinFlags::mobAwarenessRan),
new ExpectedMixin("StructureTemplatePaletteConcurrencyMixin", "common",
"net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate$Palette",
"iris$installConcurrentBlockCache",
false, ModdedMixinFlags::structureTemplatePaletteRan),
new ExpectedMixin("IrisWorldOpenFlowsMixin", "client",
"net.minecraft.client.gui.screens.worldselection.WorldOpenFlows",
"iris$openWorldCheckWorldStemCompatibility",
@@ -103,7 +107,7 @@ public final class ModdedMixinAudit {
LOGGER.error(" missing: {}", entry);
}
LOGGER.error("The mixin config was not registered for this loader (fabric.mod.json mixins, neoforge.mods.toml [[mixins]], forge MixinConfigs manifest attribute).");
LOGGER.error("Entity persistence, custom mob loot and Iris world-type labels are disabled until this is fixed.");
LOGGER.error("Entity persistence, custom mob loot, parallel structure safety, or Iris world-type labels are disabled until this is fixed.");
LOGGER.error("===============================================================");
}
@@ -31,6 +31,7 @@ public final class ModdedMixinFlags {
private static volatile boolean entityPersistenceRan;
private static volatile boolean livingEntityLootRan;
private static volatile boolean mobAwarenessRan;
private static volatile boolean structureTemplatePaletteRan;
private static volatile boolean worldOpenFlowsRan;
private static volatile boolean worldTypeEntryRan;
@@ -58,6 +59,12 @@ public final class ModdedMixinFlags {
}
}
public static void markStructureTemplatePalette() {
if (!structureTemplatePaletteRan) {
structureTemplatePaletteRan = true;
}
}
public static void markWorldOpenFlows() {
if (!worldOpenFlowsRan) {
worldOpenFlowsRan = true;
@@ -82,6 +89,10 @@ public final class ModdedMixinFlags {
return mobAwarenessRan;
}
public static boolean structureTemplatePaletteRan() {
return structureTemplatePaletteRan;
}
public static boolean worldOpenFlowsRan() {
return worldOpenFlowsRan;
}
@@ -94,6 +105,7 @@ public final class ModdedMixinFlags {
entityPersistenceRan = false;
livingEntityLootRan = false;
mobAwarenessRan = false;
structureTemplatePaletteRan = false;
worldOpenFlowsRan = false;
worldTypeEntryRan = false;
}
@@ -50,10 +50,12 @@ import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkAccess;
@@ -73,6 +75,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.IntBinaryOperator;
/**
@@ -346,6 +349,11 @@ final class ModdedNativeStructureStage {
index++;
}
}
if (!placementGroups.isEmpty()) {
ServerLevel level = world.getLevel();
visitExistingPois(chunk, (position, state) -> level.updatePOIOnBlockStateChange(
position, Blocks.AIR.defaultBlockState(), state));
}
try {
int runtimeMinY = world.getMinY();
WorldgenTerrainHeightmaps.primeStructurePlacement(
@@ -405,6 +413,10 @@ final class ModdedNativeStructureStage {
}
}
static void visitExistingPois(ChunkAccess chunk, BiConsumer<BlockPos, BlockState> visitor) {
chunk.findBlocks(PoiTypes::hasPoi, visitor);
}
private static String nativeStructureBatchContext(List<NativePlacementGroup> placementGroups) {
if (placementGroups.isEmpty()) {
return "<no resolved native structures>";
@@ -423,9 +435,21 @@ final class ModdedNativeStructureStage {
WorldgenRandom random, BoundingBox area, ChunkPos chunkPos,
String structureId, StructureStart start,
IrisNativeStructureDecision decision) {
NativeStructurePostProcessor.place(world, structureManager, generator, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> generator.engine().getHeight(x, z, true) + generator.engine().getMinHeight());
Engine current = generator.engine();
int runtimeMinY = world.getMinY();
WorldGenLevel boundedWorld = ModdedNativeStructureWorldgenAccess.create(
world, chunkPos,
worldgenSurfaceHeight(current, runtimeMinY),
worldgenFloorHeight(current, runtimeMinY));
world.setCurrentlyGenerating(() -> "Iris native structure " + structureId);
try {
NativeStructurePostProcessor.place(
boundedWorld, structureManager, generator, random, area, chunkPos,
structureId, start, decision, this::resolvePaletteBlock,
(x, z) -> current.getHeight(x, z, true) + current.getMinHeight());
} finally {
world.setCurrentlyGenerating(null);
}
}
private List<List<Structure>> structuresByStep(Registry<Structure> registry) {
@@ -0,0 +1,558 @@
package art.arcane.iris.modded;
import art.arcane.iris.nativegen.WorldgenTerrainHeightmaps;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.core.QuartPos;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.attribute.EnvironmentAttributeReader;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.flag.FeatureFlagSet;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LightLayer;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkSource;
import net.minecraft.world.level.chunk.EmptyLevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.gameevent.GameEvent;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.lighting.LevelLightEngine;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
import java.util.function.Supplier;
final class ModdedNativeStructureWorldgenAccess implements WorldGenLevel {
private static final int WRITE_RADIUS = 1;
private final WorldGenLevel delegate;
private final ChunkPos generationCenter;
private final IntBinaryOperator surfaceFirstFreeY;
private final IntBinaryOperator floorFirstFreeY;
private final Holder<Biome> fallbackBiome;
private final BiomeManager biomeManager;
private final AABB generationBounds;
private final LevelTickAccess<Block> blockTicks;
private final LevelTickAccess<Fluid> fluidTicks;
private final Long2LongOpenHashMap terrainHeights;
private final Long2ObjectOpenHashMap<ChunkAccess> outsideChunks;
private ModdedNativeStructureWorldgenAccess(WorldGenLevel delegate, Boundary boundary) {
this.delegate = Objects.requireNonNull(delegate, "Native structure world access requires a delegate");
this.generationCenter = Objects.requireNonNull(
boundary.generationCenter(), "Native structure world access requires a generation center");
this.surfaceFirstFreeY = Objects.requireNonNull(
boundary.surfaceFirstFreeY(), "Native structure world access requires a surface resolver");
this.floorFirstFreeY = Objects.requireNonNull(
boundary.floorFirstFreeY(), "Native structure world access requires an ocean-floor resolver");
BlockPos biomeSample = generationCenter.getMiddleBlockPosition(delegate.getSeaLevel());
this.fallbackBiome = delegate.getBiome(biomeSample);
this.biomeManager = delegate.getBiomeManager().withDifferentSource(this);
this.generationBounds = new AABB(
generationCenter.getMinBlockX() - 16,
delegate.getMinY(),
generationCenter.getMinBlockZ() - 16,
generationCenter.getMaxBlockX() + 17,
delegate.getMinY() + delegate.getHeight(),
generationCenter.getMaxBlockZ() + 17);
this.blockTicks = new BoundedTickAccess<>(delegate.getBlockTicks(), this::isWritable);
this.fluidTicks = new BoundedTickAccess<>(delegate.getFluidTicks(), this::isWritable);
this.terrainHeights = new Long2LongOpenHashMap();
this.terrainHeights.defaultReturnValue(Long.MIN_VALUE);
this.outsideChunks = new Long2ObjectOpenHashMap<>();
}
static ModdedNativeStructureWorldgenAccess create(WorldGenLevel delegate, ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
return new ModdedNativeStructureWorldgenAccess(delegate, new Boundary(
generationCenter, surfaceFirstFreeY, floorFirstFreeY));
}
@Override
public long getSeed() {
return delegate.getSeed();
}
@Override
public boolean ensureCanWrite(BlockPos position) {
return isWritable(position) && delegate.ensureCanWrite(position);
}
@Override
public void setCurrentlyGenerating(Supplier<String> description) {
delegate.setCurrentlyGenerating(description);
}
@Override
public ServerLevel getLevel() {
return delegate.getLevel();
}
@Override
public DifficultyInstance getCurrentDifficultyAt(BlockPos position) {
if (isReadable(position)) {
return delegate.getCurrentDifficultyAt(position);
}
return delegate.getCurrentDifficultyAt(generationCenter.getMiddleBlockPosition(
Mth.clamp(position.getY(), getMinY(), getMaxY() - 1)));
}
@Override
public long nextSubTickCount() {
return delegate.nextSubTickCount();
}
@Override
public LevelTickAccess<Block> getBlockTicks() {
return blockTicks;
}
@Override
public LevelTickAccess<Fluid> getFluidTicks() {
return fluidTicks;
}
@Override
public LevelData getLevelData() {
return delegate.getLevelData();
}
@Override
public MinecraftServer getServer() {
return delegate.getServer();
}
@Override
public ChunkSource getChunkSource() {
return delegate.getChunkSource();
}
@Override
public RandomSource getRandom() {
return delegate.getRandom();
}
@Override
public void updateNeighborsAt(BlockPos position, Block block) {
if (isWritableNeighbourhood(position)) {
delegate.updateNeighborsAt(position, block);
}
}
@Override
public void neighborShapeChanged(Direction direction, BlockPos position,
BlockPos neighbourPosition, BlockState neighbourState,
int updateFlags, int updateLimit) {
if (isWritable(position) && isWritable(neighbourPosition)) {
delegate.neighborShapeChanged(
direction, position, neighbourPosition, neighbourState, updateFlags, updateLimit);
}
}
@Override
public void playSound(Entity source, BlockPos position, SoundEvent sound,
SoundSource soundSource, float volume, float pitch) {
if (isWritable(position)) {
delegate.playSound(source, position, sound, soundSource, volume, pitch);
}
}
@Override
public void addParticle(ParticleOptions particle, double x, double y, double z,
double velocityX, double velocityY, double velocityZ) {
if (isWritable(BlockPos.containing(x, y, z))) {
delegate.addParticle(particle, x, y, z, velocityX, velocityY, velocityZ);
}
}
@Override
public void levelEvent(Entity source, int eventId, BlockPos position, int data) {
if (isWritable(position)) {
delegate.levelEvent(source, eventId, position, data);
}
}
@Override
public void gameEvent(Holder<GameEvent> event, Vec3 position, GameEvent.Context context) {
if (isWritable(BlockPos.containing(position))) {
delegate.gameEvent(event, position, context);
}
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus status, boolean create) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunk(chunkX, chunkZ, status, create);
}
return create ? outsideChunk(chunkX, chunkZ) : null;
}
@Override
public boolean hasChunk(int chunkX, int chunkZ) {
return isInsideGenerationRegion(chunkX, chunkZ) && delegate.hasChunk(chunkX, chunkZ);
}
@Override
public int getHeight(Heightmap.Types type, int x, int z) {
if (isInsideGenerationRegion(x >> 4, z >> 4)) {
return delegate.getHeight(type, x, z);
}
return height(type, x, z);
}
@Override
public int getSkyDarken() {
return delegate.getSkyDarken();
}
@Override
public BiomeManager getBiomeManager() {
return biomeManager;
}
@Override
public Holder<Biome> getNoiseBiome(int quartX, int quartY, int quartZ) {
return getUncachedNoiseBiome(quartX, quartY, quartZ);
}
@Override
public Holder<Biome> getUncachedNoiseBiome(int quartX, int quartY, int quartZ) {
if (isInsideGenerationRegion(QuartPos.toSection(quartX), QuartPos.toSection(quartZ))) {
return delegate.getUncachedNoiseBiome(quartX, quartY, quartZ);
}
return fallbackBiome;
}
@Override
public boolean isClientSide() {
return delegate.isClientSide();
}
@Override
public int getSeaLevel() {
return delegate.getSeaLevel();
}
@Override
public DimensionType dimensionType() {
return delegate.dimensionType();
}
@Override
public int getMinY() {
return delegate.getMinY();
}
@Override
public int getHeight() {
return delegate.getHeight();
}
@Override
public RegistryAccess registryAccess() {
return delegate.registryAccess();
}
@Override
public FeatureFlagSet enabledFeatures() {
return delegate.enabledFeatures();
}
@Override
public EnvironmentAttributeReader environmentAttributes() {
return delegate.environmentAttributes();
}
@Override
public LevelLightEngine getLightEngine() {
return delegate.getLightEngine();
}
@Override
public int getBrightness(LightLayer layer, BlockPos position) {
if (isReadable(position)) {
return delegate.getBrightness(layer, position);
}
return layer == LightLayer.SKY && canSeeSky(position) ? 15 : 0;
}
@Override
public int getRawBrightness(BlockPos position, int ambientDarkening) {
if (isReadable(position)) {
return delegate.getRawBrightness(position, ambientDarkening);
}
return Math.max(0, getBrightness(LightLayer.SKY, position) - ambientDarkening);
}
@Override
public boolean canSeeSky(BlockPos position) {
if (isReadable(position)) {
return delegate.canSeeSky(position);
}
return position.getY() >= height(Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ());
}
@Override
public WorldBorder getWorldBorder() {
return delegate.getWorldBorder();
}
@Override
public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) {
if (isInsideGenerationRegion(chunkX, chunkZ)) {
return delegate.getChunkForCollisions(chunkX, chunkZ);
}
return outsideChunk(chunkX, chunkZ);
}
@Override
public BlockEntity getBlockEntity(BlockPos position) {
return isReadable(position) ? delegate.getBlockEntity(position) : null;
}
@Override
public <T extends BlockEntity> Optional<T> getBlockEntity(
BlockPos position, BlockEntityType<T> type) {
return isReadable(position) ? delegate.getBlockEntity(position, type) : Optional.empty();
}
@Override
public BlockState getBlockState(BlockPos position) {
return isReadable(position) ? delegate.getBlockState(position) : terrainState(position);
}
@Override
public FluidState getFluidState(BlockPos position) {
return isReadable(position) ? delegate.getFluidState(position) : terrainState(position).getFluidState();
}
@Override
public List<Entity> getEntities(Entity source, AABB area, Predicate<? super Entity> predicate) {
AABB boundedArea = boundedArea(area);
return boundedArea == null ? List.of() : delegate.getEntities(source, boundedArea, predicate);
}
@Override
public <T extends Entity> List<T> getEntities(
EntityTypeTest<Entity, T> type,
AABB area, Predicate<? super T> predicate) {
AABB boundedArea = boundedArea(area);
return boundedArea == null ? List.of() : delegate.getEntities(type, boundedArea, predicate);
}
@Override
public List<? extends Player> players() {
return delegate.players();
}
@Override
public boolean isStateAtPosition(BlockPos position, Predicate<BlockState> predicate) {
return predicate.test(getBlockState(position));
}
@Override
public boolean isFluidAtPosition(BlockPos position, Predicate<FluidState> predicate) {
return predicate.test(getFluidState(position));
}
@Override
public BlockPos getHeightmapPos(Heightmap.Types type, BlockPos position) {
return position.atY(getHeight(type, position.getX(), position.getZ()));
}
@Override
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
return isWritable(position) && delegate.setBlock(position, state, updateFlags, updateLimit);
}
@Override
public boolean removeBlock(BlockPos position, boolean move) {
return isWritable(position) && delegate.removeBlock(position, move);
}
@Override
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
return isWritable(position) && delegate.destroyBlock(position, drop, source, updateLimit);
}
@Override
public boolean addFreshEntity(Entity entity) {
return isWritable(entity.blockPosition()) && delegate.addFreshEntity(entity);
}
private boolean isReadable(BlockPos position) {
return isInsideGenerationRegion(position.getX() >> 4, position.getZ() >> 4)
&& !isOutsideBuildHeight(position);
}
private boolean isWritable(BlockPos position) {
return isReadable(position);
}
private boolean isWritableNeighbourhood(BlockPos position) {
return isWritable(position)
&& isWritable(position.north())
&& isWritable(position.south())
&& isWritable(position.east())
&& isWritable(position.west())
&& isWritable(position.above())
&& isWritable(position.below());
}
private AABB boundedArea(AABB area) {
double minX = Math.max(area.minX, generationBounds.minX);
double minY = Math.max(area.minY, generationBounds.minY);
double minZ = Math.max(area.minZ, generationBounds.minZ);
double maxX = Math.min(area.maxX, generationBounds.maxX);
double maxY = Math.min(area.maxY, generationBounds.maxY);
double maxZ = Math.min(area.maxZ, generationBounds.maxZ);
if (minX >= maxX || minY >= maxY || minZ >= maxZ) {
return null;
}
if (minX == area.minX && minY == area.minY && minZ == area.minZ
&& maxX == area.maxX && maxY == area.maxY && maxZ == area.maxZ) {
return area;
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
boolean isInsideGenerationRegion(int chunkX, int chunkZ) {
return Math.abs(chunkX - generationCenter.x()) <= WRITE_RADIUS
&& Math.abs(chunkZ - generationCenter.z()) <= WRITE_RADIUS;
}
@Override
public boolean isOutsideBuildHeight(BlockPos position) {
return position.getY() < getMinY() || position.getY() >= getMaxY();
}
private BlockState terrainState(BlockPos position) {
if (isOutsideBuildHeight(position)) {
return Blocks.VOID_AIR.defaultBlockState();
}
long heights = heights(position.getX(), position.getZ());
int surface = (int) (heights >> 32);
int floor = (int) heights;
if (position.getY() < floor) {
return Blocks.STONE.defaultBlockState();
}
if (position.getY() < surface) {
return Blocks.WATER.defaultBlockState();
}
return Blocks.AIR.defaultBlockState();
}
private int height(Heightmap.Types type, int x, int z) {
long heights = heights(x, z);
if (type == Heightmap.Types.OCEAN_FLOOR || type == Heightmap.Types.OCEAN_FLOOR_WG) {
return (int) heights;
}
return (int) (heights >> 32);
}
private long heights(int x, int z) {
long key = ((long) x << 32) ^ (z & 0xffffffffL);
long cached = terrainHeights.get(key);
if (cached != Long.MIN_VALUE) {
return cached;
}
int floor = Mth.clamp(floorFirstFreeY.applyAsInt(x, z), getMinY(), getMaxY());
int surface = Mth.clamp(surfaceFirstFreeY.applyAsInt(x, z), floor, getMaxY());
long heights = ((long) surface << 32) | (floor & 0xffffffffL);
terrainHeights.put(key, heights);
return heights;
}
private ChunkAccess outsideChunk(int chunkX, int chunkZ) {
long key = ChunkPos.pack(chunkX, chunkZ);
ChunkAccess cached = outsideChunks.get(key);
if (cached != null) {
return cached;
}
EmptyLevelChunk chunk = new EmptyLevelChunk(getLevel(), new ChunkPos(chunkX, chunkZ), fallbackBiome);
WorldgenTerrainHeightmaps.primeTerrain(chunk, surfaceFirstFreeY, floorFirstFreeY);
outsideChunks.put(key, chunk);
return chunk;
}
private record Boundary(ChunkPos generationCenter,
IntBinaryOperator surfaceFirstFreeY,
IntBinaryOperator floorFirstFreeY) {
}
private static final class BoundedTickAccess<T> implements LevelTickAccess<T> {
private final LevelTickAccess<T> delegate;
private final Predicate<BlockPos> writable;
private BoundedTickAccess(LevelTickAccess<T> delegate, Predicate<BlockPos> writable) {
this.delegate = delegate;
this.writable = writable;
}
@Override
public void schedule(ScheduledTick<T> tick) {
if (writable.test(tick.pos())) {
delegate.schedule(tick);
}
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return writable.test(position) && delegate.hasScheduledTick(position, type);
}
@Override
public int count() {
return delegate.count();
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
return writable.test(position) && delegate.willTickThisTick(position, type);
}
}
}
@@ -344,7 +344,7 @@ public final class IrisModdedCommands {
execution = new PackDownloadExecution(
lease,
cancellation -> executeDownload(source, request, target, downloadSource, scheduler, cancellation)
cancellation -> executeDownload(source, request, target, downloadSource, cancellation)
);
PackDownloadExecution trackedExecution = execution;
execution.onCompletion(() -> clearActiveDownload(trackedExecution));
@@ -384,7 +384,6 @@ public final class IrisModdedCommands {
DownloadRequest request,
String target,
String downloadSource,
ModdedScheduler scheduler,
PackDownloader.DownloadCancellation cancellation
) throws PackDownloader.PackDownloadCancelledException {
File packs = ModdedPackCommands.packsRoot();
@@ -394,37 +393,41 @@ public final class IrisModdedCommands {
packs,
request.url(),
false,
(String message) -> scheduler.global(() -> ok(source, message)),
(String message) -> dispatchDownloadFeedback(source, () -> ok(source, message)),
cancellation
)
: PackDownloader.downloadBuiltIn(
packs,
request.pack(),
false,
(String message) -> scheduler.global(() -> ok(source, message)),
(String message) -> dispatchDownloadFeedback(source, () -> ok(source, message)),
cancellation
);
String completionMessage = downloadCompletionMessage(result);
if (result != null) {
if (completionMessage != null) {
scheduler.global(() -> ok(source, completionMessage));
dispatchDownloadFeedback(source, () -> ok(source, completionMessage));
}
return;
}
} catch (PackDownloader.PackDownloadCancelledException error) {
throw error;
} catch (PackDownloader.PackDownloadBusyException error) {
scheduler.global(() -> fail(source, error.getMessage()));
dispatchDownloadFeedback(source, () -> fail(source, error.getMessage()));
return;
} catch (IOException | RuntimeException error) {
LOGGER.error("Iris pack download failed for {}", target, error);
}
scheduler.global(() -> fail(source, IrisLanguage.plain(
dispatchDownloadFeedback(source, () -> fail(source, IrisLanguage.plain(
ModdedCommandMessages.IRIS_MODDED_COMMANDS_PACK_DOWNLOAD_FAILED_SEE_CONSOLE,
MessageArgument.untrusted("pack", target),
MessageArgument.untrusted("downloadSource", downloadSource))));
}
private static void dispatchDownloadFeedback(CommandSourceStack source, Runnable feedback) {
source.getServer().execute(feedback);
}
static String downloadBusyMessage(LifecycleOperationCoordinator.ActiveOperation operation) {
if (operation.domain() == LifecycleOperationCoordinator.Domain.PACK_MUTATION
&& operation.kind() == LifecycleOperationCoordinator.OperationKind.PACK_DOWNLOAD) {
@@ -51,7 +51,14 @@ final class ModdedPregenCommands {
return 0;
}
boolean showGui = gui && ModdedGuiHost.isGuiLaunchable();
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache)) {
boolean started;
try {
started = ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache);
} catch (IllegalArgumentException failure) {
IrisModdedCommands.fail(source, failure.getMessage());
return 0;
}
if (!started) {
IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS));
return 0;
}
@@ -55,13 +55,13 @@ public final class ModdedPregenJob {
return false;
}
PregenPerformanceProfile.apply(engine);
PregenTask task = PregenTask.builder()
.gui(gui)
.center(new Position2(centerBlockX, centerBlockZ))
.radiusX(radiusBlocks)
.radiusZ(radiusBlocks)
.build();
PregenPerformanceProfile.apply(engine);
ModdedPregenMethod moddedMethod = new ModdedPregenMethod(level, engine, sync);
PregeneratorMethod method = moddedMethod;
if (cached) {
@@ -73,6 +73,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
private final AtomicBoolean finalSaveCompleted = new AtomicBoolean(false);
private final AtomicReference<FinalSaveRequest> queuedFinalSave = new AtomicReference<>();
private final AtomicBoolean stallHintLogged = new AtomicBoolean(false);
private final AtomicBoolean failureDetailLogged = new AtomicBoolean(false);
private final int timeoutSeconds;
private final PregenMantleBackpressure backpressure;
private final PauseWhenEmptyGuard pauseGuard;
@@ -350,7 +351,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
if (e instanceof TimeoutException) {
noteStallHint();
}
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, e.toString());
logChunkFailure(x, z, e);
listener.onChunkFailed(x, z);
} finally {
markFinished();
@@ -395,7 +396,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
if (unwrap(error) instanceof TimeoutException) {
onTimeout();
}
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, error.toString());
logChunkFailure(x, z, error);
listener.onChunkFailed(x, z);
return;
}
@@ -421,6 +422,15 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
inFlightPeak.accumulateAndGet(current, Math::max);
}
private void logChunkFailure(int x, int z, Throwable failure) {
Throwable cause = unwrap(failure);
if (failureDetailLogged.compareAndSet(false, true)) {
LOGGER.warn("Iris pregen chunk {},{} failed; first failure follows", x, z, cause);
return;
}
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, cause.toString());
}
private void markFinished() {
inFlight.decrementAndGet();
if (sync) {
@@ -0,0 +1,31 @@
package art.arcane.iris.modded.mixin;
import art.arcane.iris.modded.ModdedMixinFlags;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Mixin(StructureTemplate.Palette.class)
public abstract class StructureTemplatePaletteConcurrencyMixin {
@Shadow
@Final
@Mutable
private Map<Block, List<StructureTemplate.StructureBlockInfo>> cache;
@Inject(method = "<init>(Ljava/util/List;)V", at = @At("RETURN"))
private void iris$installConcurrentBlockCache(List<StructureTemplate.StructureBlockInfo> blocks,
CallbackInfo info) {
cache = new ConcurrentHashMap<>();
ModdedMixinFlags.markStructureTemplatePalette();
}
}
@@ -6,7 +6,8 @@
"mixins": [
"EntityPersistenceMixin",
"LivingEntityLootMixin",
"MobAwarenessMixin"
"MobAwarenessMixin",
"StructureTemplatePaletteConcurrencyMixin"
],
"injectors": {
"defaultRequire": 1
@@ -102,7 +102,12 @@ public class ModdedGenerationLeaseContractTest {
String execution = method(source, "private static void executeDownload(");
assertTrue(execution.contains("PackDownloader.DownloadCancellation cancellation"));
assertTrue(execution.contains("catch (PackDownloader.PackDownloadCancelledException error)"));
assertTrue(execution.contains("dispatchDownloadFeedback(source,"));
assertFalse(execution.contains("scheduler.global("));
assertFalse(execution.contains("lease.close();"));
String feedback = method(source, "private static void dispatchDownloadFeedback(");
assertTrue(feedback.contains("source.getServer().execute(feedback);"));
}
@Test
@@ -170,6 +175,34 @@ public class ModdedGenerationLeaseContractTest {
assertTrue(pending.contains("queuedFinalSave.get() != null"));
}
@Test
public void firstPregenChunkFailureRetainsItsFullCause() throws IOException {
String methodSource = source("art/arcane/iris/modded/command/ModdedPregenMethod.java");
String logFailure = method(methodSource, "private void logChunkFailure(");
assertTrue(methodSource.contains("AtomicBoolean failureDetailLogged"));
assertTrue(logFailure.contains("unwrap(failure)"));
assertTrue(logFailure.contains("failureDetailLogged.compareAndSet(false, true)"));
assertTrue(logFailure.contains("x, z, cause);"));
assertTrue(logFailure.contains("cause.toString()"));
}
@Test
public void invalidPregenBoundsFailBeforeRuntimeMutation() throws IOException {
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);");
assertTrue(taskConstruction >= 0);
assertTrue(profileMutation > taskConstruction);
String commandSource = source("art/arcane/iris/modded/command/ModdedPregenCommands.java");
String command = method(commandSource, "static int pregenStart(");
assertTrue(command.contains("catch (IllegalArgumentException failure)"));
assertTrue(command.contains("IrisModdedCommands.fail(source, failure.getMessage())"));
}
private static String source(String relativePath) throws IOException {
String root = System.getProperty(SOURCE_ROOT_PROPERTY);
assertTrue("Missing system property " + SOURCE_ROOT_PROPERTY, root != null && !root.isBlank());
@@ -0,0 +1,169 @@
package art.arcane.iris.modded;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeGenerationSettings;
import net.minecraft.world.level.biome.BiomeSpecialEffects;
import net.minecraft.world.level.biome.MobSpawnSettings;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.PalettedContainerRO;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class ModdedNativeStructurePoiRegistrationTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void visitsOnlyExistingPoiBlocksBeforeNativePlacement() {
ProtoChunk chunk = newChunk();
BlockPos poiPosition = new BlockPos(3, 70, 5);
BlockPos ordinaryPosition = new BlockPos(4, 70, 5);
BlockState poiState = Blocks.BARREL.defaultBlockState();
chunk.setBlockState(poiPosition, poiState, 0);
chunk.setBlockState(ordinaryPosition, Blocks.STONE.defaultBlockState(), 0);
Map<BlockPos, BlockState> visited = new LinkedHashMap<>();
ModdedNativeStructureStage.visitExistingPois(
chunk, (position, state) -> visited.put(position.immutable(), state));
assertEquals(poiState, visited.get(poiPosition));
assertFalse(visited.containsKey(ordinaryPosition));
assertEquals(1, visited.size());
}
@Test
public void duplicatePrimingIsIdempotentForAnAlreadyRegisteredPoi() {
BlockPos position = new BlockPos(3, 70, 5);
Holder<PoiType> type = PoiTypes.forState(
Blocks.BARREL.defaultBlockState()).orElseThrow();
PoiSection section = new PoiSection(() -> { });
assertNotNull(section.add(position, type));
assertNull(section.add(position, type));
assertEquals(type, section.getType(position).orElseThrow());
section.remove(position);
assertTrue(section.getType(position).isEmpty());
}
@Test
public void worldgenPrimingQueuesBeforeTheLaterVanillaRemoval() {
ProtoChunk chunk = newChunk();
BlockPos position = new BlockPos(3, 70, 5);
BlockState poiState = Blocks.BARREL.defaultBlockState();
chunk.setBlockState(position, poiState, 0);
PoiSection section = new PoiSection(() -> { });
Deque<Runnable> serverQueue = new ArrayDeque<>();
AtomicInteger missingRemovals = new AtomicInteger();
ModdedNativeStructureStage.visitExistingPois(chunk,
(poiPosition, state) -> queuePoiTransition(
serverQueue, section, poiPosition,
Blocks.AIR.defaultBlockState(), state, missingRemovals));
assertTrue(section.getType(position).isEmpty());
queuePoiTransition(serverQueue, section, position,
poiState, Blocks.AIR.defaultBlockState(), missingRemovals);
while (!serverQueue.isEmpty()) {
serverQueue.removeFirst().run();
}
assertEquals(0, missingRemovals.get());
assertTrue(section.getType(position).isEmpty());
}
private static void queuePoiTransition(Deque<Runnable> serverQueue,
PoiSection section,
BlockPos position,
BlockState oldState,
BlockState newState,
AtomicInteger missingRemovals) {
Optional<Holder<PoiType>> oldType = PoiTypes.forState(oldState);
Optional<Holder<PoiType>> newType = PoiTypes.forState(newState);
if (Objects.equals(oldType, newType)) {
return;
}
BlockPos immutablePosition = position.immutable();
oldType.ifPresent(type -> serverQueue.addLast(() -> {
if (section.getType(immutablePosition).isEmpty()) {
missingRemovals.incrementAndGet();
return;
}
section.remove(immutablePosition);
}));
newType.ifPresent(type -> serverQueue.addLast(
() -> section.add(immutablePosition, type)));
}
private static ProtoChunk newChunk() {
return new ProtoChunk(
new ChunkPos(0, 0),
UpgradeData.EMPTY,
LevelHeightAccessor.create(-64, 384),
palettedContainerFactory(),
null);
}
private static PalettedContainerFactory palettedContainerFactory() {
Strategy<BlockState> blockStrategy = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
Codec<PalettedContainer<BlockState>> blockCodec = PalettedContainer.codecRW(
BlockState.CODEC, blockStrategy, Blocks.AIR.defaultBlockState());
Biome biome = new Biome.BiomeBuilder()
.hasPrecipitation(false)
.temperature(0.8F)
.downfall(0.4F)
.specialEffects(new BiomeSpecialEffects.Builder().waterColor(0x3F76E4).build())
.mobSpawnSettings(MobSpawnSettings.EMPTY)
.generationSettings(BiomeGenerationSettings.EMPTY)
.build();
Holder<Biome> biomeHolder = Holder.direct(biome);
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>(1);
biomeIds.add(biomeHolder);
Strategy<Holder<Biome>> biomeStrategy = Strategy.createForBiomes(biomeIds);
Codec<PalettedContainerRO<Holder<Biome>>> biomeCodec = PalettedContainer.codecRO(
Biome.CODEC, biomeStrategy, biomeHolder);
return new PalettedContainerFactory(
blockStrategy,
Blocks.AIR.defaultBlockState(),
blockCodec,
biomeStrategy,
biomeHolder,
biomeCodec);
}
}
@@ -0,0 +1,323 @@
package art.arcane.iris.modded;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.server.Bootstrap;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.ticks.LevelTickAccess;
import net.minecraft.world.ticks.ScheduledTick;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ModdedNativeStructureWorldgenAccessTest {
private static final int GENERATION_CENTER_X = 60;
private static final int GENERATION_CENTER_Z = 15;
private static final int SURFACE_FIRST_FREE_Y = 80;
private static final int FLOOR_FIRST_FREE_Y = 70;
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void distanceTwoTerrainReadsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
ModdedNativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
int x = generationCenter.getMiddleBlockX();
int z = (generationCenter.z() + 2) << 4;
assertSame(Blocks.STONE, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y - 1, z)).getBlock());
assertSame(Blocks.WATER, access.getBlockState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)).getBlock());
assertSame(Blocks.AIR, access.getBlockState(new BlockPos(x, SURFACE_FIRST_FREE_Y, z)).getBlock());
assertSame(Blocks.WATER.defaultBlockState().getFluidState(),
access.getFluidState(new BlockPos(x, FLOOR_FIRST_FREE_Y, z)));
assertEquals(SURFACE_FIRST_FREE_Y, access.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z));
assertEquals(FLOOR_FIRST_FREE_Y, access.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, x, z));
assertNull(access.getChunk(generationCenter.x(), generationCenter.z() + 2, ChunkStatus.FEATURES, false));
List<BlockState> loadedOnly = access.getBlockStatesIfLoaded(new AABB(
x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList();
List<BlockState> streamed = access.getBlockStates(new AABB(
x, FLOOR_FIRST_FREE_Y, z, x, FLOOR_FIRST_FREE_Y, z)).toList();
assertTrue(loadedOnly.isEmpty());
assertEquals(1, streamed.size());
assertSame(Blocks.WATER, streamed.getFirst().getBlock());
assertEquals(0, recording.terrainReads);
assertEquals(0, recording.heightReads);
assertEquals(0, recording.chunkReads);
}
@Test
public void distanceTwoMutationsAndSideEffectsNeverReachTheDelegate() {
RecordingDelegate recording = new RecordingDelegate();
ModdedNativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
BlockPos position = new BlockPos(
generationCenter.getMiddleBlockX(),
FLOOR_FIRST_FREE_Y,
(generationCenter.z() + 2) << 4);
assertFalse(access.ensureCanWrite(position));
assertFalse(access.setBlock(position, Blocks.DIRT.defaultBlockState(), 2));
assertFalse(access.removeBlock(position, false));
assertFalse(access.destroyBlock(position, false));
access.updateNeighborsAt(position, Blocks.DIRT);
access.neighborShapeChanged(
Direction.NORTH, position, position.north(),
Blocks.DIRT.defaultBlockState(), 2, 512);
access.levelEvent(null, 2001, position, 0);
access.gameEvent(null, Vec3.atCenterOf(position), null);
access.addParticle(null, position.getX(), position.getY(), position.getZ(), 0, 0, 0);
access.getBlockTicks().schedule(new ScheduledTick<>(
Blocks.DIRT, position, 1L, 0L));
assertEquals(0, recording.mutations);
assertEquals(0, recording.events);
assertEquals(0, recording.blockTicks.count());
}
@Test
public void radiusOneReadsWritesAndTicksRemainUnchanged() {
RecordingDelegate recording = new RecordingDelegate();
ModdedNativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
BlockPos position = new BlockPos(
(generationCenter.x() + 1) << 4,
FLOOR_FIRST_FREE_Y,
(generationCenter.z() - 1) << 4);
assertTrue(access.isInsideGenerationRegion(
generationCenter.x() + 1, generationCenter.z() - 1));
assertSame(Blocks.DIRT, access.getBlockState(position).getBlock());
assertEquals(91, access.getHeight(
Heightmap.Types.WORLD_SURFACE_WG, position.getX(), position.getZ()));
assertTrue(access.ensureCanWrite(position));
assertTrue(access.setBlock(position, Blocks.STONE.defaultBlockState(), 2));
assertTrue(access.removeBlock(position, false));
assertTrue(access.destroyBlock(position, false));
access.getBlockTicks().schedule(new ScheduledTick<>(
Blocks.DIRT, position, 1L, 0L));
assertEquals(1, recording.terrainReads);
assertEquals(1, recording.heightReads);
assertEquals(4, recording.mutations);
assertEquals(1, recording.blockTicks.count());
}
@Test
public void statuslessChunkReadsPreserveWorldgenDelegateSemantics() {
RecordingDelegate recording = new RecordingDelegate();
ModdedNativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
assertNull(access.getChunk(generationCenter.getWorldPosition()));
assertEquals(1, recording.statuslessChunkReads);
assertEquals(0, recording.statusChunkReads);
}
@Test
public void entityQueriesRejectDisjointAreasAndClampOverlaps() {
RecordingDelegate recording = new RecordingDelegate();
ModdedNativeStructureWorldgenAccess access = access(recording);
ChunkPos generationCenter = generationCenter();
int safeMaxX = generationCenter.getMaxBlockX() + 17;
int safeMinZ = generationCenter.getMinBlockZ() - 16;
int safeMaxZ = generationCenter.getMaxBlockZ() + 17;
AABB disjoint = new AABB(
safeMaxX, -64, safeMinZ,
safeMaxX + 16, 320, safeMaxZ);
AABB overlap = new AABB(
safeMaxX - 8, -128, safeMinZ - 8,
safeMaxX + 16, 400, safeMaxZ + 8);
EntityTypeTest<Entity, Entity> type = EntityTypeTest.forClass(Entity.class);
assertTrue(access.getEntities((Entity) null, disjoint, entity -> true).isEmpty());
assertTrue(access.getEntities(type, disjoint, entity -> true).isEmpty());
access.getEntities((Entity) null, overlap, entity -> true);
access.getEntities(type, overlap, entity -> true);
assertEquals(2, recording.entityAreas.size());
for (AABB delegatedArea : recording.entityAreas) {
assertEquals(safeMaxX - 8, delegatedArea.minX, 0D);
assertEquals(-64, delegatedArea.minY, 0D);
assertEquals(safeMinZ, delegatedArea.minZ, 0D);
assertEquals(safeMaxX, delegatedArea.maxX, 0D);
assertEquals(320, delegatedArea.maxY, 0D);
assertEquals(safeMaxZ, delegatedArea.maxZ, 0D);
}
}
private static ModdedNativeStructureWorldgenAccess access(RecordingDelegate recording) {
return ModdedNativeStructureWorldgenAccess.create(
recording.world(), generationCenter(),
(x, z) -> SURFACE_FIRST_FREE_Y,
(x, z) -> FLOOR_FIRST_FREE_Y);
}
private static ChunkPos generationCenter() {
return new ChunkPos(GENERATION_CENTER_X, GENERATION_CENTER_Z);
}
private static final class RecordingDelegate implements InvocationHandler {
private final Holder<Biome> biome;
private final BiomeManager biomeManager;
private final RecordingTicks<Block> blockTicks;
private final RecordingTicks<Fluid> fluidTicks;
private final List<AABB> entityAreas;
private int terrainReads;
private int heightReads;
private int chunkReads;
private int statuslessChunkReads;
private int statusChunkReads;
private int mutations;
private int events;
private RecordingDelegate() {
this.biome = Holder.direct((Biome) null);
this.biomeManager = new BiomeManager((x, y, z) -> biome, 13L);
this.blockTicks = new RecordingTicks<>();
this.fluidTicks = new RecordingTicks<>();
this.entityAreas = new ArrayList<>();
}
private WorldGenLevel world() {
return (WorldGenLevel) Proxy.newProxyInstance(
WorldGenLevel.class.getClassLoader(),
new Class<?>[]{WorldGenLevel.class}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) {
String name = method.getName();
if (name.equals("getSeaLevel")) {
return 63;
}
if (name.equals("getBiome")) {
return biome;
}
if (name.equals("getBiomeManager")) {
return biomeManager;
}
if (name.equals("getBlockTicks")) {
return blockTicks;
}
if (name.equals("getFluidTicks")) {
return fluidTicks;
}
if (name.equals("getMinY")) {
return -64;
}
if (name.equals("getHeight") && arguments == null) {
return 384;
}
if (name.equals("getHeight")) {
heightReads++;
return 91;
}
if (name.equals("getBlockState")) {
terrainReads++;
return Blocks.DIRT.defaultBlockState();
}
if (name.equals("getFluidState")) {
terrainReads++;
return Blocks.WATER.defaultBlockState().getFluidState();
}
if (name.equals("getChunk")) {
chunkReads++;
if (arguments.length == 2) {
statuslessChunkReads++;
} else {
statusChunkReads++;
}
return null;
}
if (name.equals("getChunkIfLoadedImmediately")) {
chunkReads++;
return null;
}
if (name.equals("getEntities")) {
entityAreas.add((AABB) arguments[1]);
return List.of();
}
if (name.equals("ensureCanWrite") || name.equals("setBlock")
|| name.equals("removeBlock") || name.equals("destroyBlock")) {
mutations++;
return true;
}
if (name.equals("updateNeighborsAt") || name.equals("neighborShapeChanged")) {
mutations++;
return null;
}
if (name.equals("levelEvent") || name.equals("gameEvent") || name.equals("addParticle")) {
events++;
return null;
}
if (name.equals("hashCode")) {
return System.identityHashCode(proxy);
}
if (name.equals("equals")) {
return proxy == arguments[0];
}
if (name.equals("toString")) {
return "native-structure-worldgen-test-delegate";
}
throw new UnsupportedOperationException(method.toString());
}
}
private static final class RecordingTicks<T> implements LevelTickAccess<T> {
private int count;
@Override
public void schedule(ScheduledTick<T> tick) {
count++;
}
@Override
public boolean hasScheduledTick(BlockPos position, T type) {
return false;
}
@Override
public int count() {
return count;
}
@Override
public boolean willTickThisTick(BlockPos position, T type) {
return false;
}
}
}
@@ -0,0 +1,77 @@
package art.arcane.iris.modded;
import org.junit.Test;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class ModdedStructureTemplateCacheTest {
@Test
public void concurrentCacheOwnsDistinctAndSharedMappingsExactlyOnce() throws Exception {
ConcurrentHashMap<String, Integer> cache = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(32);
CountDownLatch start = new CountDownLatch(1);
AtomicInteger mappingCalls = new AtomicInteger();
List<Future<Integer>> futures = new ArrayList<>();
try {
for (int task = 0; task < 256; task++) {
int value = task;
String key = task % 2 == 0 ? "shared" : "distinct-" + task;
futures.add(executor.submit(() -> {
start.await();
return cache.computeIfAbsent(key, ignored -> {
mappingCalls.incrementAndGet();
Thread.yield();
return value;
});
}));
}
start.countDown();
for (Future<Integer> future : futures) {
assertNotNull(future.get(10, TimeUnit.SECONDS));
}
} finally {
executor.shutdownNow();
assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
}
assertEquals(129, cache.size());
assertEquals(129, mappingCalls.get());
}
@Test
public void requiredCommonMixinConfigRegistersPaletteConcurrencyFix() throws Exception {
InputStream resource = ModdedStructureTemplateCacheTest.class.getClassLoader()
.getResourceAsStream("irisworldgen.entity.mixins.json");
assertNotNull(resource);
String config;
try (InputStream input = resource) {
config = new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
assertTrue(config.contains("\"StructureTemplatePaletteConcurrencyMixin\""));
Path source = Path.of(System.getProperty("iris.moddedCommonSources"))
.resolve("art/arcane/iris/modded/mixin/StructureTemplatePaletteConcurrencyMixin.java");
String mixin = Files.readString(source);
assertTrue(mixin.contains("@Mixin(StructureTemplate.Palette.class)"));
assertTrue(mixin.contains("@Shadow\n @Final\n @Mutable"));
assertTrue(mixin.contains("private Map<Block, List<StructureTemplate.StructureBlockInfo>> cache;"));
assertTrue(mixin.contains("@Inject(method = \"<init>(Ljava/util/List;)V\", at = @At(\"RETURN\"))"));
assertTrue(mixin.contains("cache = new ConcurrentHashMap<>();"));
}
}
@@ -67,6 +67,11 @@ public class NativeStructureFailureContractTest {
assertTrue(placement.contains("\"terrain preparation\""));
assertTrue(placement.contains("\"foundation repair\""));
assertFalse(placement.contains("\"terrain carving\""));
assertTrue(placement.contains("visitExistingPois(chunk"));
assertTrue(placement.contains("level.updatePOIOnBlockStateChange("));
assertTrue(placement.contains("Blocks.AIR.defaultBlockState(), state"));
assertTrue(placement.indexOf("visitExistingPois(chunk")
< placement.indexOf("WorldgenTerrainHeightmaps.primeStructurePlacement("));
assertTrue(placement.contains("prepareSurfaceStructures"));
assertTrue(placement.contains("clearIntersectingVegetation"));
assertTrue(placement.indexOf("clearIntersectingVegetation")
@@ -95,6 +100,19 @@ public class NativeStructureFailureContractTest {
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
}
@Test
public void nativeStructurePostProcessingUsesBoundedWorldgenAccess() throws IOException {
String source = moddedSource("ModdedNativeStructureStage.java");
int placementStart = source.indexOf("private void placeVanillaStructure");
int placementEnd = source.indexOf("private List<List<Structure>> structuresByStep", placementStart);
String placement = source.substring(placementStart, placementEnd);
assertTrue(placement.contains("ModdedNativeStructureWorldgenAccess.create("));
assertTrue(placement.contains("NativeStructurePostProcessor.place(\n boundedWorld"));
assertFalse(placement.contains("NativeStructurePostProcessor.place(world,"));
assertTrue(placement.contains("world.setCurrentlyGenerating(null);"));
}
@Test
public void structureFailurePreservesPhaseIdentityChunkAndCause() {
IllegalArgumentException cause = new IllegalArgumentException("broken placement");
+2 -2
View File
@@ -30,8 +30,8 @@ Properties rootProperties = new Properties()
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.2'))
String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.2.0.12-beta'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:d9026a7c8ebc391c8109f401ce79a0ce65df3969'))
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.2.0.59'))
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:5486ac97ad6d6833e27275dc768222d377721522'))
Closure<String> irisArtifactName = { String platform, String targetVersion ->
return "Iris v${project.version} [${platform}] ${targetVersion}.jar"
}