Preserved historical biome behavior

- Generated chunks retain surface and vertical biome assignments
- Floating biome assignments persist through terrain transitions
- Archived pack definitions remain available for saved biome rules
- Spawns and effects use each saved biome environment
- Saved biome lookups load asynchronously with bounded caches
- Missing biome identities recover only from exact recorded evidence
- Effects and entity placement follow existing world surfaces
- New pack fingerprints ignore Finder metadata
- Tests cover biome storage, recovery, consumers, and scoped definitions
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-05 20:30:50 -04:00
1 parent d10d70a894
commit b17fe811f3
57 files changed
+4460 -385

No files matched your search

@@ -333,6 +333,10 @@ public class CustomBiomeSource extends BiomeSource {
return Collections.unmodifiableMap(spawnBiomes);
}
Holder<Biome> getRetainedVanillaSpawnBiome(String derivativeKey) {
return resolveBiomeHolder(biomeRegistry, derivativeKey);
}
Holder<Biome> getVanillaSpawnBiome(Holder<Biome> biome) {
if (biome == null) {
return null;
@@ -18,6 +18,7 @@ import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.NativeTerrainReceipt;
import art.arcane.iris.engine.history.NativeBiomeSpawnSelection;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
@@ -929,11 +930,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
@Override
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
NativeBiomeSpawnSelection selection = NativeBiomeSpawnSelection.at(
engine, blockposition.getX(), blockposition.getY(), blockposition.getZ(),
holder.unwrapKey().map(key -> key.identifier().toString()).orElse(""));
if (selection.mode() == NativeBiomeSpawnSelection.Mode.LOADING) {
return WeightedList.of(List.of());
}
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
blockposition.getX(), blockposition.getZ(), "bukkit_nms_mob_spawns");
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_mob_spawns");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition);
return getMobsAtWithActiveRuntime(holder, structuremanager, enumcreaturetype, blockposition, selection);
}
}
@@ -941,9 +948,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
Holder<Biome> holder,
StructureManager structuremanager,
MobCategory enumcreaturetype,
BlockPos blockposition
BlockPos blockposition,
NativeBiomeSpawnSelection selection
) {
Holder<Biome> vanillaSpawnBiome = customBiomeSource.getVanillaSpawnBiome(holder);
Holder<Biome> vanillaSpawnBiome = switch (selection.mode()) {
case RETAINED -> customBiomeSource.getRetainedVanillaSpawnBiome(selection.derivativeKey());
case CURRENT -> customBiomeSource.getVanillaSpawnBiome(holder);
case NONE, LOADING -> null;
};
if (vanillaSpawnBiome == null) {
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
}
@@ -964,7 +976,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
}
int spawnRuntimeId = engine.getCacheID();
SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), enumcreaturetype);
SpawnTableKey key = new SpawnTableKey(spawnRuntimeId, holder.value(), vanillaSpawnBiome.value(), enumcreaturetype);
return mergedSpawnTables.computeIfAbsent(key, ignored -> mergeSpawnTables(vanillaSpawns, explicitSpawns));
}
@@ -1611,7 +1623,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator implements LongPred
}
}
private record SpawnTableKey(int runtimeId, Biome biome, MobCategory category) {
private record SpawnTableKey(int runtimeId, Biome biome, Biome vanillaBiome, MobCategory category) {
}
private record ReachableStructureCache(IrisDimension dimension, int runtimeId, Set<String> keys) {
@@ -600,7 +600,8 @@ public final class IrisWorldGeneratorResolver {
throw new IllegalStateException("Immutable Iris generation pack at " + packRoot
+ " does not contain dimension " + dimensionKey + ".");
}
GenerationEpoch.DimensionContract actualContract = captureDimensionContract(data, dimension);
GenerationEpoch.DimensionContract actualContract = GenerationEpochContractFactory.createForEpoch(
dimension, dimensionTypeKey(data, dimension), epoch);
if (!epoch.dimensionContract().equals(actualContract)) {
throw new IllegalStateException("Immutable Iris generation pack dimension contract changed at "
+ packRoot + ".");
@@ -612,7 +613,15 @@ public final class IrisWorldGeneratorResolver {
IrisData data,
IrisDimension dimension
) {
String dimensionTypeKey = IrisPlatforms.get()
return GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
dimensionTypeKey(data, dimension)
);
}
private static String dimensionTypeKey(IrisData data, IrisDimension dimension) {
return IrisPlatforms.get()
.registries()
.generationRegistry()
.dimensionTypeResourceKey(
@@ -620,11 +629,6 @@ public final class IrisWorldGeneratorResolver {
dimension.getLoadKey(),
dimension.getDimensionTypeKey()
);
return GenerationEpochContractFactory.create(
dimension,
dimension.getLoadKey(),
dimensionTypeKey
);
}
private static File requireActivePack(GenerationHistory history) {
@@ -31,6 +31,7 @@ import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.common.director.DirectorExecutor;
import art.arcane.iris.util.common.plugin.VolmitSender;
@@ -82,21 +83,36 @@ public class CommandWhat implements DirectorExecutor {
@Director(description = "What biome am i in?", descriptionKey = "iris.director.commandwhat.director.what_biome_am_i", origin = DirectorOrigin.PLAYER)
public void biome() {
VolmitSender commandSender = sender();
Player player = player();
onPlayerThread(player, () -> describeBiome(commandSender, player));
}
private void describeBiome(VolmitSender commandSender, Player player) {
Location location = player.getLocation();
try {
IrisBiome b = engine().getBiome(player().getLocation().getBlockX(), player().getLocation().getBlockY() - player().getWorld().getMinHeight(), player().getLocation().getBlockZ());
Engine engine = IrisToolbelt.access(location.getWorld()).getEngine();
IrisBiome b = engine.getBiome(location.getBlockX(), location.getBlockY() - location.getWorld().getMinHeight(), location.getBlockZ());
Biome derivative = b.getDerivative();
NamespacedKey derivativeKey = resolveBiomeKey(derivative);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME, MessageArgument.untrusted("value", b.getLoadKey()), MessageArgument.untrusted("value2", derivativeKey == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : derivativeKey.getKey())));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME, MessageArgument.untrusted("value", b.getLoadKey()), MessageArgument.untrusted("value2", derivativeKey == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : derivativeKey.getKey())));
} catch (SavedBiomeUnavailableException e) {
if (e.getCause() != null) {
Iris.reportError(e);
}
commandSender.sendMessage(e.isLoading()
? "Saved biome data is loading. Run the command again shortly."
: e.getMessage());
} catch (Throwable e) {
Iris.reportError(e);
Biome biome = player().getLocation().getBlock().getBiome();
Biome biome = location.getBlock().getBiome();
NamespacedKey key = resolveBiomeKey(biome);
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_NON_IRIS_BIOME, MessageArgument.untrusted("value", key == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : key)));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_NON_IRIS_BIOME, MessageArgument.untrusted("value", key == null ? IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED) : key)));
if (key == null || key.getKey().equals("custom")) {
try {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DATA_PACK_BIOME_ID, MessageArgument.untrusted("value", INMS.get().getTrueBiomeBaseKey(player().getLocation())), MessageArgument.untrusted("value2", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(player().getLocation())))));
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_DATA_PACK_BIOME_ID, MessageArgument.untrusted("value", INMS.get().getTrueBiomeBaseKey(location)), MessageArgument.untrusted("value2", INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(location)))));
} catch (Throwable ee) {
Iris.reportError(ee);
}
@@ -118,6 +134,13 @@ public class CommandWhat implements DirectorExecutor {
IrisRegion r = EngineBukkitOps.getRegion(engine, chunk);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IREGION, MessageArgument.untrusted("value", r.getLoadKey()), MessageArgument.untrusted("value2", r.getName())));
} catch (SavedBiomeUnavailableException e) {
if (e.getCause() != null) {
Iris.reportError(e);
}
commandSender.sendMessage(e.isLoading()
? "Saved biome data is loading. Run the command again shortly."
: e.getMessage());
} catch (Throwable e) {
Iris.reportError(e);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_WHAT_IRIS_WORLDS_ONLY));
@@ -0,0 +1,80 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.localization.BukkitCommandMessagesExtended;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.iris.util.common.director.DirectorContext;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.localization.MessageArgument;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class CommandWhatBiomeTest {
@Test
public void scheduledBiomeDescriptionUsesThePlayersWorldAfterCommandContextClears() {
VolmitSender sender = mock(VolmitSender.class);
Player player = mock(Player.class);
World world = mock(World.class);
Engine engine = mock(Engine.class);
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
IrisBiome biome = mock(IrisBiome.class);
AtomicReference<Runnable> scheduled = new AtomicReference<>();
when(sender.player()).thenReturn(player);
when(sender.isPlayer()).thenReturn(true);
when(player.getWorld()).thenReturn(world);
when(player.getLocation()).thenReturn(new Location(world, 19, 70, -3));
when(world.getMinHeight()).thenReturn(-64);
when(generator.getEngine()).thenReturn(engine);
when(engine.getBiome(19, 134, -3)).thenReturn(biome);
when(biome.getLoadKey()).thenReturn("retained-flat");
try (MockedStatic<J> scheduler = mockStatic(J.class);
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class);
MockedStatic<IrisLanguage> language = mockStatic(IrisLanguage.class)) {
scheduler.when(() -> J.runEntity(eq(player), any(Runnable.class))).thenAnswer(invocation -> {
scheduled.set(invocation.getArgument(1));
return true;
});
toolbelt.when(() -> IrisToolbelt.access(world)).thenReturn(generator);
language.when(() -> IrisLanguage.plain(RuntimeUiMessages.STATUS_UNREGISTERED)).thenReturn("unregistered");
language.when(() -> IrisLanguage.text(eq(BukkitCommandMessagesExtended.COMMAND_WHAT_IBIOME),
any(MessageArgument.class), any(MessageArgument.class))).thenReturn("Iris biome: retained-flat");
DirectorContext.touch(sender);
try {
new CommandWhat().biome();
} finally {
DirectorContext.remove();
}
assertNull(DirectorContext.get());
assertNotNull(scheduled.get());
verifyNoInteractions(engine);
scheduled.get().run();
verify(engine).getBiome(19, 134, -3);
verify(sender).sendMessage("Iris biome: retained-flat");
verify(world, never()).getBlockAt(anyInt(), anyInt(), anyInt());
}
}
}
@@ -20,6 +20,7 @@ package art.arcane.iris.modded;
import art.arcane.iris.nativegen.NativeTransitionColumn;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.history.NativeBiomeSpawnSelection;
import java.util.Optional;
import art.arcane.iris.nativegen.NativeGenerationWriteGuard;
import art.arcane.iris.core.loader.IrisData;
@@ -920,6 +921,11 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(
Holder<Biome> biome, StructureManager structureManager, MobCategory category, BlockPos pos) {
Engine current = engine();
NativeBiomeSpawnSelection selection = NativeBiomeSpawnSelection.at(current, pos.getX(), pos.getY(), pos.getZ(),
biome.unwrapKey().map(key -> key.identifier().toString()).orElse(""));
if (selection.mode() == NativeBiomeSpawnSelection.Mode.LOADING) {
return WeightedList.of(List.of());
}
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openHistoryCoordinateScope(
current, pos.getX(), pos.getZ(), "modded_mob_spawn_table");
GenerationSessionLease lease = requireGenerationLease(current, "modded_mob_spawn_table");
@@ -933,8 +939,15 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
Registry<Biome> registry = structureManager.registryAccess().lookupOrThrow(Registries.BIOME);
spawnTables.initializeVanillaSpawnBiomes(registry);
Holder<Biome> vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
Holder<Biome> vanillaSpawnBiome;
if (selection.mode() == NativeBiomeSpawnSelection.Mode.RETAINED) {
vanillaSpawnBiome = spawnTables.resolveBiomeHolder(registry, selection.derivativeKey());
} else if (selection.mode() == NativeBiomeSpawnSelection.Mode.CURRENT) {
spawnTables.initializeVanillaSpawnBiomes(registry);
vanillaSpawnBiome = spawnTables.vanillaSpawnBiome(biome.value());
} else {
vanillaSpawnBiome = null;
}
if (vanillaSpawnBiome == null) {
return explicitSpawns;
}
@@ -951,6 +964,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return spawnTables.mergedSpawnTable(
current,
biome.value(),
vanillaSpawnBiome.value(),
category,
vanillaSpawns,
explicitSpawns
@@ -20,13 +20,13 @@ package art.arcane.iris.modded;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.framework.EngineAssignedComponent;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCommand;
import art.arcane.iris.engine.object.IrisCommandRegistry;
import art.arcane.iris.engine.object.IrisEffect;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.math.RNG;
import net.minecraft.core.Holder;
@@ -36,6 +36,7 @@ import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.protocol.game.ClientboundSoundPacket;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
@@ -193,18 +194,21 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
return;
}
samplePlayer(state);
try {
samplePlayer(state);
} catch (SavedBiomeUnavailableException unavailable) {
state.environment = null;
state.sampled(false);
return;
}
if (!IrisSettings.get().getWorld().isEffectSystem()) {
return;
}
IrisRegion region = state.region();
if (region != null) {
applyEffects(level, player, region.getEffects());
}
IrisBiome biome = state.biome();
if (biome != null) {
applyEffects(level, player, biome.getEffects());
BiomeEnvironment environment = state.environment;
try (BiomeEnvironment.Scope ignored = getEngine().openBiomeEnvironmentScope(environment)) {
applyEffects(level, player, environment.region().getEffects());
applyEffects(level, player, environment.biome().getEffects());
}
}
@@ -219,14 +223,14 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
return;
}
state.sampled(true);
state.lastPosition(player.getX(), player.getY(), player.getZ());
state.lastSample(now);
int blockX = floor(player.getX());
int blockY = floor(player.getY()) - getEngine().getWorld().minHeight();
int blockZ = floor(player.getZ());
state.biome(getEngine().getBiome(blockX, blockY, blockZ));
state.region(getEngine().getRegion(blockX, blockY, blockZ));
BiomeEnvironment environment = getEngine().getBiomeEnvironment(blockX, blockY, blockZ);
state.environment = environment;
state.sampled(true);
state.lastPosition(player.getX(), player.getY(), player.getZ());
state.lastSample(now);
}
private void applyEffects(ServerLevel level, ServerPlayer player, Iterable<IrisEffect> effects) {
@@ -299,9 +303,12 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
double sideways = RNG.r.d(-effect.getParticleDistanceWidth(), effect.getParticleDistanceWidth());
double surfaceX = player.getX() + direction.x * forward + direction.z * sideways;
double surfaceZ = player.getZ() + direction.z * forward - direction.x * sideways;
int surfaceY = getEngine().getHeight(floor(surfaceX), floor(surfaceZ));
if (level.getChunkSource().getChunkNow(floor(surfaceX) >> 4, floor(surfaceZ) >> 4) == null) {
return;
}
int surfaceY = level.getHeight(Heightmap.Types.OCEAN_FLOOR, floor(surfaceX), floor(surfaceZ));
double x = surfaceX + RNG.r.d();
double y = surfaceY + 1.0D + level.getMinY() + RNG.r.i(effect.getParticleOffset());
double y = surfaceY + RNG.r.i(effect.getParticleOffset());
double z = surfaceZ + RNG.r.d();
double altX = randomized(effect.getParticleAltX(), effect.isRandomAltX());
double altY = randomized(effect.getParticleAltY(), effect.isRandomAltY());
@@ -421,8 +428,7 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
private static final class PlayerState {
private ServerPlayer player;
private IrisBiome biome;
private IrisRegion region;
private BiomeEnvironment environment;
private double lastX;
private double lastY;
private double lastZ;
@@ -446,22 +452,6 @@ public final class ModdedEngineEffects extends EngineAssignedComponent implement
this.player = player;
}
private IrisBiome biome() {
return biome;
}
private void biome(IrisBiome biome) {
this.biome = biome;
}
private IrisRegion region() {
return region;
}
private void region(IrisRegion region) {
this.region = region;
}
private double lastX() {
return lastX;
}
@@ -175,7 +175,7 @@ final class ModdedGenerationHistoryStorage {
GenerationEpoch epoch = history.activeEpoch();
Path packRoot = normalize(history.activePackRoot());
validatePack(packRoot, epoch.packFingerprint());
validateDimension(packRoot, epoch.dimensionContract());
validateDimension(packRoot, epoch);
return new ActivePack(
packRoot,
epoch.dimensionContract().dimensionKey(),
@@ -194,8 +194,9 @@ final class ModdedGenerationHistoryStorage {
private static void validateDimension(
Path packRoot,
GenerationEpoch.DimensionContract recordedContract
GenerationEpoch epoch
) throws IOException {
GenerationEpoch.DimensionContract recordedContract = epoch.dimensionContract();
IrisData data = IrisData.openDatapackCompiler(packRoot.toFile());
try {
IrisDimension dimension = data.getDimensionLoader().load(recordedContract.dimensionKey(), false);
@@ -203,10 +204,10 @@ final class ModdedGenerationHistoryStorage {
throw new IOException("Immutable generation pack does not contain dimension '"
+ recordedContract.dimensionKey() + "'.");
}
GenerationEpoch.DimensionContract actualContract = GenerationEpochContractFactory.create(
GenerationEpoch.DimensionContract actualContract = GenerationEpochContractFactory.createForEpoch(
dimension,
dimension.getLoadKey(),
recordedContract.dimensionTypeKey()
recordedContract.dimensionTypeKey(),
epoch
);
if (!recordedContract.equals(actualContract)) {
throw new IOException("Immutable generation pack no longer matches its recorded dimension contract.");
@@ -137,10 +137,11 @@ final class ModdedSpawnTableMerger {
WeightedList<MobSpawnSettings.SpawnerData> mergedSpawnTable(
Engine engine,
Biome biome,
Biome vanillaBiome,
MobCategory category,
WeightedList<MobSpawnSettings.SpawnerData> vanillaSpawns,
WeightedList<MobSpawnSettings.SpawnerData> explicitSpawns) {
SpawnTableKey key = new SpawnTableKey(engine.getCacheID(), biome, category);
SpawnTableKey key = new SpawnTableKey(engine.getCacheID(), biome, vanillaBiome, category);
return mergedSpawnTables.computeIfAbsent(
key,
ignored -> NativeSpawnTableMerger.merge(vanillaSpawns, explicitSpawns)
@@ -153,7 +154,7 @@ final class ModdedSpawnTableMerger {
initializedRuntimeIdentities.remove(runtimeIdentity);
}
private Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
Holder<Biome> resolveBiomeHolder(Registry<Biome> registry, String key) {
if (key == null || key.isBlank()) {
return null;
}
@@ -176,6 +177,6 @@ final class ModdedSpawnTableMerger {
private record SpawnBiomeKey(int runtimeIdentity, Biome biome) {
}
private record SpawnTableKey(int runtimeIdentity, Biome biome, MobCategory category) {
private record SpawnTableKey(int runtimeIdentity, Biome biome, Biome vanillaBiome, MobCategory category) {
}
}
@@ -22,6 +22,8 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
import art.arcane.iris.engine.framework.EngineWorldManager;
import art.arcane.iris.engine.framework.LootResolver;
@@ -32,7 +34,6 @@ import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawnGroup;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.object.IrisSurface;
@@ -53,12 +54,16 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.level.NaturalSpawner;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.AABB;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
@@ -152,7 +157,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
private void recoverLoadedInitialSpawns(ServerLevel level) {
if (!markerSystemEnabled()) {
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
return;
}
long now = System.currentTimeMillis();
@@ -194,7 +199,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (initialSpawnQueue.isEmpty()) {
return;
}
if (!markerSystemEnabled()) {
if (!markerSystemEnabled() && !ambientSystemEnabled()) {
initialSpawnQueue.clear();
return;
}
@@ -217,6 +222,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
retry = true;
warmupMantleChunkAsync(key, chunkX, chunkZ);
}
} catch (SavedBiomeUnavailableException unavailable) {
retry = unavailable.isLoading();
} catch (Throwable e) {
IrisLogging.reportError(e);
retry = true;
@@ -230,7 +237,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
}
private boolean initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
boolean initialSpawnChunk(ServerLevel level, int chunkX, int chunkZ) {
if (!ModdedEntitySpawner.chunksSafe(level, chunkX, chunkZ)) {
return false;
}
@@ -244,9 +251,15 @@ public final class ModdedWorldManager implements EngineWorldManager {
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
try {
List<PreparedMarkerSpawn> markers = markerSystemEnabled()
? prepareMarkerSpawns(level, chunkX, chunkZ, chunk) : List.of();
Optional<BiomeEnvironment> environment = ambientSystemEnabled()
? resolveSurfaceEnvironment(chunkX, chunkZ) : Optional.empty();
chunk.raiseFlagUnchecked(INITIAL_SPAWN_COMPLETION_FLAG, () -> {
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, true);
scheduleInitialFollowUp(level, chunkX, chunkZ);
spawnPreparedMarkers(level, markers, true);
if (environment.isPresent()) {
scheduleInitialFollowUp(level, chunkX, chunkZ, environment.get());
}
});
} finally {
chunk.release();
@@ -254,7 +267,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
return true;
}
private void scheduleInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
private void scheduleInitialFollowUp(ServerLevel level, int chunkX, int chunkZ, BiomeEnvironment environment) {
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
if (scheduler == null) {
IrisLogging.error("Iris could not schedule the initial entity-spawn follow-up because the modded scheduler is unavailable.");
@@ -264,11 +277,11 @@ public final class ModdedWorldManager implements EngineWorldManager {
() -> EngineLifecycleTasks.run(
engine,
"modded_world_manager_initial_spawn_followup",
() -> runInitialFollowUp(level, chunkX, chunkZ)),
() -> runInitialFollowUp(level, chunkX, chunkZ, environment)),
RNG.r.i(5, 200));
}
private void runInitialFollowUp(ServerLevel level, int chunkX, int chunkZ) {
private void runInitialFollowUp(ServerLevel level, int chunkX, int chunkZ, BiomeEnvironment environment) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
if (closed || engine.isClosed() || mantle.isClosed() || !isEntitySpawningEnabledForCurrentWorld()) {
return;
@@ -280,16 +293,10 @@ public final class ModdedWorldManager implements EngineWorldManager {
return;
}
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
try {
if (markerSystemEnabled()) {
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, false);
if (ambientSystemEnabled()) {
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
spawnAmbient(level, chunkX, chunkZ, true, environment);
}
if (ambientSystemEnabled()) {
spawnAmbient(level, chunkX, chunkZ, true);
}
} finally {
chunk.release();
}
}
@@ -360,6 +367,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
long key = ambientChunkSample[RNG.r.nextInt(sampled)];
try {
ambientSpawnChunk(level, unpackX(key), unpackZ(key));
} catch (SavedBiomeUnavailableException unavailable) {
continue;
} catch (Throwable e) {
IrisLogging.reportError(e);
}
@@ -378,7 +387,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
try {
if (markerSystemEnabled()) {
spawnMarkerSpawners(level, chunkX, chunkZ, chunk, false);
spawnPreparedMarkers(level, prepareMarkerSpawns(level, chunkX, chunkZ, chunk), false);
}
if (ambientSystemEnabled()) {
spawnAmbient(level, chunkX, chunkZ, false);
@@ -388,29 +397,38 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
}
private void spawnMarkerSpawners(ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk, boolean initial) {
private List<PreparedMarkerSpawn> prepareMarkerSpawns(ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
int minHeight = engine.getWorld().minHeight();
KList<IrisPosition> obstructed = new KList<>();
List<PreparedMarkerSpawn> prepared = new ArrayList<>();
chunk.iterate(MatterMarker.class, (Integer x, Integer yf, Integer z, MatterMarker marker) -> {
String tag = marker.getTag();
if (tag.equals("cave_floor") || tag.equals("cave_ceiling")) {
return;
}
IrisMarker resolved = engine.getData().getMarkerLoader().load(tag);
if (resolved == null) {
return;
}
int worldX = (chunkX << 4) + (x & 15);
int worldZ = (chunkZ << 4) + (z & 15);
int worldY = yf + minHeight;
BiomeEnvironment environment;
try {
environment = engine.getBiomeEnvironment(worldX, yf, worldZ);
} catch (SavedBiomeUnavailableException unavailable) {
if (unavailable.isLoading()) {
throw unavailable;
}
return;
}
IrisMarker resolved = environment.data().getMarkerLoader().load(tag);
if (resolved == null) {
return;
}
if (resolved.isEmptyAbove() && aboveObstructed(level, worldX, worldY, worldZ)) {
obstructed.add(new IrisPosition(worldX, yf, worldZ));
return;
}
KList<IrisSpawner> spawners = resolveMarkerSpawners(resolved);
KList<IrisSpawner> spawners = resolveMarkerSpawners(resolved, environment);
if (spawners.isEmpty()) {
return;
}
@@ -418,18 +436,19 @@ public final class ModdedWorldManager implements EngineWorldManager {
if (chosen == null) {
return;
}
spawnFromSpawner(level, new IrisPosition(worldX, worldY, worldZ), chosen, initial);
prepared.add(new PreparedMarkerSpawn(new IrisPosition(worldX, worldY, worldZ), chosen, environment));
});
Mantle<Matter> mantle = engine.getMantle().getMantle();
for (IrisPosition position : obstructed) {
mantle.remove(position.getX(), position.getY(), position.getZ(), MatterMarker.class);
}
return List.copyOf(prepared);
}
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker) {
private KList<IrisSpawner> resolveMarkerSpawners(IrisMarker marker, BiomeEnvironment environment) {
KList<IrisSpawner> spawners = new KList<>();
for (String key : marker.getSpawners()) {
IrisSpawner spawner = engine.getData().getSpawnerLoader().load(key);
IrisSpawner spawner = environment.data().getSpawnerLoader().load(key);
if (spawner == null) {
IrisLogging.error("Cannot load spawner: " + key + " for marker on " + engine.getName());
continue;
@@ -440,6 +459,14 @@ public final class ModdedWorldManager implements EngineWorldManager {
return spawners;
}
private void spawnPreparedMarkers(ServerLevel level, List<PreparedMarkerSpawn> markers, boolean initial) {
for (PreparedMarkerSpawn marker : markers) {
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(marker.environment())) {
spawnFromSpawner(level, marker.position(), marker.spawner(), initial);
}
}
}
private void spawnFromSpawner(ServerLevel level, IrisPosition position, IrisSpawner spawner, boolean initial) {
KList<IrisEntitySpawn> spawns = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
if (spawns.isEmpty()) {
@@ -465,22 +492,41 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
}
private Optional<BiomeEnvironment> resolveSurfaceEnvironment(int chunkX, int chunkZ) {
try {
return Optional.of(engine.getSurfaceBiomeEnvironment((chunkX << 4) + 8, (chunkZ << 4) + 8));
} catch (SavedBiomeUnavailableException unavailable) {
if (unavailable.isLoading()) {
throw unavailable;
}
return Optional.empty();
}
}
private void spawnAmbient(ServerLevel level, int chunkX, int chunkZ, boolean initial) {
Optional<BiomeEnvironment> resolved = resolveSurfaceEnvironment(chunkX, chunkZ);
if (resolved.isEmpty()) {
return;
}
BiomeEnvironment environment = resolved.get();
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
spawnAmbient(level, chunkX, chunkZ, initial, environment);
}
}
private void spawnAmbient(ServerLevel level, int chunkX, int chunkZ, boolean initial, BiomeEnvironment environment) {
IrisComplex complex = engine.getComplex();
if (complex == null) {
return;
}
int blockX = chunkX << 4;
int blockZ = chunkZ << 4;
IrisBiome biome = engine.getSurfaceBiome(blockX, blockZ);
IrisRegion region = engine.getRegion(blockX, blockZ);
IrisBiome biome = environment.biome();
int chunkMobs = countChunkLivingEntities(level, chunkX, chunkZ);
KList<IrisEntitySpawn> pool = new KList<>();
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(engine.getDimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(region.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, engine.getData().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.dimension().getEntitySpawners()), biome, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(environment.region().getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
collectSpawns(pool, environment.data().getSpawnerLoader().loadAll(biome.getEntitySpawners()), null, chunkX, chunkZ, chunkMobs, initial);
if (pool.isEmpty()) {
return;
}
@@ -556,8 +602,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
} else {
worldX = (chunkX << 4) + RNG.r.i(16);
worldZ = (chunkZ << 4) + RNG.r.i(16);
int surfaceY = level.getMinY() + engine.getHeight(worldX, worldZ, false);
int solidY = level.getMinY() + engine.getHeight(worldX, worldZ, true);
int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, worldX, worldZ) - 1;
int solidY = level.getHeight(Heightmap.Types.OCEAN_FLOOR, worldX, worldZ) - 1;
worldY = group == IrisSpawnGroup.NORMAL
? surfaceY + 1
: RNG.r.i(solidY + 1, surfaceY);
@@ -864,4 +910,8 @@ public final class ModdedWorldManager implements EngineWorldManager {
}
return failure;
}
private record PreparedMarkerSpawn(IrisPosition position, IrisSpawner spawner, BiomeEnvironment environment) {
}
}
@@ -24,6 +24,7 @@ import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.iris.core.localization.ModdedCommandMessages;
import art.arcane.iris.core.localization.RuntimeUiMessages;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.object.IrisBiome;
@@ -194,6 +195,13 @@ public final class ModdedWhatCommands {
MessageArgument.untrusted("biome", nativeBiome.key()),
MessageArgument.trusted("id", nativeBiome.id())));
return 1;
} catch (SavedBiomeUnavailableException error) {
if (error.getCause() != null) {
ModdedIrisLog.error("Iris saved biome lookup failed in {}",
source.getLevel().dimension().identifier(), error);
}
IrisModdedCommands.fail(source, error.getMessage());
return 0;
} catch (Throwable error) {
logLookupFailure(source, "biome", error,
ModdedCommandMessages.IRIS_MODDED_COMMANDS_BIOME_LOOKUP_FAILED_2);
@@ -223,6 +231,13 @@ public final class ModdedWhatCommands {
MessageArgument.untrusted("region", region.getLoadKey()),
MessageArgument.untrusted("name", region.getName())));
return 1;
} catch (SavedBiomeUnavailableException error) {
if (error.getCause() != null) {
ModdedIrisLog.error("Iris saved biome lookup failed in {}",
source.getLevel().dimension().identifier(), error);
}
IrisModdedCommands.fail(source, error.getMessage());
return 0;
} catch (Throwable error) {
logLookupFailure(source, "region", error,
ModdedCommandMessages.IRIS_MODDED_COMMANDS_REGION_LOOKUP_FAILED_2);
@@ -0,0 +1,131 @@
package art.arcane.iris.modded;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerPlayer;
import org.junit.Test;
import org.junit.BeforeClass;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import org.mockito.MockedStatic;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedSavedBiomeConsumersTest {
@BeforeClass
public static void initializeRuntimeTypes() throws Exception {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
PlatformBlockState air = mock(PlatformBlockState.class);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState("AIR")).thenReturn(air);
Class.forName(EngineMantle.class.getName());
}
}
@Test
@SuppressWarnings("unchecked")
public void loadingDefinitionsLeaveInitialFlagUnclaimed() {
Engine engine = mock(Engine.class);
EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
ServerLevel level = mock(ServerLevel.class);
when(engine.getMantle()).thenReturn(engineMantle);
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
when(engineMantle.getMantle()).thenReturn(mantle);
when(mantle.isChunkLoaded(2, -1)).thenReturn(true);
when(mantle.getChunk(2, -1)).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk);
BiomeEnvironment environment = environment();
when(engine.getSurfaceBiomeEnvironment(40, -8))
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
.thenReturn(environment);
IrisSettings settings = new IrisSettings();
settings.getWorld().setAmbientEntitySpawningSystem(true);
settings.getWorld().setMarkerEntitySpawningSystem(false);
ModdedWorldManager manager = new ModdedWorldManager(engine);
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class);
MockedStatic<ModdedEntitySpawner> spawns = mockStatic(ModdedEntitySpawner.class)) {
configured.when(IrisSettings::get).thenReturn(settings);
spawns.when(() -> ModdedEntitySpawner.chunksSafe(level, 2, -1)).thenReturn(true);
assertThrows(SavedBiomeUnavailableException.class, () -> manager.initialSpawnChunk(level, 2, -1));
verify(chunk, never()).raiseFlagUnchecked(eq(ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG), any());
assertTrue(manager.initialSpawnChunk(level, 2, -1));
verify(chunk).raiseFlagUnchecked(eq(ModdedWorldManager.INITIAL_SPAWN_COMPLETION_FLAG), any());
} finally {
manager.close();
}
}
@Test
public void playerEffectsRetryLoadingWithoutMovement() throws Exception {
Engine engine = mock(Engine.class);
IrisWorld world = mock(IrisWorld.class);
ServerLevel level = mock(ServerLevel.class);
ServerPlayer player = mock(ServerPlayer.class);
when(engine.getWorld()).thenReturn(world);
when(world.minHeight()).thenReturn(-64);
when(player.isAlive()).thenReturn(true);
when(player.level()).thenReturn(level);
BiomeEnvironment environment = environment();
when(engine.getBiomeEnvironment(0, 64, 0))
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
.thenReturn(environment);
IrisSettings settings = new IrisSettings();
settings.getWorld().setEffectSystem(false);
Class<?> stateType = Class.forName(ModdedEngineEffects.class.getName() + "$PlayerState");
Constructor<?> constructor = stateType.getDeclaredConstructor(ServerPlayer.class);
constructor.setAccessible(true);
Object state = constructor.newInstance(player);
Method tick = ModdedEngineEffects.class.getDeclaredMethod("tickPlayer", ServerLevel.class, stateType);
tick.setAccessible(true);
Field selected = stateType.getDeclaredField("environment");
selected.setAccessible(true);
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class)) {
configured.when(IrisSettings::get).thenReturn(settings);
ModdedEngineEffects effects = new ModdedEngineEffects(engine);
tick.invoke(effects, level, state);
assertNull(selected.get(state));
tick.invoke(effects, level, state);
assertSame(environment, selected.get(state));
verify(engine, times(2)).getBiomeEnvironment(0, 64, 0);
}
}
private static BiomeEnvironment environment() {
return new BiomeEnvironment(3L, mock(IrisBiome.class), mock(IrisRegion.class),
mock(IrisDimension.class), mock(IrisData.class));
}
}