Merged biome retention

- Saved biome behavior is integrated with current Iris changes
This commit is contained in:
Brian Neumann-Fopiano
2026-09-05 20:30:52 -04:00
57 changed files with 4460 additions and 385 deletions
@@ -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.modded.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));
}
}
@@ -22,6 +22,7 @@ import art.arcane.iris.engine.EngineBackgroundTasks.BackgroundTaskDrain;
import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.EngineMetrics;
import art.arcane.iris.engine.framework.EngineMode;
@@ -130,6 +131,9 @@ public class IrisEngine implements Engine {
final ThreadLocal<RuntimeAssembly> runtimeAssembly = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final ThreadLocal<BiomeEnvironmentBinding> biomeEnvironmentScopes = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final GenerationRuntimeScopeState generationRuntimeScopes = new GenerationRuntimeScopeState();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@@ -543,18 +547,112 @@ public class IrisEngine implements Engine {
}
}
@Override
public BiomeEnvironment getBiomeEnvironment(int x, int y, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
if (saved.isPresent()) {
return saved.get();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome environment")) {
return Engine.super.getBiomeEnvironment(x, y, z);
}
}
@Override
public BiomeEnvironment getSurfaceBiomeEnvironment(int x, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, 0, z, true);
if (saved.isPresent()) {
return saved.get();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome environment")) {
return Engine.super.getSurfaceBiomeEnvironment(x, z);
}
}
@Override
public BiomeEnvironment.Scope openBiomeEnvironmentScope(BiomeEnvironment environment) {
BiomeEnvironmentBinding binding = new BiomeEnvironmentBinding(
Objects.requireNonNull(environment, "environment"), biomeEnvironmentScopes.get());
biomeEnvironmentScopes.set(binding);
return binding;
}
private Optional<BiomeEnvironment> resolveSavedBiomeEnvironment(int x, int y, int z, boolean surface) {
if (!usesSavedBiomeEnvironment()) {
return Optional.empty();
}
return generationHistoryRuntimeRouter.biomes().resolve(x, y + getWorld().minHeight(), z, surface);
}
private boolean usesSavedBiomeEnvironment() {
if (hasGenerationRuntimeScope() || generationHistoryRuntimeRouter == null) {
return false;
}
IrisContext context = IrisContext.get();
return context == null || context.getEngine() != this || context.getChunkContext() == null;
}
@BlockCoordinates
@Override
public IrisBiome getBiome(int x, int y, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
if (saved.isPresent()) {
return saved.get().biome();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome")) {
return Engine.super.getBiome(x, y, z);
}
}
@BlockCoordinates
@Override
public IrisBiome getBiomeOrMantle(int x, int y, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
if (saved.isPresent()) {
return saved.get().biome();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome or mantle biome")) {
return Engine.super.getBiomeOrMantle(x, y, z);
}
}
@BlockCoordinates
@Override
public IrisRegion getRegion(int x, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, 0, z, true);
if (saved.isPresent()) {
return saved.get().region();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a region")) {
return Engine.super.getRegion(x, z);
}
}
@BlockCoordinates
@Override
public IrisRegion getRegion(int x, int y, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
if (saved.isPresent()) {
return saved.get().region();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical region")) {
return Engine.super.getRegion(x, y, z);
}
}
@BlockCoordinates
@Override
public IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
if (saved.isPresent()) {
return saved.get().biome();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave or mantle biome")) {
return Engine.super.getCaveOrMantleBiome(x, y, z);
@@ -564,6 +662,12 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public IrisBiome getCaveBiome(int x, int z) {
if (usesSavedBiomeEnvironment()) {
Optional<BiomeEnvironment> saved = generationHistoryRuntimeRouter.biomes().resolveCaveBase(x, z);
if (saved.isPresent()) {
return saved.get().biome();
}
}
if (usesScopedNaturalTerrain()) {
return Engine.super.getCaveBiome(x, z);
}
@@ -587,6 +691,10 @@ public class IrisEngine implements Engine {
int z,
IrisDimensionCarvingResolver.State state
) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
if (saved.isPresent()) {
return saved.get().biome();
}
if (usesScopedNaturalTerrain()) {
return Engine.super.getCaveBiome(x, y, z, state);
}
@@ -599,6 +707,10 @@ public class IrisEngine implements Engine {
@BlockCoordinates
@Override
public IrisBiome getSurfaceBiome(int x, int z) {
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, 0, z, true);
if (saved.isPresent()) {
return saved.get().biome();
}
if (usesScopedNaturalTerrain()) {
return Engine.super.getSurfaceBiome(x, z);
}
@@ -1183,6 +1295,10 @@ public class IrisEngine implements Engine {
@Override
public IrisData getData() {
BiomeEnvironmentBinding environment = biomeEnvironmentScopes.get();
if (environment != null && !hasGenerationRuntimeScope()) {
return environment.environment.data();
}
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.target.getData();
@@ -1193,6 +1309,10 @@ public class IrisEngine implements Engine {
@Override
public IrisDimension getDimension() {
BiomeEnvironmentBinding environment = biomeEnvironmentScopes.get();
if (environment != null && !hasGenerationRuntimeScope()) {
return environment.environment.dimension();
}
RuntimeAssembly assembly = runtimeAssembly.get();
if (assembly != null) {
return assembly.target.getDimension();
@@ -1608,4 +1728,36 @@ public class IrisEngine implements Engine {
}
}
private final class BiomeEnvironmentBinding implements BiomeEnvironment.Scope {
private final BiomeEnvironment environment;
private final BiomeEnvironmentBinding previous;
private final Thread owner = Thread.currentThread();
private boolean closed;
private BiomeEnvironmentBinding(BiomeEnvironment environment, BiomeEnvironmentBinding previous) {
this.environment = environment;
this.previous = previous;
}
@Override
public void close() {
if (Thread.currentThread() != owner) {
throw new IllegalStateException("Biome environment scope must close on its owning thread.");
}
if (closed) {
return;
}
if (biomeEnvironmentScopes.get() != this) {
throw new IllegalStateException("Biome environment scopes must close in reverse order.");
}
if (previous == null) {
biomeEnvironmentScopes.remove();
} else {
biomeEnvironmentScopes.set(previous);
}
closed = true;
}
}
}
@@ -18,6 +18,10 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.object.IrisMarker;
import art.arcane.iris.engine.object.IrisPosition;
@@ -28,13 +32,16 @@ import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.matter.MatterMarker;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import org.bukkit.Chunk;
import org.bukkit.World;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* Resolves the mantle spawn markers of a chunk into spawners. On Folia the mantle read runs
@@ -49,80 +56,10 @@ final class MarkerSpawnScanner {
this.manager = manager;
}
Map<IrisPosition, KSet<IrisSpawner>> getSpawnersFromMarkers(Chunk c) {
Map<IrisPosition, KSet<IrisSpawner>> p = new KMap<>();
Set<IrisPosition> b = new KSet<>();
if (J.isFolia()) {
if (!manager.getMantle().isChunkLoaded(c.getX(), c.getZ())) {
manager.chunkMaintenance.warmupMantleChunkAsync(c.getX(), c.getZ());
}
return p;
}
manager.getMantle().iterateChunk(c.getX(), c.getZ(), MatterMarker.class, (x, y, z, t) -> {
if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = manager.getData().getMarkerLoader().load(t.getTag());
if (mark == null) {
return;
}
IrisPosition pos = new IrisPosition((c.getX() << 4) + x, y, (c.getZ() << 4) + z);
if (isMarkerObstructed(c, pos, mark.isEmptyAbove())) {
b.add(pos);
return;
}
for (String i : mark.getSpawners()) {
IrisSpawner m = manager.getData().getSpawnerLoader().load(i);
if (m == null) {
IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName());
continue;
}
if (m.isCompatExcluded()) {
continue;
}
m.setReferenceMarker(mark);
// This is so fucking incorrect its a joke
//noinspection ConstantConditions
if (m != null) {
p.computeIfAbsent(pos, (k) -> new KSet<>()).add(m);
}
}
});
for (IrisPosition i : b) {
manager.getEngine().getMantle().getMantle().remove(i.getX(), i.getY(), i.getZ(), MatterMarker.class);
}
return p;
}
void forEachMarkerSpawner(Chunk c, BiConsumer<IrisPosition, KSet<IrisSpawner>> consumer) {
if (c == null || consumer == null) {
return;
}
if (!J.isFolia()) {
int minY = manager.getEngine().getWorld().minHeight();
getSpawnersFromMarkers(c).forEach((relative, spawners) -> {
if (spawners.isEmpty()) {
return;
}
consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), spawners);
});
return;
}
int chunkX = c.getX();
int chunkZ = c.getZ();
World world = c.getWorld();
void scanMarkerSpawners(Chunk chunk, boolean initialOnly, Consumer<List<PreparedMarkerSpawn>> consumer) {
int chunkX = chunk.getX();
int chunkZ = chunk.getZ();
World world = chunk.getWorld();
long key = Cache.key(chunkX, chunkZ);
if (!markerScanQueue.add(key)) {
return;
@@ -130,57 +67,82 @@ final class MarkerSpawnScanner {
J.a(manager.managedTask("bukkit_world_manager_marker_scan", () -> {
try {
Map<IrisPosition, MarkerSpawnData> markerData = collectMarkerSpawnData(chunkX, chunkZ);
if (markerData.isEmpty()) {
if (initialOnly && manager.getMantle().hasFlag(chunkX, chunkZ, MantleFlag.INITIAL_SPAWNED_MARKER)) {
markerScanQueue.remove(key);
return;
}
J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_marker_scan_region", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
Chunk chunk = world.getChunkAt(chunkX, chunkZ);
int minY = manager.getEngine().getWorld().minHeight();
markerData.forEach((relative, data) -> {
if (data.spawners.isEmpty()) {
return;
}
if (isMarkerObstructed(chunk, relative, data.requiresEmptyAbove)) {
removeMarkerAsync(relative);
return;
}
consumer.accept(new IrisPosition(relative.getX(), relative.getY() + minY, relative.getZ()), data.spawners);
});
}));
} catch (Throwable e) {
IrisLogging.reportError(e);
} finally {
Map<IrisPosition, MarkerSpawnData> markerData = collectMarkerSpawnData(chunkX, chunkZ);
boolean accepted = J.runRegion(world, chunkX, chunkZ,
manager.managedTask("bukkit_world_manager_marker_scan_region", () -> {
try {
if (!world.isChunkLoaded(chunkX, chunkZ) || !Chunks.isSafe(world, chunkX, chunkZ)) {
return;
}
Chunk loaded = world.getChunkAt(chunkX, chunkZ);
int minimumY = manager.getEngine().getWorld().minHeight();
List<PreparedMarkerSpawn> prepared = new ArrayList<>(markerData.size());
for (Map.Entry<IrisPosition, MarkerSpawnData> entry : markerData.entrySet()) {
IrisPosition position = entry.getKey();
MarkerSpawnData data = entry.getValue();
if (data.spawners.isEmpty()) {
continue;
}
if (isMarkerObstructed(loaded, position, data.requiresEmptyAbove)) {
removeMarkerAsync(position);
continue;
}
prepared.add(new PreparedMarkerSpawn(new IrisPosition(position.getX(),
position.getY() + minimumY, position.getZ()), data.spawners, data.environment));
}
consumer.accept(List.copyOf(prepared));
} finally {
markerScanQueue.remove(key);
}
}, () -> markerScanQueue.remove(key)));
if (!accepted) {
markerScanQueue.remove(key);
}
} catch (SavedBiomeUnavailableException e) {
markerScanQueue.remove(key);
} catch (Throwable e) {
markerScanQueue.remove(key);
IrisLogging.reportError(e);
}
}, () -> markerScanQueue.remove(key)));
}
private Map<IrisPosition, MarkerSpawnData> collectMarkerSpawnData(int chunkX, int chunkZ) {
if (manager.getEngine() instanceof IrisEngine engine) {
engine.getGenerationHistoryRuntimeRouter().ifPresent(router -> router.biomes().prepareChunk(chunkX, chunkZ));
}
Map<IrisPosition, MarkerSpawnData> markerData = new KMap<>();
manager.getMantle().iterateChunk(chunkX, chunkZ, MatterMarker.class, (x, y, z, t) -> {
if (t.getTag().equals("cave_floor") || t.getTag().equals("cave_ceiling")) {
return;
}
IrisMarker mark = manager.getData().getMarkerLoader().load(t.getTag());
BiomeEnvironment environment;
try {
environment = manager.getEngine().getBiomeEnvironment((chunkX << 4) + x, y,
(chunkZ << 4) + z);
} catch (SavedBiomeUnavailableException e) {
if (e.isLoading()) {
throw e;
}
return;
}
IrisData definitions = environment.data();
IrisMarker mark = definitions.getMarkerLoader().load(t.getTag());
if (mark == null) {
return;
}
IrisPosition position = new IrisPosition((chunkX << 4) + x, y, (chunkZ << 4) + z);
MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData());
MarkerSpawnData data = markerData.computeIfAbsent(position, k -> new MarkerSpawnData(environment));
data.requiresEmptyAbove = data.requiresEmptyAbove || mark.isEmptyAbove();
for (String i : mark.getSpawners()) {
IrisSpawner spawner = manager.getData().getSpawnerLoader().load(i);
IrisSpawner spawner = definitions.getSpawnerLoader().load(i);
if (spawner == null) {
IrisLogging.error("Cannot load spawner: " + i + " for marker on " + manager.getName());
continue;
@@ -223,8 +185,16 @@ final class MarkerSpawnScanner {
}));
}
record PreparedMarkerSpawn(IrisPosition position, KSet<IrisSpawner> spawners, BiomeEnvironment environment) {
}
private static final class MarkerSpawnData {
private final KSet<IrisSpawner> spawners = new KSet<>();
private final BiomeEnvironment environment;
private boolean requiresEmptyAbove;
private MarkerSpawnData(BiomeEnvironment environment) {
this.environment = environment;
}
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.FloatingBiomeOverlay;
import art.arcane.iris.engine.history.SavedTerrainChunk;
import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
@@ -29,10 +30,10 @@ public final class ResolvedTerrainProvider {
private static final int MAXIMUM_HEIGHT_CHUNKS = 4_096;
private final Engine engine;
private final LinkedHashMap<Long, SavedTerrainChunk> completed = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> pending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, SavedTerrainChunk> rawCompleted = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> rawPending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, ResolvedTerrain> completed = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> pending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, ResolvedTerrain> rawCompleted = new LinkedHashMap<>(64, 0.75F, true);
private final ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> rawPending = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, int[]> heights = new LinkedHashMap<>(64, 0.75F, true);
public ResolvedTerrainProvider(Engine engine) {
@@ -40,7 +41,7 @@ public final class ResolvedTerrainProvider {
}
public TerrainBoundarySignature column(int blockX, int blockZ) {
return resolve(blockX >> 4, blockZ >> 4).column(blockX, blockZ);
return resolve(blockX >> 4, blockZ >> 4).terrain().column(blockX, blockZ);
}
public int height(int blockX, int blockZ, boolean ignoreFluid) {
@@ -51,7 +52,7 @@ public final class ResolvedTerrainProvider {
return cached[(ignoreFluid ? 256 : 0) + (blockX & 15) * 16 + (blockZ & 15)];
}
}
TerrainBoundarySignature column = resolve(blockX >> 4, blockZ >> 4).column(blockX, blockZ);
TerrainBoundarySignature column = resolve(blockX >> 4, blockZ >> 4).terrain().column(blockX, blockZ);
return ignoreFluid ? column.oceanFloorHeight() : column.surfaceHeight();
}
@@ -60,7 +61,9 @@ public final class ResolvedTerrainProvider {
SavedTerrainChunk terrain;
if (context.getComplex().getTransitionGenerationPlan() != null
&& context.getComplex().getTransitionGenerationPlan().hasTransitionAtChunk(x >> 4, z >> 4)) {
terrain = resolve(x >> 4, z >> 4);
ResolvedTerrain resolved = resolve(x >> 4, z >> 4);
terrain = resolved.terrain();
context.setFloatingBiomes(resolved.floatingBiomes());
copy(terrain, blocks, biomes, context);
} else {
mode.generateTerrain(x, z, blocks, biomes, multicore, context);
@@ -70,6 +73,7 @@ public final class ResolvedTerrainProvider {
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router != null) {
router.recordNaturalTerrain(terrain.boundaryOnly());
router.recordFloatingBiomes(x >> 4, z >> 4, context.getFloatingBiomes());
}
}
}
@@ -86,36 +90,36 @@ public final class ResolvedTerrainProvider {
}
}
private SavedTerrainChunk resolve(int chunkX, int chunkZ) {
private ResolvedTerrain resolve(int chunkX, int chunkZ) {
return resolve(chunkX, chunkZ, false);
}
private SavedTerrainChunk resolve(int chunkX, int chunkZ, boolean raw) {
LinkedHashMap<Long, SavedTerrainChunk> cache = raw ? rawCompleted : completed;
ConcurrentHashMap<Long, CompletableFuture<SavedTerrainChunk>> loads = raw ? rawPending : pending;
private ResolvedTerrain resolve(int chunkX, int chunkZ, boolean raw) {
LinkedHashMap<Long, ResolvedTerrain> cache = raw ? rawCompleted : completed;
ConcurrentHashMap<Long, CompletableFuture<ResolvedTerrain>> loads = raw ? rawPending : pending;
long key = chunkKey(chunkX, chunkZ);
synchronized (cache) {
SavedTerrainChunk cached = cache.get(key);
ResolvedTerrain cached = cache.get(key);
if (cached != null) {
return cached;
}
}
CompletableFuture<SavedTerrainChunk> result = new CompletableFuture<>();
CompletableFuture<SavedTerrainChunk> existing = loads.putIfAbsent(key, result);
CompletableFuture<ResolvedTerrain> result = new CompletableFuture<>();
CompletableFuture<ResolvedTerrain> existing = loads.putIfAbsent(key, result);
if (existing != null) {
return existing.join();
}
try {
synchronized (cache) {
SavedTerrainChunk published = cache.get(key);
ResolvedTerrain published = cache.get(key);
if (published != null) {
result.complete(published);
return published;
}
}
SavedTerrainChunk terrain = raw ? compute(chunkX, chunkZ) : contain(chunkX, chunkZ);
ResolvedTerrain terrain = raw ? compute(chunkX, chunkZ) : contain(chunkX, chunkZ);
if (!raw) {
cacheHeights(key, terrain);
cacheHeights(key, terrain.terrain());
}
synchronized (cache) {
cache.put(key, terrain);
@@ -156,14 +160,15 @@ public final class ResolvedTerrainProvider {
}
}
private SavedTerrainChunk contain(int chunkX, int chunkZ) {
SavedTerrainChunk raw = resolve(chunkX, chunkZ, true);
private ResolvedTerrain contain(int chunkX, int chunkZ) {
ResolvedTerrain raw = resolve(chunkX, chunkZ, true);
TransitionGenerationPlan plan = engine.getComplex().getTransitionGenerationPlan();
if (plan == null || !plan.hasTransitionAtChunk(chunkX, chunkZ)) {
return raw;
}
try {
return TransitionFluidContainment.contain(raw, plan, (x, z) -> rawColumn(plan, raw, x, z));
return new ResolvedTerrain(TransitionFluidContainment.contain(raw.terrain(), plan,
(x, z) -> rawColumn(plan, raw.terrain(), x, z)), raw.floatingBiomes());
} catch (IOException failure) {
throw new IllegalStateException("Unable to contain transition fluids at " + chunkX + "," + chunkZ, failure);
}
@@ -181,10 +186,10 @@ public final class ResolvedTerrainProvider {
return geometry;
}
return (x >> 4) == current.chunkX() && (z >> 4) == current.chunkZ()
? current.column(x, z).geometry() : resolve(x >> 4, z >> 4, true).column(x, z).geometry();
? current.column(x, z).geometry() : resolve(x >> 4, z >> 4, true).terrain().column(x, z).geometry();
}
private SavedTerrainChunk compute(int chunkX, int chunkZ) {
private ResolvedTerrain compute(int chunkX, int chunkZ) {
int x = Math.multiplyExact(chunkX, 16);
int z = Math.multiplyExact(chunkZ, 16);
try (GenerationHistoryRuntimeRouter.CoordinateScope runtimeScope = engine instanceof IrisEngine irisEngine
@@ -198,7 +203,7 @@ public final class ResolvedTerrainProvider {
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, engine.getHeight(), 16);
try (IrisContext.Scope ignored = IrisContext.open(engine, sessionId, context)) {
engine.getMode().generateTerrain(x, z, blocks, biomes, false, context);
return capture(x, z, blocks, biomes, context, false);
return new ResolvedTerrain(capture(x, z, blocks, biomes, context, false), context.getFloatingBiomes());
}
} catch (IOException failure) {
throw new IllegalStateException("Unable to scope natural terrain at " + chunkX + "," + chunkZ, failure);
@@ -207,6 +212,10 @@ public final class ResolvedTerrainProvider {
private SavedTerrainChunk capture(int x, int z, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, ChunkContext context, boolean boundaryOnly) {
FloatingBiomeOverlay floating = context.getFloatingBiomes();
if (floating != null) {
floating.retainHighestSurfaces((localX, localZ) -> highestSolid(blocks, localX, localZ));
}
try {
return IrisTransitionGeometryActuator.capture(x, z, blocks, biomes, engine.getMinHeight(), context, boundaryOnly);
} catch (IOException failure) {
@@ -214,6 +223,16 @@ public final class ResolvedTerrainProvider {
}
}
private static int highestSolid(Hunk<PlatformBlockState> blocks, int localX, int localZ) {
for (int y = blocks.getHeight() - 1; y >= 0; y--) {
PlatformBlockState state = blocks.getRaw(localX, y, localZ);
if (state != null && !state.isAir() && !state.isFluid()) {
return y;
}
}
return -1;
}
private static void copy(SavedTerrainChunk terrain, Hunk<PlatformBlockState> blocks,
Hunk<PlatformBiome> biomes, ChunkContext context) {
PlatformRegistries registries = IrisPlatforms.get().registries();
@@ -237,4 +256,8 @@ public final class ResolvedTerrainProvider {
}
}
}
private record ResolvedTerrain(SavedTerrainChunk terrain, FloatingBiomeOverlay floatingBiomes) {
}
}
@@ -20,18 +20,15 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.Chunks;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import org.bukkit.Chunk;
import org.bukkit.World;
@@ -280,37 +277,9 @@ final class WorldChunkMaintenance {
EngineBukkitOps.updateChunk(manager.getEngine(), chunk);
}
if (!manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) {
return;
if (manager.entitySpawner.isEntitySpawningEnabledForCurrentWorld()) {
manager.entitySpawner.spawnInitially(chunk);
}
if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
return;
}
if (!J.isFolia() && !manager.getMantle().isChunkLoaded(chunkX, chunkZ)) {
warmupMantleChunkAsync(chunkX, chunkZ);
return;
}
raiseInitialSpawnMarkerFlag(world, chunkX, chunkZ, () -> {
int delay = RNG.r.i(5, 200);
J.runRegion(world, chunkX, chunkZ, manager.managedTask("bukkit_world_manager_initial_spawn_followup", () -> {
if (!world.isChunkLoaded(chunkX, chunkZ)) {
return;
}
manager.entitySpawner.spawnIn(world.getChunkAt(chunkX, chunkZ), true);
}), delay);
Chunk markerChunk = world.getChunkAt(chunkX, chunkZ);
manager.markerScanner.forEachMarkerSpawner(markerChunk, (block, spawners) -> {
IrisSpawner s = new KList<>(spawners).getRandom();
if (s == null) {
return;
}
manager.entitySpawner.spawn(block, s, true);
});
});
}
void raiseInitialSpawnMarkerFlag(World world, int chunkX, int chunkZ, Runnable onFirstRaise) {
@@ -22,11 +22,12 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.object.IRare;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.plugin.Chunks;
@@ -44,6 +45,8 @@ import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import java.util.List;
import java.util.Optional;
import art.arcane.iris.engine.MarkerSpawnScanner.PreparedMarkerSpawn;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -266,6 +269,10 @@ final class WorldEntitySpawner {
}
void spawnIn(Chunk c, boolean initial) {
if (initial) {
spawnInitially(c);
return;
}
if (manager.getEngine().isClosed()) {
return;
}
@@ -280,17 +287,11 @@ final class WorldEntitySpawner {
}
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
manager.markerScanner.forEachMarkerSpawner(c, (block, spawners) -> {
IrisSpawner s = new KList<>(spawners).getRandom();
if (s == null) {
return;
manager.markerScanner.scanMarkerSpawners(c, false, markers -> {
for (PreparedMarkerSpawn marker : markers) {
spawnPreparedMarker(marker, false);
}
spawn(block, s, false);
J.runRegion(c.getWorld(), c.getX(), c.getZ(), manager.managedTask(
"bukkit_world_manager_marker_spawn_followup",
() -> manager.chunkMaintenance.raiseInitialSpawnMarkerFlag(c.getWorld(), c.getX(), c.getZ(),
() -> spawn(block, s, true))));
prepareInitialSpawn(c, markers);
});
}
@@ -298,26 +299,91 @@ final class WorldEntitySpawner {
return;
}
try {
BiomeEnvironment environment = manager.getEngine().getSurfaceBiomeEnvironment(
(c.getX() << 4) + 8, (c.getZ() << 4) + 8);
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(environment)) {
spawnAmbient(c, initial, environment);
}
} catch (SavedBiomeUnavailableException e) {
return;
}
}
void spawnInitially(Chunk chunk) {
if (manager.getEngine().isClosed() || !isEntitySpawningEnabledForCurrentWorld()) {
return;
}
if (IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) {
manager.markerScanner.scanMarkerSpawners(chunk, true, markers -> prepareInitialSpawn(chunk, markers));
} else if (IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
prepareInitialSpawn(chunk, List.of());
}
}
void prepareInitialSpawn(Chunk chunk, List<PreparedMarkerSpawn> markers) {
Optional<BiomeEnvironment> environment = Optional.empty();
if (IrisSettings.get().getWorld().isAmbientEntitySpawningSystem()) {
try {
environment = Optional.of(manager.getEngine().getSurfaceBiomeEnvironment(
(chunk.getX() << 4) + 8, (chunk.getZ() << 4) + 8));
} catch (SavedBiomeUnavailableException e) {
if (e.isLoading()) {
return;
}
}
}
Optional<BiomeEnvironment> preparedEnvironment = environment;
manager.chunkMaintenance.raiseInitialSpawnMarkerFlag(chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> {
for (PreparedMarkerSpawn marker : markers) {
spawnPreparedMarker(marker, true);
}
if (preparedEnvironment.isPresent()) {
J.runRegion(chunk.getWorld(), chunk.getX(), chunk.getZ(),
manager.managedTask("bukkit_world_manager_initial_spawn_followup", () -> {
if (!chunk.getWorld().isChunkLoaded(chunk.getX(), chunk.getZ())) {
return;
}
BiomeEnvironment selected = preparedEnvironment.get();
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(selected)) {
spawnAmbient(chunk, true, selected);
}
}), RNG.r.i(5, 200));
}
});
}
private void spawnPreparedMarker(PreparedMarkerSpawn marker, boolean initial) {
IrisSpawner spawner = new KList<>(marker.spawners()).getRandom();
if (spawner == null || manager.getEngine().isClosed()) {
return;
}
try (BiomeEnvironment.Scope ignored = manager.getEngine().openBiomeEnvironmentScope(marker.environment())) {
spawnMarker(marker.position(), spawner, initial);
}
}
private void spawnAmbient(Chunk c, boolean initial, BiomeEnvironment environment) {
//@builder
// Excluded spawners cannot spawn anything on this Minecraft version, so they never enter the selection pool.
Predicate<IrisSpawner> filter = i -> !i.isCompatExcluded() && i.canSpawn(manager.getEngine(), c.getX(), c.getZ());
ChunkCounter counter = new ChunkCounter(c.getEntities());
IrisBiome biome = EngineBukkitOps.getSurfaceBiome(manager.getEngine(), c);
IrisEntitySpawn v = spawnRandomly(Stream.concat(manager.getData().getSpawnerLoader()
.loadAll(manager.getDimension().getEntitySpawners())
IrisBiome biome = environment.biome();
IrisEntitySpawn v = spawnRandomly(Stream.concat(environment.data().getSpawnerLoader()
.loadAll(environment.dimension().getEntitySpawners())
.shuffleCopy(RNG.r)
.stream()
.filter(filter)
.filter((i) -> i.isValid(biome)),
Stream.concat(manager.getData()
Stream.concat(environment.data()
.getSpawnerLoader()
.loadAll(manager.getEngine().getRegion(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners())
.loadAll(environment.region().getEntitySpawners())
.shuffleCopy(RNG.r)
.stream()
.filter(filter),
manager.getData().getSpawnerLoader()
.loadAll(manager.getEngine().getSurfaceBiome(PowerOfTwoCoordinates.chunkToBlock(c.getX()), PowerOfTwoCoordinates.chunkToBlock(c.getZ())).getEntitySpawners())
environment.data().getSpawnerLoader()
.loadAll(environment.biome().getEntitySpawners())
.shuffleCopy(RNG.r)
.stream()
.filter(filter)))
@@ -353,15 +419,7 @@ final class WorldEntitySpawner {
}
}
void spawn(IrisPosition block, IrisSpawner spawner, boolean initial) {
if (manager.getEngine().isClosed()) {
return;
}
if (spawner == null) {
return;
}
private void spawnMarker(IrisPosition block, IrisSpawner spawner, boolean initial) {
KList<IrisEntitySpawn> s = initial ? spawner.getInitialSpawns() : spawner.getSpawns();
if (s.isEmpty()) {
return;
@@ -0,0 +1,26 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import java.util.Objects;
public record BiomeEnvironment(long activationId, IrisBiome biome, IrisRegion region,
IrisDimension dimension, IrisData data) {
public BiomeEnvironment {
if (activationId < 0) {
throw new IllegalArgumentException("Biome activation cannot be negative.");
}
Objects.requireNonNull(biome, "biome");
Objects.requireNonNull(region, "region");
Objects.requireNonNull(dimension, "dimension");
Objects.requireNonNull(data, "data");
}
public interface Scope extends AutoCloseable {
@Override
void close();
}
}
@@ -93,6 +93,19 @@ import java.util.stream.Collectors;
public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, Hotloadable {
IrisComplex getComplex();
default BiomeEnvironment getBiomeEnvironment(int x, int y, int z) {
return new BiomeEnvironment(0L, getBiome(x, y, z), getRegion(x, y, z), getDimension(), getData());
}
default BiomeEnvironment getSurfaceBiomeEnvironment(int x, int z) {
return new BiomeEnvironment(0L, getSurfaceBiome(x, z), getRegion(x, z), getDimension(), getData());
}
default BiomeEnvironment.Scope openBiomeEnvironmentScope(BiomeEnvironment environment) {
return () -> {};
}
default @Nullable UpperDimensionContext getUpperContext() {
return null;
}
@@ -22,7 +22,7 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisEffect;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
@@ -35,6 +35,7 @@ import org.bukkit.entity.Player;
public class EnginePlayer {
private final Engine engine;
private final Player player;
private BiomeEnvironment environment;
private IrisBiome biome;
private IrisRegion region;
private Location lastLocation;
@@ -56,6 +57,12 @@ public class EnginePlayer {
return;
}
try (BiomeEnvironment.Scope ignored = engine.openBiomeEnvironmentScope(environment)) {
applyEffects();
}
}
private void applyEffects() {
if (region != null) {
for (IrisEffect effect : region.getEffects()) {
try {
@@ -92,15 +99,22 @@ public class EnginePlayer {
boolean sampled = lastLocation != null;
double distanceSquared = sampled ? current.distanceSquared(lastLocation) : 0D;
if (needsSample(sampled, ticksSinceLastSample(), distanceSquared)) {
BiomeEnvironment sampledEnvironment = engine.getBiomeEnvironment(current.getBlockX(),
current.getBlockY() - engine.getWorld().minHeight(), current.getBlockZ());
environment = sampledEnvironment;
biome = sampledEnvironment.biome();
region = sampledEnvironment.region();
lastLocation = current;
lastSample = M.ms();
biome = EngineBukkitOps.getBiome(engine, current);
region = EngineBukkitOps.getRegion(engine, current);
}
return false;
} catch (SavedBiomeUnavailableException e) {
environment = null;
biome = null;
region = null;
lastLocation = null;
} catch (Throwable e) {
IrisLogging.reportError(e);
}
return true;
}
@@ -0,0 +1,86 @@
package art.arcane.iris.engine.history;
import java.util.Objects;
import java.util.function.IntBinaryOperator;
public final class FloatingBiomeOverlay {
private final int height;
private final int quartHeight;
private final Identity[] volume;
private final Surface[] surfaces = new Surface[256];
public FloatingBiomeOverlay(int height) {
if (height <= 0 || height > SavedBiomeChunk.MAXIMUM_HEIGHT) {
throw new IllegalArgumentException("Invalid floating biome height: " + height);
}
this.height = height;
quartHeight = (height + 3) / 4;
volume = new Identity[16 * quartHeight];
}
public void record(int localX, int y, int localZ, Identity identity) {
requirePosition(localX, y, localZ);
Objects.requireNonNull(identity, "identity");
if ((localX & 3) == 0 && (localZ & 3) == 0 && (y & 3) == 0) {
volume[index(localX, y, localZ)] = identity;
}
int column = localX * 16 + localZ;
Surface previous = surfaces[column];
if (previous == null || y >= previous.y()) {
surfaces[column] = new Surface(y, identity);
}
}
public Identity volumeAt(int localX, int y, int localZ) {
requirePosition(localX, y, localZ);
return volume[index(localX, y, localZ)];
}
public void retainHighestSurfaces(IntBinaryOperator highestSolid) {
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int column = localX * 16 + localZ;
Surface surface = surfaces[column];
if (surface != null && highestSolid.applyAsInt(localX, localZ) != surface.y()) {
surfaces[column] = null;
}
}
}
}
public Identity surfaceAt(int localX, int localZ) {
requirePosition(localX, 0, localZ);
Surface surface = surfaces[localX * 16 + localZ];
return surface == null ? null : surface.identity();
}
public int surfaceYAt(int localX, int localZ) {
requirePosition(localX, 0, localZ);
Surface surface = surfaces[localX * 16 + localZ];
return surface == null ? -1 : surface.y();
}
public int height() {
return height;
}
private int index(int localX, int y, int localZ) {
return ((localX >> 2) * 4 + (localZ >> 2)) * quartHeight + (y >> 2);
}
private void requirePosition(int localX, int y, int localZ) {
if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16 || y < 0 || y >= height) {
throw new IllegalArgumentException("Floating biome position is outside its generation chunk.");
}
}
public record Identity(String biomeKey, String regionKey) {
public Identity {
Objects.requireNonNull(biomeKey, "biomeKey");
Objects.requireNonNull(regionKey, "regionKey");
}
}
private record Surface(int y, Identity identity) {
}
}
@@ -51,6 +51,27 @@ public final class GenerationEpochContractFactory {
String dimensionKey,
String dimensionTypeKey,
int dimensionTypeFingerprintSchema
) {
return createContract(dimension, dimensionKey, dimensionTypeKey,
dimensionTypeFingerprintSchema, GenerationPackFingerprint.CURRENT_VERSION);
}
public static GenerationEpoch.DimensionContract createForEpoch(
IrisDimension dimension,
String dimensionTypeKey,
GenerationEpoch epoch
) {
GenerationEpoch recorded = Objects.requireNonNull(epoch, "epoch");
return createContract(dimension, dimension.getLoadKey(), dimensionTypeKey,
recorded.dimensionContract().dimensionTypeFingerprintSchema(), recorded.packFingerprintVersion());
}
private static GenerationEpoch.DimensionContract createContract(
IrisDimension dimension,
String dimensionKey,
String dimensionTypeKey,
int dimensionTypeFingerprintSchema,
int packFingerprintVersion
) {
requireSupportedDimensionTypeFingerprintSchema(dimensionTypeFingerprintSchema);
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
@@ -83,7 +104,7 @@ public final class GenerationEpochContractFactory {
upperDimensionKey,
upperTerrainEnabled ? requiredDimension.getUpperDimensionGap() : 0,
upperTerrainEnabled
? requireUpperTerrainPackFingerprint(requiredDimension, upperDimensionKey)
? requireUpperTerrainPackFingerprint(requiredDimension, upperDimensionKey, packFingerprintVersion)
: NO_UPPER_TERRAIN_FINGERPRINT,
dimensionTypeFingerprintSchema,
fingerprintDimensionType(dimensionType, dimensionTypeFingerprintSchema)
@@ -151,7 +172,8 @@ public final class GenerationEpochContractFactory {
private static String requireUpperTerrainPackFingerprint(
IrisDimension dimension,
String upperDimensionKey
String upperDimensionKey,
int packFingerprintVersion
) {
IrisData data = Objects.requireNonNull(
dimension.getLoader(),
@@ -167,7 +189,7 @@ public final class GenerationEpochContractFactory {
try {
return GenerationPackFingerprint.compute(
data.getDataFolder().toPath(),
GenerationPackFingerprint.CURRENT_VERSION
packFingerprintVersion
);
} catch (IOException failure) {
throw new IllegalStateException("Unable to fingerprint the immutable upper terrain pack.", failure);
@@ -36,6 +36,7 @@ public final class GenerationHistory {
private final GenerationBoundaryStore boundaries;
private final TerrainBoundarySignatureStore terrainSignatures;
private final GenerationSemanticIndex semantics;
private final SavedBiomeStore savedBiomes;
private final GenerationKernelRegistry kernels;
private final GenerationAdmission admission;
private final Map<Long, GenerationBoundary> boundaryCache;
@@ -56,12 +57,17 @@ public final class GenerationHistory {
this.boundaries = new GenerationBoundaryStore(paths.dimensionRoot());
this.terrainSignatures = new TerrainBoundarySignatureStore(paths.dimensionRoot());
this.semantics = GenerationSemanticIndex.loadRequired(paths.dimensionRoot());
this.savedBiomes = SavedBiomeStore.open(paths.dimensionRoot());
this.admission = new GenerationAdmission(paths.dimensionRoot());
this.boundaryCache = boundedCache(MAXIMUM_CACHED_BOUNDARIES);
this.terrainSignatureCache = boundedCache(MAXIMUM_CACHED_TERRAIN_SIGNATURES);
validateReferencedState();
}
public SavedBiomeStore savedBiomes() {
return savedBiomes;
}
public static GenerationHistory create(
Path dimensionRoot,
Path packSource,
@@ -507,7 +513,6 @@ public final class GenerationHistory {
promoteSavedBoundary();
}
}
packs.releaseArchivedPacks(store.manifest());
}
}
}
@@ -530,6 +535,7 @@ public final class GenerationHistory {
return SavedTerrainChunk.hasTerrain(SavedTerrainChunk.readStatus(paths.dimensionRoot(), chunkX, chunkZ));
});
semantics.discardUnstoredClaims(inventory, selected);
savedBiomes.discardUnstoredClaims(inventory, selected);
ownership.discardUnstoredClaims(inventory, selected);
return inventory;
}
@@ -23,6 +23,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final IrisEngine engine;
private final GenerationHistory history;
private final SavedBiomeRuntime biomes;
private final GenerationBoundarySignatureSampler signatureSampler;
private final ActivationRuntimeFactory runtimeFactory;
private final LinkedHashMap<Long, RuntimeCacheEntry> bindings;
@@ -54,6 +55,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
this.inactive = stateLock.newCondition();
this.operationDepth = ThreadLocal.withInitial(() -> 0);
this.scopedRoute = new ThreadLocal<>();
this.biomes = new SavedBiomeRuntime(engine, history);
GenerationActivation active = history.activeActivation();
GenerationEpoch epoch = requireEpoch(active);
@@ -81,6 +83,10 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
}
}
public SavedBiomeRuntime biomes() {
return biomes;
}
public static GenerationHistoryRuntimeRouter attach(
IrisEngine engine,
GenerationHistory history,
@@ -297,6 +303,14 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
route.naturalTerrain = captured;
}
public void recordFloatingBiomes(int chunkX, int chunkZ, FloatingBiomeOverlay overlay) {
RuntimeRoute route = scopedRoute.get();
if (route == null || route.chunkX() != chunkX || route.chunkZ() != chunkZ) {
throw new IllegalStateException("Floating biomes must be captured inside their chunk generation route.");
}
route.floatingBiomes = overlay;
}
public void preloadActiveRuntimes() throws IOException {
enterOperation();
try {
@@ -387,6 +401,11 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
} catch (Throwable detachFailure) {
failure = detachFailure;
}
try {
biomes.close();
} catch (Throwable biomeFailure) {
failure = appendFailure(failure, biomeFailure);
}
try {
retireBindings(retired);
} catch (Throwable retirementFailure) {
@@ -620,18 +639,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
if (failure != null) {
throw new IllegalStateException("Failed to retire cached generation runtimes.", failure);
}
if (!retired.isEmpty()) {
stateLock.lock();
try {
if (bindings.size() == 1 && retiringBindings.isEmpty()) {
new GenerationPackRepository(history.paths().dimensionRoot()).releaseArchivedPacks(history.manifest());
}
} catch (IOException cleanupFailure) {
throw new IllegalStateException("Unable to release archived generation packs after runtime retirement.", cleanupFailure);
} finally {
stateLock.unlock();
}
}
}
private void requireBindingContract(
@@ -897,7 +905,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
+ activation.activationId() + " pack " + expectedPack + ".");
}
binding.target().getData().bindGenerationRegistryContract(epoch.registryContract());
requireDimensionContract(binding.target().getDimension(), epoch.dimensionContract(), actualPack);
requireDimensionContract(binding.target().getDimension(), epoch, actualPack);
}
@Override
@@ -917,7 +925,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
throw new IOException("Immutable generation pack does not contain dimension '"
+ epoch.dimensionContract().dimensionKey() + "': " + packRoot);
}
requireDimensionContract(dimension, epoch.dimensionContract(), packRoot);
requireDimensionContract(dimension, epoch, packRoot);
TransitionGenerationPlan plan = activation.isInitial()
? null
: history.transitionPlan(activation.activationId());
@@ -947,15 +955,16 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private static void requireDimensionContract(
IrisDimension dimension,
GenerationEpoch.DimensionContract recorded,
GenerationEpoch epoch,
Path packRoot
) throws IOException {
GenerationEpoch.DimensionContract recorded = epoch.dimensionContract();
GenerationEpoch.DimensionContract loaded;
try {
loaded = GenerationEpochContractFactory.create(
loaded = GenerationEpochContractFactory.createForEpoch(
dimension,
dimension.getLoadKey(),
recorded.dimensionTypeKey()
recorded.dimensionTypeKey(),
epoch
);
} catch (RuntimeException failure) {
throw new IOException("Unable to validate immutable generation dimension at " + packRoot + ".", failure);
@@ -976,6 +985,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
private final GenerationTransitionGate.Participation participation;
private int activeScopes;
private volatile SavedTerrainChunk naturalTerrain;
private volatile FloatingBiomeOverlay floatingBiomes;
private boolean closed;
private RuntimeRoute(
@@ -1044,6 +1054,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
stage,
caveSpace
);
router.biomes.capture(SavedBiomeCapture.capture(router.engine, stage, router.biomes, floatingBiomes));
return router.history.claimGeneratedSemantics(stage, semantics);
}
@@ -23,7 +23,7 @@ import java.util.List;
import java.util.Objects;
public final class GenerationPackFingerprint {
public static final int CURRENT_VERSION = 1;
public static final int CURRENT_VERSION = 2;
private static final int BUFFER_BYTES = 64 * 1_024;
private GenerationPackFingerprint() {
@@ -31,23 +31,31 @@ public final class GenerationPackFingerprint {
public static String compute(Path packRoot, int version) throws IOException {
requireSupported(version);
return computeVersionOne(packRoot);
return switch (version) {
case 1 -> computeVersionOne(packRoot);
case 2 -> computeTree(packRoot, true);
default -> throw new IOException("Unsupported Iris generation pack fingerprint version " + version + ".");
};
}
public static void requireSupported(int version) throws IOException {
if (version != 1) {
if (version != 1 && version != 2) {
throw new IOException("Unsupported Iris generation pack fingerprint version " + version + ".");
}
}
private static String computeVersionOne(Path packRoot) throws IOException {
return computeTree(packRoot, false);
}
private static String computeTree(Path packRoot, boolean ignoreFinderMetadata) throws IOException {
Path root = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(root)) {
throw new IOException("Generation pack fingerprint root is missing or unsafe: " + root);
}
Path realRoot = root.toRealPath(LinkOption.NOFOLLOW_LINKS);
List<FingerprintEntry> entries = new ArrayList<>();
collectTree(realRoot, entries);
collectTree(realRoot, entries, ignoreFinderMetadata);
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
MessageDigest digest = sha256();
byte[] buffer = new byte[BUFFER_BYTES];
@@ -75,7 +83,11 @@ public final class GenerationPackFingerprint {
return HexFormat.of().formatHex(digest.digest());
}
private static void collectTree(Path root, List<FingerprintEntry> entries) throws IOException {
private static void collectTree(
Path root,
List<FingerprintEntry> entries,
boolean ignoreFinderMetadata
) throws IOException {
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(
@@ -98,7 +110,8 @@ public final class GenerationPackFingerprint {
Path relativePath = root.relativize(file);
String fileName = file.getFileName().toString();
if ((relativePath.getNameCount() == 1 && PackDirectoryResolver.isHiddenName(fileName))
|| fileName.endsWith(".code-workspace")) {
|| fileName.endsWith(".code-workspace")
|| (ignoreFinderMetadata && fileName.equals(".DS_Store"))) {
return FileVisitResult.CONTINUE;
}
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
@@ -96,29 +96,6 @@ public final class GenerationPackRepository {
}
}
public synchronized void releaseArchivedPacks(GenerationManifest manifest) throws IOException {
GenerationManifest retained = Objects.requireNonNull(manifest, "manifest");
String activeEpochId = retained.activeEpoch().epochId();
String pendingEpochId = retained.pendingEpoch().map(GenerationEpoch::epochId).orElse(null);
for (GenerationEpoch epoch : retained.epochs()) {
String epochId = epoch.epochId();
if (epochId.equals(activeEpochId) || epochId.equals(pendingEpochId)) {
continue;
}
try (GenerationPublicationLock ignored = GenerationPublicationLock.acquire(
epochsRoot, ".epoch-" + epochId + ".lock")) {
validateExistingAncestors(epochId);
Path pack = packRoot(epochId);
if (!Files.exists(pack, LinkOption.NOFOLLOW_LINKS)) {
continue;
}
requireSafeDirectory(pack, "Generation epoch pack is unsafe");
AtomicDirectoryPublisher.deleteTree(pack);
forceDirectory(epochRoot(epochId));
}
}
}
public Path requireExactPack(
String epochId,
String packFingerprint,
@@ -0,0 +1,69 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.framework.Engine;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public record NativeBiomeSpawnSelection(Mode mode, String derivativeKey) {
private static final NativeBiomeSpawnSelection CURRENT = new NativeBiomeSpawnSelection(Mode.CURRENT, "");
private static final NativeBiomeSpawnSelection NONE = new NativeBiomeSpawnSelection(Mode.NONE, "");
private static final NativeBiomeSpawnSelection LOADING = new NativeBiomeSpawnSelection(Mode.LOADING, "");
public NativeBiomeSpawnSelection {
Objects.requireNonNull(mode, "mode");
Objects.requireNonNull(derivativeKey, "derivativeKey");
if (mode == Mode.RETAINED) {
derivativeKey = ChunkGenerationSemantics.requireResourceKey(derivativeKey);
} else if (!derivativeKey.isEmpty()) {
throw new IllegalArgumentException("Only retained native spawn selections have a derivative key");
}
}
public static NativeBiomeSpawnSelection at(Engine engine, int blockX, int worldY, int blockZ, String physicalBiomeKey) {
if (!(engine instanceof IrisEngine irisEngine) || irisEngine.hasGenerationRuntimeScope()) {
return CURRENT;
}
GenerationHistoryRuntimeRouter router = irisEngine.getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return CURRENT;
}
try {
return router.biomes().nativeSpawnSelection(blockX, worldY, blockZ, physicalBiomeKey);
} catch (SavedBiomeUnavailableException unavailable) {
return unavailable.isLoading() ? LOADING : NONE;
}
}
static Map<String, String> retainedDerivatives(IrisData data) {
Map<String, String> derivatives = new LinkedHashMap<>();
for (String key : data.getDimensionLoader().getPossibleKeys()) {
IrisDimension dimension = data.getDimensionLoader().load(key);
if (dimension == null) {
continue;
}
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
if (biome == null || !biome.isCustom()) {
continue;
}
for (IrisBiomeCustom custom : biome.getCustomDerivitives()) {
derivatives.putIfAbsent(data.customBiomeResourceKey(dimension, custom), biome.getVanillaDerivativeKey());
}
}
}
return Map.copyOf(derivatives);
}
public enum Mode {
CURRENT,
RETAINED,
NONE,
LOADING
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public final class SavedBiomeCapture {
private SavedBiomeCapture() {
}
public static SavedBiomeChunk capture(Engine engine, GenerationHistory.GenerationStage stage, SavedBiomeRuntime saved, FloatingBiomeOverlay floating) throws IOException {
int minimumY = engine.getMinHeight();
int height = engine.getHeight();
if (floating != null && floating.height() != height) {
throw new IllegalArgumentException("Floating biome overlay has a different generation height.");
}
int startX = Math.multiplyExact(stage.chunkX(), 16);
int startZ = Math.multiplyExact(stage.chunkZ(), 16);
long activationId = stage.activation().activationId();
SavedBiomeChunk.Builder result = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(
stage.chunkX(), stage.chunkZ(), activationId, minimumY, height));
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan();
Map<Long, Optional<SavedBiomeChunk>> historical = new HashMap<>();
Map<Integer, List<SavedBiomeChunk.Span>> vertical = new HashMap<>(16);
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int blockX = startX + localX;
int blockZ = startZ + localZ;
IrisRegion region = engine.getRegion(blockX, blockZ);
IrisBiome surface = engine.getSurfaceBiome(blockX, blockZ);
int surfaceY = minimumY + engine.getHeight(blockX, blockZ);
SavedBiomeChunk.Cell surfaceCell = cell(activationId, surface, region);
if (floating != null) {
FloatingBiomeOverlay.Identity floatingSurface = floating.surfaceAt(localX, localZ);
if (floatingSurface != null) {
surfaceCell = overlay(surfaceCell, floatingSurface);
surfaceY = minimumY + floating.surfaceYAt(localX, localZ);
}
}
if (transition != null) {
surfaceCell = historicalCell(new Sample(blockX, surfaceY, blockZ, true), surfaceCell, transition, saved, historical);
}
int quartX = localX & ~3;
int quartZ = localZ & ~3;
int quartKey = quartX * 16 + quartZ;
List<SavedBiomeChunk.Span> spans = vertical.get(quartKey);
if (spans == null) {
spans = captureVertical(new Column(engine, activationId, startX + quartX, startZ + quartZ), transition, saved, historical, floating);
vertical.put(quartKey, spans);
}
SavedBiomeChunk.Cell caveBase = cell(activationId, engine.getCaveBiome(blockX, blockZ), region);
result.column(localX, localZ, new SavedBiomeChunk.Column(surfaceCell, caveBase, spans));
}
}
return result.build();
}
private static List<SavedBiomeChunk.Span> captureVertical(Column column, TransitionGenerationPlan transition,
SavedBiomeRuntime saved, Map<Long, Optional<SavedBiomeChunk>> historical, FloatingBiomeOverlay floating) throws IOException {
Engine engine = column.engine();
int minimumY = engine.getMinHeight();
int maximumY = minimumY + engine.getHeight();
List<SavedBiomeChunk.Span> spans = new ArrayList<>();
SavedBiomeChunk.Cell previous = null;
int startY = minimumY;
for (int worldY = minimumY; worldY < maximumY; worldY += 4) {
IrisBiome biome = engine.getBiomeOrMantle(column.blockX(), worldY - minimumY, column.blockZ());
IrisRegion region = engine.getRegion(column.blockX(), worldY - minimumY, column.blockZ());
SavedBiomeChunk.Cell current = cell(column.activationId(), biome, region);
if (floating != null) {
current = overlay(current, floating.volumeAt(column.blockX() & 15, worldY - minimumY, column.blockZ() & 15));
}
if (transition != null) {
current = historicalCell(new Sample(column.blockX(), worldY, column.blockZ(), false), current, transition, saved, historical);
}
if (previous != null && !previous.equals(current)) {
spans.add(new SavedBiomeChunk.Span(startY, worldY, previous));
startY = worldY;
}
previous = current;
}
spans.add(new SavedBiomeChunk.Span(startY, maximumY, Objects.requireNonNull(previous, "biome")));
return List.copyOf(spans);
}
private static SavedBiomeChunk.Cell historicalCell(Sample sample, SavedBiomeChunk.Cell current,
TransitionGenerationPlan transition, SavedBiomeRuntime saved,
Map<Long, Optional<SavedBiomeChunk>> historical) throws IOException {
TransitionGenerationPlan.TerrainSample terrain = transition.terrainSampleAt(sample.x(), sample.z());
if (transition.historicalPhysicalBiomeKeyAt(sample.x(), sample.y(), sample.z(), terrain).isEmpty()) {
return current;
}
TerrainBoundarySignature signature = Objects.requireNonNull(terrain.nearestSignature(), "historical biome boundary");
int chunkX = Math.floorDiv(signature.blockX(), 16);
int chunkZ = Math.floorDiv(signature.blockZ(), 16);
long key = ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL);
Optional<SavedBiomeChunk> recorded = historical.get(key);
if (recorded == null) {
try {
recorded = saved.snapshot(chunkX, chunkZ);
} catch (SavedBiomeUnavailableException unavailable) {
recorded = Optional.empty();
}
historical.put(key, recorded);
}
if (recorded.isEmpty()) {
return SavedBiomeChunk.Cell.unresolved(saved.activationAt(chunkX, chunkZ));
}
SavedBiomeChunk source = recorded.get();
int localX = Math.floorMod(signature.blockX(), 16);
int localZ = Math.floorMod(signature.blockZ(), 16);
return sample.surface() ? source.surfaceAt(localX, localZ)
: source.biomeAt(localX, Math.max(source.header().minimumY(),
Math.min(sample.y(), source.header().maximumYExclusive() - 1)), localZ);
}
private static SavedBiomeChunk.Cell overlay(SavedBiomeChunk.Cell current, FloatingBiomeOverlay.Identity floating) {
return floating == null ? current
: new SavedBiomeChunk.Cell(current.activationId(), floating.biomeKey(), floating.regionKey());
}
private static SavedBiomeChunk.Cell cell(long activationId, IrisBiome biome, IrisRegion region) {
return new SavedBiomeChunk.Cell(activationId, Objects.requireNonNull(biome, "biome").getLoadKey(),
Objects.requireNonNull(region, "region").getLoadKey());
}
private record Column(Engine engine, long activationId, int blockX, int blockZ) {
}
private record Sample(int x, int y, int z, boolean surface) {
}
}
@@ -0,0 +1,226 @@
package art.arcane.iris.engine.history;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
public final class SavedBiomeChunk {
public static final int MAXIMUM_HEIGHT = 65_536;
public static final long MAXIMUM_ESTIMATED_BYTES = 64L * 1024L * 1024L;
private static final int COLUMN_COUNT = 256;
private final Header header;
private final Column[] columns;
private final long estimatedBytes;
private SavedBiomeChunk(Header header, Column[] columns) {
this.header = header;
this.columns = columns.clone();
long bytes = 128L + COLUMN_COUNT * 16L;
for (Column column : this.columns) {
Objects.requireNonNull(column, "Every saved biome column is required");
int expectedY = header.minimumY();
for (Span span : column.vertical()) {
if (span.minimumY() != expectedY) {
throw new IllegalArgumentException("Saved biome spans must cover the column without gaps or overlaps");
}
expectedY = span.maximumYExclusive();
bytes = checkedSize(bytes + cellBytes(span.cell()));
}
if (expectedY != header.maximumYExclusive()) {
throw new IllegalArgumentException("Saved biome spans must cover the entire world height");
}
bytes = checkedSize(bytes + cellBytes(column.surface()) + cellBytes(column.caveBase()));
}
estimatedBytes = bytes;
}
public static Builder builder(Header header) {
return new Builder(Objects.requireNonNull(header, "header"));
}
public Header header() {
return header;
}
public int chunkX() {
return header.chunkX();
}
public int chunkZ() {
return header.chunkZ();
}
public long activationId() {
return header.activationId();
}
public Column column(int localX, int localZ) {
return columns[columnIndex(localX, localZ)];
}
public Cell surfaceAt(int localX, int localZ) {
return column(localX, localZ).surface();
}
public Cell caveBaseAt(int localX, int localZ) {
return column(localX, localZ).caveBase();
}
public Cell biomeAt(int localX, int worldY, int localZ) {
if (worldY < header.minimumY() || worldY >= header.maximumYExclusive()) {
throw new IndexOutOfBoundsException("Biome height is outside the saved world height");
}
List<Span> spans = column(localX, localZ).vertical();
int low = 0;
int high = spans.size() - 1;
while (low < high) {
int middle = (low + high) >>> 1;
if (worldY >= spans.get(middle).maximumYExclusive()) {
low = middle + 1;
} else {
high = middle;
}
}
return spans.get(low).cell();
}
long estimatedBytes() {
return estimatedBytes;
}
@Override
public boolean equals(Object compared) {
return this == compared || compared instanceof SavedBiomeChunk other
&& header.equals(other.header) && Arrays.equals(columns, other.columns);
}
@Override
public int hashCode() {
return 31 * header.hashCode() + Arrays.hashCode(columns);
}
private static long checkedSize(long bytes) {
if (bytes > MAXIMUM_ESTIMATED_BYTES) {
throw new IllegalArgumentException("Saved biome chunk exceeds its decoded footprint limit");
}
return bytes;
}
private static long cellBytes(Cell cell) {
return cell.isResolved()
? 160L + cell.biomeKey().length() * 2L + cell.regionKey().length() * 2L
: 160L;
}
private static int columnIndex(int localX, int localZ) {
if (localX < 0 || localX >= 16 || localZ < 0 || localZ >= 16) {
throw new IndexOutOfBoundsException("Saved biome local coordinates must be between 0 and 15");
}
return localZ * 16 + localX;
}
public record Header(int chunkX, int chunkZ, long activationId, int minimumY, int height) {
public Header {
if (activationId <= 0L || height <= 0 || height > MAXIMUM_HEIGHT
|| (long) minimumY + height > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Invalid saved biome chunk header");
}
}
public int maximumYExclusive() {
return minimumY + height;
}
}
public enum Resolution {
RESOLVED,
UNRESOLVED
}
public record Cell(long activationId, String biomeKey, String regionKey, Resolution resolution) {
public Cell {
if (activationId <= 0L) {
throw new IllegalArgumentException("Saved biome activation must be positive");
}
Objects.requireNonNull(resolution, "resolution");
if (resolution == Resolution.RESOLVED) {
if (biomeKey == null || regionKey == null) {
throw new IllegalArgumentException("Resolved saved biomes require both resource keys");
}
biomeKey = ChunkGenerationSemantics.requireResourceKey(biomeKey);
regionKey = ChunkGenerationSemantics.requireResourceKey(regionKey);
} else if (biomeKey != null || regionKey != null) {
throw new IllegalArgumentException("Unresolved saved biomes must not contain resource keys");
}
}
public Cell(long activationId, String biomeKey, String regionKey) {
this(activationId, biomeKey, regionKey, Resolution.RESOLVED);
}
public static Cell unresolved(long activationId) {
return new Cell(activationId, null, null, Resolution.UNRESOLVED);
}
public boolean isResolved() {
return resolution == Resolution.RESOLVED;
}
}
public record Span(int minimumY, int maximumYExclusive, Cell cell) {
public Span {
if (minimumY >= maximumYExclusive) {
throw new IllegalArgumentException("Saved biome spans must have positive height");
}
Objects.requireNonNull(cell, "cell");
}
}
public record Column(Cell surface, Cell caveBase, List<Span> vertical) {
public Column {
Objects.requireNonNull(surface, "surface");
Objects.requireNonNull(caveBase, "caveBase");
Objects.requireNonNull(vertical, "vertical");
if (vertical.isEmpty() || vertical.size() > MAXIMUM_HEIGHT) {
throw new IllegalArgumentException("Invalid saved biome column span count");
}
ArrayList<Span> compact = new ArrayList<>(vertical.size());
for (Span span : vertical) {
Objects.requireNonNull(span, "span");
if (!compact.isEmpty()) {
Span previous = compact.getLast();
if (previous.maximumYExclusive() != span.minimumY()) {
throw new IllegalArgumentException("Saved biome spans must be contiguous");
}
if (previous.cell().equals(span.cell())) {
compact.set(compact.size() - 1,
new Span(previous.minimumY(), span.maximumYExclusive(), span.cell()));
continue;
}
}
compact.add(span);
}
vertical = List.copyOf(compact);
}
}
public static final class Builder {
private final Header header;
private final Column[] columns = new Column[COLUMN_COUNT];
private Builder(Header header) {
this.header = header;
}
public Builder column(int localX, int localZ, Column column) {
columns[columnIndex(localX, localZ)] = Objects.requireNonNull(column, "column");
return this;
}
public SavedBiomeChunk build() {
return new SavedBiomeChunk(header, columns);
}
}
}
@@ -0,0 +1,128 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
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.IrisRiverPolicy;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public final class SavedBiomeRecovery {
private SavedBiomeRecovery() {
}
public static Optional<SavedBiomeChunk> recover(Input input) throws IOException {
Objects.requireNonNull(input, "input");
Optional<ChunkGenerationSemantics> recorded = input.history().semantics(input.chunkX(), input.chunkZ());
if (recorded.isEmpty()) {
return Optional.empty();
}
ChunkGenerationSemantics semantics = recorded.get();
if (!semantics.sealed() || semantics.surfaceBiomeKeys().size() != 1
|| semantics.regionKeys().size() != 1) {
return Optional.empty();
}
GenerationActivation activation = input.history().resolveActivation(input.chunkX(), input.chunkZ());
if (activation.activationId() != semantics.activationId()) {
return Optional.empty();
}
String biomeKey = semantics.surfaceBiomeKeys().iterator().next();
String regionKey = semantics.regionKeys().iterator().next();
if (!onlyKey(semantics.caveBiomeKeys(), biomeKey)) {
return Optional.empty();
}
IrisDimension dimension = input.dimension();
GenerationEpoch.DimensionContract contract = input.history().resolveEpoch(input.chunkX(), input.chunkZ())
.dimensionContract();
if (!contract.dimensionKey().equals(dimension.getLoadKey())
|| contract.minHeight() != dimension.getMinHeight()
|| contract.maxHeight() != dimension.getMaxHeight()
|| contract.upperTerrainEnabled() || dimension.hasUpperDimension() || dimension.hasDimensionStack()
|| !dimension.getCarving().isEmpty()
|| !optionalKey(dimension.getFocus(), biomeKey)
|| !optionalKey(dimension.getFocusRegion(), regionKey)
|| !singleChoice(dimension.getRegions(), regionKey)) {
return Optional.empty();
}
IrisRegion region = input.data().getRegionLoader().load(regionKey);
IrisBiome biome = input.data().getBiomeLoader().load(biomeKey);
if (region == null || biome == null
|| !regionKey.equals(region.getLoadKey()) || !biomeKey.equals(biome.getLoadKey())
|| !singleChoice(region.getLandBiomes(), biomeKey)
|| !singleChoice(region.getSeaBiomes(), biomeKey)
|| !singleChoice(region.getShoreBiomes(), biomeKey)
|| !singleChoice(region.getCaveBiomes(), biomeKey)
|| !biome.getChildren().isEmpty() || !biome.getFloatingChildBiomes().isEmpty()
|| !optionalKey(biome.getCarvingBiome(), biomeKey)
|| !sameRiverBiomes(dimension.getRiverPolicy(), biomeKey)
|| !sameRiverBiomes(region.getRiverPolicy(), biomeKey)
|| !sameRiverBiomes(biome.getRiverPolicy(), biomeKey)
|| hasHistoricalColumns(input, activation)) {
return Optional.empty();
}
SavedBiomeChunk.Header header = new SavedBiomeChunk.Header(input.chunkX(), input.chunkZ(),
activation.activationId(), contract.minHeight(), contract.height());
SavedBiomeChunk.Cell cell = new SavedBiomeChunk.Cell(activation.activationId(), biomeKey, regionKey);
SavedBiomeChunk.Column column = new SavedBiomeChunk.Column(cell, cell,
List.of(new SavedBiomeChunk.Span(header.minimumY(), header.maximumYExclusive(), cell)));
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(header);
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
builder.column(localX, localZ, column);
}
}
return Optional.of(builder.build());
}
private static boolean hasHistoricalColumns(Input input, GenerationActivation activation) throws IOException {
if (activation.isInitial()) {
return false;
}
TransitionGenerationPlan transition = input.history().transitionPlan(activation.activationId());
int minimumX = Math.multiplyExact(input.chunkX(), 16);
int minimumZ = Math.multiplyExact(input.chunkZ(), 16);
for (int localZ = 0; localZ < 16; localZ++) {
for (int localX = 0; localX < 16; localX++) {
if (transition.newEpochWeightAt(Math.addExact(minimumX, localX),
Math.addExact(minimumZ, localZ)) != 1D) {
return true;
}
}
}
return false;
}
private static boolean singleChoice(Collection<String> keys, String expected) {
return keys != null && !keys.isEmpty() && onlyKey(keys, expected);
}
private static boolean onlyKey(Collection<String> keys, String expected) {
for (String key : keys) {
if (!expected.equals(key)) {
return false;
}
}
return true;
}
private static boolean optionalKey(String key, String expected) {
return key != null && (key.isEmpty() || expected.equals(key));
}
private static boolean sameRiverBiomes(IrisRiverPolicy policy, String biomeKey) {
return policy == null || onlyKey(policy.getAllBiomeIds(), biomeKey);
}
public record Input(GenerationHistory history, int chunkX, int chunkZ, IrisData data, IrisDimension dimension) {
public Input {
Objects.requireNonNull(history, "history");
Objects.requireNonNull(data, "data");
Objects.requireNonNull(dimension, "dimension");
}
}
}
@@ -0,0 +1,421 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.BiomeEnvironment;
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.spi.IrisLogging;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public final class SavedBiomeRuntime implements AutoCloseable {
private static final int MAXIMUM_QUERIES = 128;
private static final int MAXIMUM_PENDING_QUERIES = 256;
private static final long MAXIMUM_QUERY_BYTES = SavedBiomeChunk.MAXIMUM_ESTIMATED_BYTES;
private final IrisEngine engine;
private final GenerationHistory history;
private final SavedBiomeStore store;
private final Map<String, Definitions> definitions = new ConcurrentHashMap<>();
private final LinkedHashMap<Long, PreparedQuery> queries = new LinkedHashMap<>(32, 0.75F, true);
private final Map<Long, PendingQuery> pending = new HashMap<>();
private long queryBytes;
private final ExecutorService reads = Executors.newFixedThreadPool(2, task -> {
Thread thread = new Thread(task, "Iris Saved Biomes");
thread.setDaemon(true);
return thread;
});
private volatile boolean closed;
SavedBiomeRuntime(IrisEngine engine, GenerationHistory history) throws IOException {
this.engine = Objects.requireNonNull(engine, "engine");
this.history = Objects.requireNonNull(history, "history");
store = history.savedBiomes();
}
public Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, boolean surface) {
return resolve(blockX, worldY, blockZ, surface ? QueryKind.SURFACE : QueryKind.VOLUME);
}
public NativeBiomeSpawnSelection nativeSpawnSelection(int blockX, int worldY, int blockZ, String physicalBiomeKey) {
PreparedQuery prepared = query(Math.floorDiv(blockX, 16), Math.floorDiv(blockZ, 16));
if (prepared.failure() != null) {
throw prepared.failure();
}
if (prepared.chunk().isEmpty()) {
return new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.CURRENT, "");
}
SavedBiomeChunk chunk = prepared.chunk().get();
SavedBiomeChunk.Cell cell = chunk.biomeAt(Math.floorMod(blockX, 16),
Math.max(chunk.header().minimumY(), Math.min(worldY, chunk.header().maximumYExclusive() - 1)),
Math.floorMod(blockZ, 16));
GenerationActivation activation = history.manifest().activation(cell.activationId()).orElseThrow(() ->
new SavedBiomeUnavailableException("Historical biome generation is missing: " + cell.activationId(), false));
Definitions source = definitions.get(activation.epochId());
String derivative = source == null ? null : source.nativeDerivatives().get(physicalBiomeKey);
return derivative == null
? new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.NONE, "")
: new NativeBiomeSpawnSelection(NativeBiomeSpawnSelection.Mode.RETAINED, derivative);
}
public void prepareChunk(int chunkX, int chunkZ) {
PreparedQuery prepared = query(chunkX, chunkZ);
if (prepared.failure() != null) {
throw prepared.failure();
}
}
public Optional<BiomeEnvironment> resolveCaveBase(int blockX, int blockZ) {
return resolve(blockX, 0, blockZ, QueryKind.CAVE_BASE);
}
private Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, QueryKind kind) {
int chunkX = Math.floorDiv(blockX, 16);
int chunkZ = Math.floorDiv(blockZ, 16);
PreparedQuery query = query(chunkX, chunkZ);
if (query.failure() != null) {
throw query.failure();
}
if (query.chunk().isEmpty()) {
return Optional.empty();
}
SavedBiomeChunk chunk = query.chunk().get();
int localX = Math.floorMod(blockX, 16);
int localZ = Math.floorMod(blockZ, 16);
SavedBiomeChunk.Cell cell = switch (kind) {
case SURFACE -> chunk.surfaceAt(localX, localZ);
case CAVE_BASE -> chunk.caveBaseAt(localX, localZ);
case VOLUME -> chunk.biomeAt(localX, Math.max(chunk.header().minimumY(),
Math.min(worldY, chunk.header().maximumYExclusive() - 1)), localZ);
};
if (!cell.isResolved()) {
throw new SavedBiomeUnavailableException("This position has no recorded Iris biome identity for generation "
+ cell.activationId() + ". The saved terrain and Minecraft biome remain available, but Iris biome rules cannot run.", false);
}
SavedBiomeUnavailableException unavailable = query.cellFailures().get(cell);
if (unavailable != null) {
throw unavailable;
}
BiomeEnvironment environment = query.environments().get(cell);
if (environment == null) {
throw new SavedBiomeUnavailableException("Historical biome definitions are unavailable for generation " + cell.activationId() + ".", false);
}
return Optional.of(environment);
}
public Optional<SavedBiomeChunk> snapshot(int chunkX, int chunkZ) throws IOException {
requireOpen();
Optional<SavedBiomeChunk> saved = store.get(chunkX, chunkZ);
if (saved.isPresent()) {
return saved;
}
if (history.isActiveUnowned(chunkX, chunkZ) && history.semantics(chunkX, chunkZ).isEmpty()) {
return Optional.empty();
}
long activationId = history.resolveActivation(chunkX, chunkZ).activationId();
Definitions source = definitions(activationId);
Optional<SavedBiomeChunk> recovered = SavedBiomeRecovery.recover(new SavedBiomeRecovery.Input(
history, chunkX, chunkZ, source.data(), source.dimension()));
if (recovered.isPresent()) {
store.claimAndPersist(recovered.get());
return recovered;
}
throw new SavedBiomeUnavailableException("This chunk has no exact saved Iris biome assignment for generation "
+ activationId + ". Its terrain is preserved; historical biome rules cannot be recovered safely.", false);
}
public long activationAt(int chunkX, int chunkZ) {
return history.resolveActivation(chunkX, chunkZ).activationId();
}
public void capture(SavedBiomeChunk chunk) throws IOException {
requireOpen();
store.claimAndPersist(chunk);
synchronized (queries) {
long key = key(chunk.chunkX(), chunk.chunkZ());
PreparedQuery removed = queries.remove(key);
if (removed != null) {
queryBytes -= removed.estimatedBytes();
}
PendingQuery loading = pending.get(key);
if (loading != null) {
loading.invalidated = true;
}
}
}
@Override
public synchronized void close() {
synchronized (queries) {
closed = true;
reads.shutdown();
}
boolean interrupted = false;
Throwable failure = null;
try {
while (true) {
try {
if (reads.awaitTermination(1L, TimeUnit.DAYS)) {
break;
}
} catch (InterruptedException interruption) {
interrupted = true;
}
}
synchronized (queries) {
queries.clear();
pending.clear();
queryBytes = 0L;
}
for (Definitions source : definitions.values()) {
try {
try {
source.data().unregisterEngine(engine);
} finally {
source.data().close();
}
} catch (Throwable closeFailure) {
if (failure == null) {
failure = closeFailure;
} else {
failure.addSuppressed(closeFailure);
}
}
}
definitions.clear();
if (failure != null) {
throw new IllegalStateException("Unable to close saved biome definitions", failure);
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
int cachedQueryCount() {
synchronized (queries) {
return queries.size();
}
}
long cachedQueryBytes() {
synchronized (queries) {
return queryBytes;
}
}
int pendingQueryCount() {
synchronized (queries) {
return pending.size();
}
}
private PreparedQuery query(int chunkX, int chunkZ) {
long key = key(chunkX, chunkZ);
synchronized (queries) {
requireOpen();
PreparedQuery cached = queries.get(key);
if (cached != null) {
return cached;
}
if (!pending.containsKey(key) && pending.size() < MAXIMUM_PENDING_QUERIES) {
PendingQuery loading = new PendingQuery();
pending.put(key, loading);
reads.execute(() -> readQuery(chunkX, chunkZ, loading));
}
}
throw new SavedBiomeUnavailableException("Saved biome information is loading at chunk "
+ chunkX + "," + chunkZ + ". Try again shortly.", true);
}
private void readQuery(int chunkX, int chunkZ, PendingQuery loading) {
long key = key(chunkX, chunkZ);
PreparedQuery prepared = null;
try {
if (!closed) {
Optional<SavedBiomeChunk> saved = snapshot(chunkX, chunkZ);
prepared = saved.isEmpty()
? new PreparedQuery(Optional.empty(), Map.of(), Map.of(), null, 256L)
: prepareDefinitions(saved.get());
}
} catch (SavedBiomeUnavailableException unavailable) {
if (!closed) {
IrisLogging.warnOnce("saved-biome:" + history.paths().dimensionRoot() + ":" + unavailable.getMessage(),
unavailable.getMessage());
}
prepared = new PreparedQuery(Optional.empty(), Map.of(), Map.of(), unavailable, 512L);
} catch (IOException | RuntimeException failure) {
if (!closed && IrisLogging.warnOnce("saved-biome:" + history.paths().dimensionRoot() + ":" + failure.getMessage(),
"Failed to read saved biome information for chunk " + chunkX + "," + chunkZ + ".")) {
IrisLogging.reportError(failure);
}
prepared = new PreparedQuery(Optional.empty(), Map.of(), Map.of(), new SavedBiomeUnavailableException(
"Unable to read the saved biome at chunk " + chunkX + "," + chunkZ + ".", failure), 512L);
} finally {
synchronized (queries) {
pending.remove(key, loading);
if (!closed && !loading.invalidated && prepared != null) {
cacheQuery(key, prepared);
}
}
}
}
private void cacheQuery(long key, PreparedQuery prepared) {
PreparedQuery previous = queries.put(key, prepared);
if (previous != null) {
queryBytes -= previous.estimatedBytes();
}
queryBytes += prepared.estimatedBytes();
Iterator<Map.Entry<Long, PreparedQuery>> entries = queries.entrySet().iterator();
while (queries.size() > MAXIMUM_QUERIES || queryBytes > MAXIMUM_QUERY_BYTES) {
queryBytes -= entries.next().getValue().estimatedBytes();
entries.remove();
}
}
private PreparedQuery prepareDefinitions(SavedBiomeChunk chunk) throws IOException {
if (chunk.estimatedBytes() + 256L > MAXIMUM_QUERY_BYTES) {
throw new SavedBiomeUnavailableException("Saved biome definitions exceed the query memory limit.", false);
}
Map<SavedBiomeChunk.Cell, BiomeEnvironment> environments = new HashMap<>();
Map<SavedBiomeChunk.Cell, SavedBiomeUnavailableException> failures = new HashMap<>();
Set<Long> nativeOwners = new HashSet<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
SavedBiomeChunk.Column column = chunk.column(x, z);
prepareEnvironment(chunk, column.surface(), environments, failures);
prepareEnvironment(chunk, column.caveBase(), environments, failures);
for (SavedBiomeChunk.Span span : column.vertical()) {
if (nativeOwners.add(span.cell().activationId())) {
prepareNativeDefinitions(span.cell().activationId());
}
prepareEnvironment(chunk, span.cell(), environments, failures);
}
}
}
return new PreparedQuery(Optional.of(chunk), Map.copyOf(environments), Map.copyOf(failures), null,
chunk.estimatedBytes() + environments.size() * 256L + failures.size() * 512L + 256L);
}
private void prepareNativeDefinitions(long activationId) throws IOException {
try {
definitions(activationId);
} catch (SavedBiomeUnavailableException unavailable) {
IrisLogging.warnOnce("saved-biome:" + history.paths().dimensionRoot() + ":" + unavailable.getMessage(),
unavailable.getMessage());
}
}
private void prepareEnvironment(SavedBiomeChunk chunk, SavedBiomeChunk.Cell cell,
Map<SavedBiomeChunk.Cell, BiomeEnvironment> environments,
Map<SavedBiomeChunk.Cell, SavedBiomeUnavailableException> failures) throws IOException {
if (!cell.isResolved() || environments.containsKey(cell) || failures.containsKey(cell)) {
return;
}
if (chunk.estimatedBytes() + environments.size() * 256L + (failures.size() + 1L) * 512L + 256L > MAXIMUM_QUERY_BYTES) {
throw new SavedBiomeUnavailableException("Saved biome definitions exceed the query memory limit.", false);
}
try {
environments.put(cell, definitions(cell.activationId()).environment(cell));
} catch (SavedBiomeUnavailableException unavailable) {
failures.put(cell, unavailable);
IrisLogging.warnOnce("saved-biome:" + history.paths().dimensionRoot() + ":" + unavailable.getMessage(),
unavailable.getMessage());
}
}
private Definitions definitions(long activationId) throws IOException {
GenerationActivation activation = history.manifest().activation(activationId).orElseThrow(() ->
new IOException("Historical biome generation is missing: " + activationId));
String epochId = activation.epochId();
Definitions cached = definitions.get(epochId);
if (cached != null) {
return cached;
}
synchronized (definitions) {
cached = definitions.get(epochId);
if (cached != null) {
return cached;
}
GenerationEpoch epoch = history.manifest().epoch(activation.epochId()).orElseThrow(() ->
new IOException("Historical biome epoch is missing: " + activation.epochId()));
Path pack = history.paths().packRoot(epoch.epochId());
if (!Files.exists(pack, LinkOption.NOFOLLOW_LINKS)) {
throw new SavedBiomeUnavailableException("The saved pack definitions for generation " + activationId
+ " are missing. Its terrain remains saved, but its Iris biome rules cannot run.", false);
}
IrisData data = IrisData.openRuntime(history.packRoot(activationId).toFile());
try {
data.bindGenerationRegistryContract(epoch.registryContract());
data.registerEngine(engine);
IrisDimension dimension = data.getDimensionLoader().load(epoch.dimensionContract().dimensionKey());
if (dimension == null) {
throw new IOException("Historical dimension is no longer supported: " + epoch.dimensionContract().dimensionKey());
}
Definitions loaded = new Definitions(data, dimension, NativeBiomeSpawnSelection.retainedDerivatives(data));
definitions.put(epochId, loaded);
return loaded;
} catch (Throwable failure) {
data.unregisterEngine(engine);
data.close();
throw failure;
}
}
}
private void requireOpen() {
if (closed) {
throw new IllegalStateException("Saved biome runtime is closed.");
}
}
private static long key(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL);
}
private record PreparedQuery(Optional<SavedBiomeChunk> chunk,
Map<SavedBiomeChunk.Cell, BiomeEnvironment> environments,
Map<SavedBiomeChunk.Cell, SavedBiomeUnavailableException> cellFailures,
SavedBiomeUnavailableException failure, long estimatedBytes) {
}
private static final class PendingQuery {
private boolean invalidated;
}
private enum QueryKind {
SURFACE, CAVE_BASE, VOLUME
}
private record Definitions(IrisData data, IrisDimension dimension, Map<String, String> nativeDerivatives) {
private BiomeEnvironment environment(SavedBiomeChunk.Cell cell) {
IrisBiome biome = data.getBiomeLoader().load(cell.biomeKey());
IrisRegion region = data.getRegionLoader().load(cell.regionKey());
if (biome == null || region == null) {
throw new SavedBiomeUnavailableException("Historical biome " + cell.biomeKey() + " in region "
+ cell.regionKey() + " is no longer supported for generation " + cell.activationId()
+ ". Its terrain remains saved, but its Iris biome rules cannot run.", false);
}
return new BiomeEnvironment(cell.activationId(), biome, region, dimension, data);
}
}
}
@@ -0,0 +1,670 @@
package art.arcane.iris.engine.history;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public final class SavedBiomeStore {
private static final Pattern REGION_FILE = Pattern.compile("r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.ibio");
private static final int MAGIC = 0x4942494F;
private static final int FORMAT_VERSION = 1;
private static final int HEADER_BYTES = 20;
private static final int MAXIMUM_PALETTE_CELLS = 65_536;
private static final int MAXIMUM_RECORD_BYTES = 16 * 1024 * 1024;
private static final int MAXIMUM_DECODED_BYTES = 64 * 1024 * 1024;
private static final long MAXIMUM_REGION_BYTES = 1024L * 1024L * 1024L;
private static final int MAXIMUM_CACHED_REGIONS = 64;
private static final int MAXIMUM_CACHED_CHUNKS = 128;
private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L;
private final Path dimensionRoot;
private final Path directory;
private final Object[] regionLocks = new Object[64];
private final LinkedHashMap<Long, RegionIndex> regions = new LinkedHashMap<>(64, 0.75F, true);
private final LinkedHashMap<Long, Optional<SavedBiomeChunk>> chunks = new LinkedHashMap<>(128, 0.75F, true);
private long cachedBytes;
private SavedBiomeStore(Path dimensionRoot) throws IOException {
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toAbsolutePath().normalize();
directory = this.dimensionRoot.resolve("iris/generation/biomes");
requireSafeAncestors();
for (int index = 0; index < regionLocks.length; index++) {
regionLocks[index] = new Object();
}
}
public static SavedBiomeStore open(Path dimensionRoot) throws IOException {
return new SavedBiomeStore(dimensionRoot);
}
public Path storageDirectory() {
return directory;
}
public Optional<SavedBiomeChunk> get(int chunkX, int chunkZ) throws IOException {
long chunkKey = key(chunkX, chunkZ);
synchronized (chunks) {
Optional<SavedBiomeChunk> cached = chunks.get(chunkKey);
if (cached != null) {
return cached;
}
}
synchronized (regionLock(chunkX, chunkZ)) {
synchronized (chunks) {
Optional<SavedBiomeChunk> cached = chunks.get(chunkKey);
if (cached != null) {
return cached;
}
}
RegionIndex region = region(chunkX, chunkZ);
int slot = slot(chunkX, chunkZ);
Optional<SavedBiomeChunk> result = region.offsets[slot] == 0L
? Optional.empty() : Optional.of(readRecord(region, slot));
cacheChunk(chunkKey, result);
return result;
}
}
public Optional<SavedBiomeChunk> cached(int chunkX, int chunkZ) {
synchronized (chunks) {
return chunks.getOrDefault(key(chunkX, chunkZ), Optional.empty());
}
}
public boolean claimAndPersist(SavedBiomeChunk chunk) throws IOException {
SavedBiomeChunk required = Objects.requireNonNull(chunk, "chunk");
synchronized (regionLock(required.chunkX(), required.chunkZ())) {
Optional<SavedBiomeChunk> existing = get(required.chunkX(), required.chunkZ());
if (existing.isPresent()) {
if (!existing.get().equals(required)) {
throw new IOException("Conflicting saved biome claim at chunk " + required.chunkX() + ", " + required.chunkZ());
}
return false;
}
byte[] body = encode(required);
RegionIndex region = region(required.chunkX(), required.chunkZ());
ensureRegionFile(region);
long offset;
try (RandomAccessFile output = new RandomAccessFile(region.path.toFile(), "rw")) {
offset = output.length();
if (offset + body.length + 8L > MAXIMUM_REGION_BYTES) {
throw new IOException("Saved biome region exceeds its storage limit: " + region.path);
}
try {
output.seek(offset);
output.writeInt(body.length);
output.write(body);
output.writeInt(checksum(body));
output.getChannel().force(true);
} catch (IOException failure) {
try {
output.setLength(offset);
output.getChannel().force(true);
} catch (IOException rollback) {
failure.addSuppressed(rollback);
}
throw failure;
}
}
int slot = slot(required.chunkX(), required.chunkZ());
region.offsets[slot] = offset;
region.lengths[slot] = body.length;
cacheChunk(key(required.chunkX(), required.chunkZ()), Optional.of(required));
return true;
}
}
public int discardUnstoredClaims(WorldChunkInventory inventory, Set<Long> selectedActivations) throws IOException {
Objects.requireNonNull(inventory, "inventory");
Set<Long> selected = Set.copyOf(selectedActivations);
for (long activationId : selected) {
if (activationId <= 0L) {
throw new IllegalArgumentException("Saved biome activation must be positive");
}
}
requireSafeAncestors();
if (selected.isEmpty() || !Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
return 0;
}
int removed = 0;
try (DirectoryStream<Path> files = Files.newDirectoryStream(directory, "r.*.*.ibio")) {
for (Path path : files) {
Matcher matcher = REGION_FILE.matcher(path.getFileName().toString());
if (!matcher.matches()) {
continue;
}
int regionX = parseRegionCoordinate(matcher.group(1));
int regionZ = parseRegionCoordinate(matcher.group(2));
synchronized (regionLock(regionX << 5, regionZ << 5)) {
RegionIndex region = region(regionX << 5, regionZ << 5);
boolean[] discarded = new boolean[1024];
int count = 0;
for (int slot = 0; slot < 1024; slot++) {
int chunkX = (regionX << 5) + (slot & 31);
int chunkZ = (regionZ << 5) + (slot >> 5);
if (region.offsets[slot] != 0L && !inventory.contains(chunkX, chunkZ)
&& selected.contains(readRecord(region, slot).activationId())) {
discarded[slot] = true;
count++;
}
}
if (count != 0) {
rewriteRegion(region, discarded);
removed = Math.addExact(removed, count);
}
}
}
}
return removed;
}
int cachedRegionCount() {
synchronized (regions) {
return regions.size();
}
}
int cachedChunkCount() {
synchronized (chunks) {
return chunks.size();
}
}
long cachedByteCount() {
synchronized (chunks) {
return cachedBytes;
}
}
private RegionIndex region(int chunkX, int chunkZ) throws IOException {
int regionX = chunkX >> 5;
int regionZ = chunkZ >> 5;
long regionKey = key(regionX, regionZ);
synchronized (regions) {
RegionIndex cached = regions.get(regionKey);
if (cached != null) {
return cached;
}
}
requireSafeAncestors();
RegionIndex loaded = new RegionIndex(directory.resolve("r." + regionX + "." + regionZ + ".ibio"), regionX, regionZ);
if (Files.exists(loaded.path, LinkOption.NOFOLLOW_LINKS)) {
loadIndex(loaded);
}
synchronized (regions) {
regions.put(regionKey, loaded);
while (regions.size() > MAXIMUM_CACHED_REGIONS) {
regions.remove(regions.keySet().iterator().next());
}
}
return loaded;
}
private void loadIndex(RegionIndex region) throws IOException {
if (!Files.isRegularFile(region.path, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Saved biome region is not a regular file: " + region.path);
}
try (RandomAccessFile input = new RandomAccessFile(region.path.toFile(), "rw")) {
long fileLength = input.length();
if (fileLength < HEADER_BYTES || fileLength > MAXIMUM_REGION_BYTES) {
throw new IOException("Invalid saved biome region size: " + region.path);
}
byte[] header = new byte[HEADER_BYTES];
input.readFully(header);
ByteBuffer fields = ByteBuffer.wrap(header);
if (fields.getInt() != MAGIC || fields.getInt() != FORMAT_VERSION
|| fields.getInt() != region.regionX || fields.getInt() != region.regionZ
|| fields.getInt() != checksum(header, HEADER_BYTES - 4)) {
throw new IOException("Invalid saved biome region header: " + region.path);
}
byte[] buffer = new byte[8192];
int records = 0;
while (input.getFilePointer() < fileLength) {
long offset = input.getFilePointer();
if (fileLength - offset < Integer.BYTES) {
truncateTail(input, offset);
break;
}
int length = input.readInt();
if (length < 12 || length > MAXIMUM_RECORD_BYTES) {
throw new IOException("Invalid saved biome record size: " + region.path);
}
if (fileLength - input.getFilePointer() < length + 4L) {
truncateTail(input, offset);
break;
}
CRC32 crc = new CRC32();
input.readFully(buffer, 0, 8);
crc.update(buffer, 0, 8);
ByteBuffer coordinates = ByteBuffer.wrap(buffer, 0, 8);
int chunkX = coordinates.getInt();
int chunkZ = coordinates.getInt();
for (int remaining = length - 8; remaining > 0;) {
int count = Math.min(remaining, buffer.length);
input.readFully(buffer, 0, count);
crc.update(buffer, 0, count);
remaining -= count;
}
if (input.readInt() != (int) crc.getValue() || chunkX >> 5 != region.regionX || chunkZ >> 5 != region.regionZ) {
throw new IOException("Invalid saved biome record checksum or coordinates: " + region.path);
}
int slot = slot(chunkX, chunkZ);
if (++records > 1024 || region.offsets[slot] != 0L) {
throw new IOException("Duplicate saved biome chunk record: " + region.path);
}
region.offsets[slot] = offset;
region.lengths[slot] = length;
}
}
}
private SavedBiomeChunk readRecord(RegionIndex region, int slot) throws IOException {
byte[] encoded = new byte[region.lengths[slot]];
try (RandomAccessFile input = new RandomAccessFile(region.path.toFile(), "r")) {
input.seek(region.offsets[slot]);
if (input.readInt() != encoded.length) {
throw new IOException("Saved biome record length changed: " + region.path);
}
input.readFully(encoded);
if (input.readInt() != checksum(encoded)) {
throw new IOException("Saved biome record checksum mismatch: " + region.path);
}
}
SavedBiomeChunk result = decode(encoded);
if (result.chunkX() >> 5 != region.regionX || result.chunkZ() >> 5 != region.regionZ
|| slot(result.chunkX(), result.chunkZ()) != slot) {
throw new IOException("Saved biome record coordinates do not match its index: " + region.path);
}
return result;
}
private void ensureRegionFile(RegionIndex region) throws IOException {
requireSafeAncestors();
Files.createDirectories(directory);
if (Files.exists(region.path, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isRegularFile(region.path, LinkOption.NOFOLLOW_LINKS)) {
throw new IOException("Saved biome region is not a regular file: " + region.path);
}
return;
}
ByteBuffer header = ByteBuffer.allocate(HEADER_BYTES);
header.putInt(MAGIC).putInt(FORMAT_VERSION).putInt(region.regionX).putInt(region.regionZ);
header.putInt(checksum(header.array(), HEADER_BYTES - 4));
Path temporary = Files.createTempFile(directory, ".biomes-", ".tmp");
try {
try (FileChannel output = FileChannel.open(temporary, StandardOpenOption.WRITE)) {
header.flip();
while (header.hasRemaining()) {
output.write(header);
}
output.force(true);
}
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE);
forceDirectory(directory);
} finally {
Files.deleteIfExists(temporary);
}
}
private static void forceDirectory(Path path) throws IOException {
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
channel.force(true);
} catch (UnsupportedOperationException failure) {
throw new IOException("Saved biome directory cannot be durability-synced: " + path, failure);
}
}
private void rewriteRegion(RegionIndex region, boolean[] discarded) throws IOException {
Path temporary = Files.createTempFile(directory, ".biomes-", ".tmp");
try {
try (FileChannel input = FileChannel.open(region.path, StandardOpenOption.READ);
FileChannel output = FileChannel.open(temporary, StandardOpenOption.WRITE)) {
transfer(input, output, 0L, HEADER_BYTES);
for (int slot = 0; slot < 1024; slot++) {
if (region.offsets[slot] != 0L && !discarded[slot]) {
transfer(input, output, region.offsets[slot], region.lengths[slot] + 8L);
}
}
output.force(true);
}
Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
synchronized (regions) {
regions.remove(key(region.regionX, region.regionZ));
}
synchronized (chunks) {
for (int slot = 0; slot < 1024; slot++) {
long chunkKey = key((region.regionX << 5) + (slot & 31), (region.regionZ << 5) + (slot >> 5));
Optional<SavedBiomeChunk> removed = chunks.remove(chunkKey);
if (removed != null && removed.isPresent()) {
cachedBytes -= removed.get().estimatedBytes();
}
}
}
forceDirectory(directory);
} finally {
Files.deleteIfExists(temporary);
}
}
private static void transfer(FileChannel input, FileChannel output, long offset, long length) throws IOException {
while (length > 0L) {
long count = input.transferTo(offset, length, output);
if (count <= 0L) {
throw new IOException("Saved biome region ended during rewrite");
}
offset += count;
length -= count;
}
}
private static int parseRegionCoordinate(String coordinate) throws IOException {
try {
int parsed = Integer.parseInt(coordinate);
if (parsed < Integer.MIN_VALUE >> 5 || parsed > Integer.MAX_VALUE >> 5) {
throw new IOException("Saved biome region coordinate is out of range");
}
return parsed;
} catch (NumberFormatException failure) {
throw new IOException("Invalid saved biome region coordinate", failure);
}
}
private void requireSafeAncestors() throws IOException {
for (Path path = directory; path != null && path.startsWith(dimensionRoot); path = path.getParent()) {
if (Files.isSymbolicLink(path)) {
throw new IOException("Saved biome storage must not use symbolic links: " + path);
}
}
}
private void cacheChunk(long key, Optional<SavedBiomeChunk> value) {
synchronized (chunks) {
Optional<SavedBiomeChunk> previous = chunks.remove(key);
if (previous != null && previous.isPresent()) {
cachedBytes -= previous.get().estimatedBytes();
}
long weight = value.map(SavedBiomeChunk::estimatedBytes).orElse(0L);
if (weight > MAXIMUM_CACHED_BYTES) {
return;
}
chunks.put(key, value);
cachedBytes += weight;
Iterator<Map.Entry<Long, Optional<SavedBiomeChunk>>> entries = chunks.entrySet().iterator();
while (chunks.size() > MAXIMUM_CACHED_CHUNKS || cachedBytes > MAXIMUM_CACHED_BYTES) {
Optional<SavedBiomeChunk> removed = entries.next().getValue();
cachedBytes -= removed.map(SavedBiomeChunk::estimatedBytes).orElse(0L);
entries.remove();
}
}
}
private Object regionLock(int chunkX, int chunkZ) {
long key = key(chunkX >> 5, chunkZ >> 5);
return regionLocks[(int) (key ^ (key >>> 32)) & (regionLocks.length - 1)];
}
private static long key(int x, int z) {
return (long) x << 32 | z & 0xffffffffL;
}
private static int slot(int chunkX, int chunkZ) {
return (chunkZ & 31) * 32 + (chunkX & 31);
}
private static void truncateTail(RandomAccessFile file, long length) throws IOException {
file.setLength(length);
file.getChannel().force(true);
}
private static int checksum(byte[] bytes) {
return checksum(bytes, bytes.length);
}
private static int checksum(byte[] bytes, int length) {
CRC32 checksum = new CRC32();
checksum.update(bytes, 0, length);
return (int) checksum.getValue();
}
private static byte[] encode(SavedBiomeChunk chunk) throws IOException {
LinkedHashMap<SavedBiomeChunk.Cell, Integer> palette = new LinkedHashMap<>();
LinkedHashMap<SavedBiomeChunk.Column, Integer> columns = new LinkedHashMap<>();
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
SavedBiomeChunk.Column column = chunk.column(x, z);
columns.computeIfAbsent(column, ignored -> columns.size());
palette.computeIfAbsent(column.surface(), ignored -> palette.size());
palette.computeIfAbsent(column.caveBase(), ignored -> palette.size());
for (SavedBiomeChunk.Span span : column.vertical()) {
palette.computeIfAbsent(span.cell(), ignored -> palette.size());
}
}
}
if (palette.size() > MAXIMUM_PALETTE_CELLS) {
throw new IOException("Saved biome palette exceeds its cell limit");
}
ByteArrayOutputStream raw = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(raw)) {
output.writeLong(chunk.activationId());
output.writeInt(chunk.header().minimumY());
output.writeInt(chunk.header().height());
output.writeInt(palette.size());
for (SavedBiomeChunk.Cell cell : palette.keySet()) {
output.writeLong(cell.activationId());
output.writeByte(cell.isResolved() ? 0 : 1);
if (cell.isResolved()) {
output.writeUTF(cell.biomeKey());
output.writeUTF(cell.regionKey());
}
}
output.writeInt(columns.size());
for (SavedBiomeChunk.Column column : columns.keySet()) {
writeUnsigned(output, palette.get(column.surface()));
writeUnsigned(output, palette.get(column.caveBase()));
writeUnsigned(output, column.vertical().size());
for (SavedBiomeChunk.Span span : column.vertical()) {
writeUnsigned(output, span.maximumYExclusive() - chunk.header().minimumY());
writeUnsigned(output, palette.get(span.cell()));
}
}
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
output.writeByte(columns.get(chunk.column(x, z)));
}
}
}
if (raw.size() > MAXIMUM_DECODED_BYTES) {
throw new IOException("Saved biome chunk exceeds its decoded size limit");
}
ByteArrayOutputStream encoded = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(encoded);
output.writeInt(chunk.chunkX());
output.writeInt(chunk.chunkZ());
output.writeInt(raw.size());
Deflater deflater = new Deflater(Deflater.BEST_SPEED);
try (DeflaterOutputStream compressed = new DeflaterOutputStream(output, deflater)) {
raw.writeTo(compressed);
} finally {
deflater.end();
}
if (encoded.size() > MAXIMUM_RECORD_BYTES) {
throw new IOException("Saved biome chunk exceeds its encoded size limit");
}
return encoded.toByteArray();
}
private static SavedBiomeChunk decode(byte[] encoded) throws IOException {
try (DataInputStream header = new DataInputStream(new ByteArrayInputStream(encoded))) {
int chunkX = header.readInt();
int chunkZ = header.readInt();
int decodedLength = header.readInt();
if (decodedLength < 24 || decodedLength > MAXIMUM_DECODED_BYTES) {
throw new IOException("Invalid saved biome decoded size");
}
byte[] raw;
try (InflaterInputStream compressed = new InflaterInputStream(header)) {
raw = compressed.readNBytes(decodedLength);
if (raw.length != decodedLength || compressed.read() != -1) {
throw new IOException("Saved biome compressed length mismatch");
}
}
try (DataInputStream input = new DataInputStream(new ByteArrayInputStream(raw))) {
SavedBiomeChunk.Header metadata = new SavedBiomeChunk.Header(chunkX, chunkZ, input.readLong(), input.readInt(), input.readInt());
DecodeBudget budget = new DecodeBudget();
int paletteSize = input.readInt();
if (paletteSize <= 0 || paletteSize > MAXIMUM_PALETTE_CELLS || paletteSize > input.available() / 9) {
throw new IOException("Invalid saved biome palette size");
}
budget.reserve(paletteSize * 64L);
ArrayList<SavedBiomeChunk.Cell> palette = new ArrayList<>(paletteSize);
for (int index = 0; index < paletteSize; index++) {
long activationId = input.readLong();
int resolution = input.readUnsignedByte();
SavedBiomeChunk.Cell cell = switch (resolution) {
case 0 -> new SavedBiomeChunk.Cell(activationId, readKey(input, budget), readKey(input, budget));
case 1 -> SavedBiomeChunk.Cell.unresolved(activationId);
default -> throw new IOException("Invalid saved biome resolution state");
};
palette.add(cell);
}
int columnCount = input.readInt();
if (columnCount <= 0 || columnCount > 256) {
throw new IOException("Invalid saved biome column palette size");
}
budget.reserve(columnCount * 128L);
ArrayList<SavedBiomeChunk.Column> columns = new ArrayList<>(columnCount);
for (int index = 0; index < columnCount; index++) {
SavedBiomeChunk.Cell surface = paletteCell(palette, readUnsigned(input));
SavedBiomeChunk.Cell caveBase = paletteCell(palette, readUnsigned(input));
int count = readUnsigned(input);
if (count <= 0 || count > metadata.height() || count > input.available() / 2) {
throw new IOException("Invalid saved biome span count");
}
budget.reserve(count * 80L);
ArrayList<SavedBiomeChunk.Span> spans = new ArrayList<>(count);
int start = metadata.minimumY();
for (int spanIndex = 0; spanIndex < count; spanIndex++) {
int end = Math.addExact(metadata.minimumY(), readUnsigned(input));
spans.add(new SavedBiomeChunk.Span(start, end, paletteCell(palette, readUnsigned(input))));
start = end;
}
columns.add(new SavedBiomeChunk.Column(surface, caveBase, spans));
}
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(metadata);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
int column = input.readUnsignedByte();
if (column >= columns.size()) {
throw new IOException("Invalid saved biome column index");
}
builder.column(x, z, columns.get(column));
}
}
if (input.read() != -1) {
throw new IOException("Unexpected saved biome payload bytes");
}
return builder.build();
}
} catch (IllegalArgumentException | ArithmeticException failure) {
throw new IOException("Invalid saved biome chunk payload", failure);
}
}
private static String readKey(DataInputStream input, DecodeBudget budget) throws IOException {
int length = input.readUnsignedShort();
if (length > ChunkGenerationSemantics.MAX_KEY_BYTES * 3 || length > input.available()) {
throw new IOException("Invalid saved biome resource key size");
}
budget.reserve(64L + length * 4L);
byte[] encoded = new byte[length + 2];
ByteBuffer.wrap(encoded).putShort((short) length);
input.readFully(encoded, 2, length);
try (DataInputStream key = new DataInputStream(new ByteArrayInputStream(encoded))) {
return key.readUTF();
}
}
private static SavedBiomeChunk.Cell paletteCell(List<SavedBiomeChunk.Cell> palette, int index) throws IOException {
if (index < 0 || index >= palette.size()) {
throw new IOException("Invalid saved biome palette index");
}
return palette.get(index);
}
private static void writeUnsigned(DataOutputStream output, int value) throws IOException {
while ((value & ~0x7f) != 0) {
output.writeByte((value & 0x7f) | 0x80);
value >>>= 7;
}
output.writeByte(value);
}
private static int readUnsigned(DataInputStream input) throws IOException {
int result = 0;
for (int shift = 0; shift < 35; shift += 7) {
int value = input.readUnsignedByte();
if (shift == 28 && (value & 0xf8) != 0) {
throw new IOException("Invalid saved biome variable integer");
}
result |= (value & 0x7f) << shift;
if ((value & 0x80) == 0) {
return result;
}
}
throw new IOException("Invalid saved biome variable integer");
}
private static final class DecodeBudget {
private long remaining = SavedBiomeChunk.MAXIMUM_ESTIMATED_BYTES - 8192L;
private void reserve(long bytes) throws IOException {
if (bytes < 0L || bytes > remaining) {
throw new IOException("Saved biome chunk exceeds its decoded footprint limit");
}
remaining -= bytes;
}
}
private static final class RegionIndex {
private final Path path;
private final int regionX;
private final int regionZ;
private final long[] offsets = new long[1024];
private final int[] lengths = new int[1024];
private RegionIndex(Path path, int regionX, int regionZ) {
this.path = path;
this.regionX = regionX;
this.regionZ = regionZ;
}
}
}
@@ -0,0 +1,19 @@
package art.arcane.iris.engine.history;
public final class SavedBiomeUnavailableException extends IllegalStateException {
private final boolean loading;
public SavedBiomeUnavailableException(String message, boolean loading) {
super(message);
this.loading = loading;
}
public SavedBiomeUnavailableException(String message, Throwable cause) {
super(message, cause);
loading = false;
}
public boolean isLoading() {
return loading;
}
}
@@ -50,6 +50,7 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
import art.arcane.iris.engine.history.FloatingBiomeOverlay;
import java.util.Optional;
import java.util.IdentityHashMap;
@@ -395,7 +396,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
ChunkContext context
) {
try {
IdentityHashMap<IrisFloatingChildBiomes, PlatformBiome> matterByEntry = new IdentityHashMap<>();
IdentityHashMap<IrisFloatingChildBiomes, FloatingBiome> matterByEntry = new IdentityHashMap<>();
for (int k = 0; k <= sample.topIdx; k++) {
if (!sample.solidMask[k]) {
continue;
@@ -408,16 +409,18 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
continue;
}
IrisFloatingChildBiomes entry = sample.entryAt(k);
PlatformBiome matter = matterByEntry.get(entry);
FloatingBiome matter = matterByEntry.get(entry);
if (matter == null) {
IrisBiome target = entry == null ? parent : entry.getRealBiome(parent, data);
if (target == null) {
continue;
}
matter = createSkyBiome(target, wx, wz);
matter = new FloatingBiome(createSkyBiome(target, wx, wz),
new FloatingBiomeOverlay.Identity(target.getLoadKey(),
context.getRegion().get(wx & 15, wz & 15).getLoadKey()));
matterByEntry.put(entry, matter);
}
PlatformBiome selected = matter;
PlatformBiome selected = matter.physical();
TransitionGenerationPlan transition = context.getComplex().getTransitionGenerationPlan();
if (transition != null) {
Optional<String> historical = transition.historicalPhysicalBiomeKeyAt(wx, y + getEngine().getMinHeight(), wz);
@@ -426,6 +429,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
}
}
context.setNaturalBiome(wx & 15, y, wz & 15, selected);
context.floatingBiomes(chunkHeight).record(wx & 15, y, wz & 15, matter.identity());
if (!context.isSpeculativeTerrain()) {
getEngine().getMantle().getMantle().set(wx, y, wz, BiomeInjectMatter.get(selected.key()));
}
@@ -451,4 +455,8 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier<Platf
return IrisPlatforms.get().registries().biome(target.getSkyBiomeKey(rng, getEngine(), wx, 0, wz));
}
private record FloatingBiome(PlatformBiome physical, FloatingBiomeOverlay.Identity identity) {
}
}
@@ -37,6 +37,8 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Location;
import org.bukkit.HeightMap;
import org.bukkit.World;
import org.bukkit.NamespacedKey;
import org.bukkit.Particle;
import org.bukkit.Sound;
@@ -242,24 +244,7 @@ public class IrisEffect {
if (particleType != null) {
Location part = p.getLocation().clone().add(p.getLocation().getDirection().clone().multiply(RNG.r.i(particleDistance) + particleAway)).clone().add(p.getLocation().getDirection().clone().rotateAroundY(Math.toRadians(90)).multiply(RNG.r.d(-particleDistanceWidth, particleDistanceWidth)));
part.setY(Math.round(g.getHeight(part.getBlockX(), part.getBlockZ())) + 1);
part.add(RNG.r.d(), 0, RNG.r.d());
int offset = p.getWorld().getMinHeight();
if (extra != 0) {
p.spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset),
part.getZ(),
particleCount,
randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX,
randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY,
randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ,
extra);
} else {
p.spawnParticle(particleType, part.getX(), part.getY() + offset + RNG.r.i(particleOffset), part.getZ(),
particleCount,
randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX,
randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY,
randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ);
}
applyParticles(p, particleType, part);
}
if (commandRegistry != null) {
@@ -285,6 +270,41 @@ public class IrisEffect {
});
}
void applyParticles(Player player, Particle particle, Location location) {
World world = location.getWorld();
if (J.isOwnedByCurrentRegion(world, location.getBlockX() >> 4, location.getBlockZ() >> 4)) {
sampleParticleSurface(player, particle, location);
} else {
J.runAt(location, () -> sampleParticleSurface(player, particle, location));
}
}
private void sampleParticleSurface(Player player, Particle particle, Location location) {
World world = location.getWorld();
if (!world.isChunkLoaded(location.getBlockX() >> 4, location.getBlockZ() >> 4)) {
return;
}
location.setY(world.getHighestBlockYAt(location.getBlockX(), location.getBlockZ(), HeightMap.OCEAN_FLOOR) + 1);
location.add(RNG.r.d(), 0, RNG.r.d());
BukkitFx.run(player, () -> emitParticles(player, particle, location));
}
private void emitParticles(Player player, Particle particle, Location location) {
if (player.getWorld() != location.getWorld()) {
return;
}
double xOffset = randomAltX ? RNG.r.d(-particleAltX, particleAltX) : particleAltX;
double yOffset = randomAltY ? RNG.r.d(-particleAltY, particleAltY) : particleAltY;
double zOffset = randomAltZ ? RNG.r.d(-particleAltZ, particleAltZ) : particleAltZ;
if (extra != 0) {
player.spawnParticle(particle, location.getX(), location.getY() + RNG.r.i(particleOffset),
location.getZ(), particleCount, xOffset, yOffset, zOffset, extra);
} else {
player.spawnParticle(particle, location.getX(), location.getY() + RNG.r.i(particleOffset),
location.getZ(), particleCount, xOffset, yOffset, zOffset);
}
}
public void apply(Entity p) {
if (!shouldApplyNow()) {
return;
@@ -23,6 +23,7 @@ import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.service.EntityRiseSVC;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.framework.Engine;
@@ -507,6 +508,7 @@ public class IrisEntity extends IrisRegistrant {
}
private static void bindLoot(IrisEntity entity, Engine gen, Lootable l, Location finalAt, RNG rng) {
IrisData definitions = gen.getData();
l.setLootTable(new LootTable() {
@Override
public NamespacedKey getKey() {
@@ -518,7 +520,7 @@ public class IrisEntity extends IrisRegistrant {
KList<ItemStack> items = new KList<>();
for (String fi : entity.getLoot().getTables()) {
IrisLootTable i = gen.getData().getLootLoader().load(fi);
IrisLootTable i = definitions.getLootLoader().load(fi);
items.addAll(i.getLoot(gen.isStudio(), gen.getSeedManager().getLoot(), InventorySlotType.STORAGE, finalAt.getWorld(), finalAt.getBlockX(), finalAt.getBlockY(), finalAt.getBlockZ()));
}
@@ -42,6 +42,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.bukkit.Chunk;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
@@ -80,9 +81,9 @@ public class IrisEntitySpawn implements IRare {
for (int id = 0; id < spawns; id++) {
int x = (c.getX() * 16) + rng.i(16);
int z = (c.getZ() * 16) + rng.i(16);
World world = BukkitWorldBinding.tryBind(gen.getWorld()) ? BukkitWorldBinding.world(gen.getWorld()) : null;
int h = gen.getHeight(x, z, true) + (world == null ? -64 : world.getMinHeight());
int hf = gen.getHeight(x, z, false) + (world == null ? -64 : world.getMinHeight());
World world = c.getWorld();
int h = world.getHighestBlockYAt(x, z, HeightMap.OCEAN_FLOOR);
int hf = world.getHighestBlockYAt(x, z, HeightMap.WORLD_SURFACE);
Location l = switch (getReferenceSpawner().getGroup()) {
case NORMAL -> new Location(c.getWorld(), x, hf + 1, z);
case CAVE -> findCaveSpawnLocation(gen, c, rng);
@@ -8,6 +8,7 @@ import art.arcane.iris.engine.framework.EngineMetrics;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.engine.object.FloatingIslandBoundarySampler;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.history.FloatingBiomeOverlay;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformBiome;
@@ -22,6 +23,7 @@ public class ChunkContext {
private boolean naturalTerrain = true;
private boolean speculativeTerrain;
private Hunk<PlatformBiome> terrainBiomes;
private FloatingBiomeOverlay floatingBiomes;
private final int x;
private final int z;
private final IrisComplex complex;
@@ -141,6 +143,21 @@ public class ChunkContext {
this.terrainBiomes = terrainBiomes;
}
public FloatingBiomeOverlay getFloatingBiomes() {
return floatingBiomes;
}
public void setFloatingBiomes(FloatingBiomeOverlay floatingBiomes) {
this.floatingBiomes = floatingBiomes;
}
public FloatingBiomeOverlay floatingBiomes(int height) {
if (floatingBiomes == null) {
floatingBiomes = new FloatingBiomeOverlay(height);
}
return floatingBiomes;
}
public void setNaturalBiome(int localX, int y, int localZ, PlatformBiome biome) {
if (terrainBiomes != null) {
terrainBiomes.setRaw(localX, y, localZ, biome);
@@ -64,12 +64,26 @@ public class WorldReplacementBootstrapTest {
}
@Test
public void rejectsNestedMetadataAddedAfterImmutableEpochPublication() throws Exception {
public void acceptsFinderMetadataAddedAfterImmutableEpochPublication() throws Exception {
Transaction transaction = stagedTransaction(Phase.ARMED, true, "original");
configureReplacement(transaction);
Path dimensions = activePackRoot(paths(transaction).stage()).resolve("dimensions");
Files.writeString(dimensions.resolve(".DS_Store"), "Finder metadata");
WorldReplacementBootstrap.ReconcileResult result = reconcile();
assertEquals(1, result.published());
assertEquals("replacement", replacementContent(target.worldDirectory()));
assertEquals(Phase.PUBLISHED, loadSingle().phase());
}
@Test
public void rejectsNestedResourceAddedAfterImmutableEpochPublication() throws Exception {
Transaction transaction = stagedTransaction(Phase.ARMED, true, "original");
configureReplacement(transaction);
Path dimensions = activePackRoot(paths(transaction).stage()).resolve("dimensions");
Files.writeString(dimensions.resolve(".hidden-resource"), "changed resource");
assertThrows(IOException.class, this::reconcile);
assertTrue(Files.isDirectory(paths(transaction).stage()));
@@ -3,6 +3,12 @@ package art.arcane.iris.engine;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.engine.framework.EngineEffects;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.history.SavedBiomeRuntime;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.object.IrisRegion;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EngineStage;
import art.arcane.iris.engine.framework.EngineTarget;
@@ -59,6 +65,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class IrisEngineGenerationRuntimeScopeTest {
@@ -86,6 +93,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, selected.runtime);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
setField(engine, "generationHistoryRuntimeRouter", router);
doReturn(null).when(engine).getDimensionStackContext();
doReturn(false).when(engine).answersFromNaturalTerrain(anyInt(), anyInt());
@@ -128,6 +136,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
RuntimeFixture selected = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
setField(engine, "generationHistoryRuntimeRouter", router);
ProceduralStream<IrisBiome> caves = mock(ProceduralStream.class);
when(active.complex.getCaveBiomeStream()).thenReturn(caves);
@@ -149,6 +158,82 @@ public class IrisEngineGenerationRuntimeScopeTest {
verify(router, times(3)).openCoordinateScope(19, -3);
}
@Test
@SuppressWarnings("unchecked")
public void unboundContentContextSkipsSavedBiomesAndSelectsTheCoordinateRuntime() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture selected = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, selected.runtime);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(router.biomes()).thenReturn(saved);
setField(engine, "generationHistoryRuntimeRouter", router);
doReturn(null).when(engine).getDimensionStackContext();
doReturn(false).when(engine).answersFromNaturalTerrain(anyInt(), anyInt());
ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(active.complex);
when(context.isNaturalTerrain()).thenReturn(false);
IrisBiome cave = mock(IrisBiome.class);
IrisBiome surface = mock(IrisBiome.class);
IrisRegion region = mock(IrisRegion.class);
ProceduralStream<IrisBiome> caves = mock(ProceduralStream.class);
ProceduralStream<IrisBiome> surfaces = mock(ProceduralStream.class);
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
when(selected.complex.getCaveBiomeStream()).thenReturn(caves);
when(selected.complex.getTrueBiomeStream()).thenReturn(surfaces);
when(selected.complex.getRegionStream()).thenReturn(regions);
when(caves.get(19D, -3D)).thenReturn(cave);
when(surfaces.get(19D, -3D)).thenReturn(surface);
when(regions.get(19D, -3D)).thenReturn(region);
when(router.openCoordinateScope(19, -3)).thenAnswer(invocation -> {
assertSame(active.complex, engine.getComplex());
IrisEngine.GenerationRuntimeScope runtimeScope = engine.openGenerationRuntimeScope(binding);
GenerationHistoryRuntimeRouter.CoordinateScope coordinateScope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
doAnswer(close -> {
runtimeScope.close();
return null;
}).when(coordinateScope).close();
return coordinateScope;
});
try (IrisContext.Scope ignored = IrisContext.open(engine, 73L, context)) {
assertSame(cave, engine.getCaveBiome(19, -3));
assertSame(surface, engine.getSurfaceBiome(19, -3));
assertSame(region, engine.getRegion(19, -3));
assertSame(region, engine.getRegion(19, 20, -3));
assertSame(active.complex, engine.getComplex());
}
verify(router, times(4)).openCoordinateScope(19, -3);
verifyNoInteractions(saved);
assertNull(IrisContext.get());
}
@Test
public void gameplayContextWithoutAChunkStillReadsSavedBiomes() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
when(active.target.getWorld().minHeight()).thenReturn(-64);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(router.biomes()).thenReturn(saved);
setField(engine, "generationHistoryRuntimeRouter", router);
BiomeEnvironment environment = new BiomeEnvironment(4L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
when(saved.resolve(19, -64, -3, true)).thenReturn(Optional.of(environment));
when(saved.resolveCaveBase(19, -3)).thenReturn(Optional.of(environment));
try (IrisContext.Scope ignored = IrisContext.open(engine, 0L, null)) {
assertSame(environment.biome(), engine.getCaveBiome(19, -3));
assertSame(environment.biome(), engine.getSurfaceBiome(19, -3));
assertSame(environment.region(), engine.getRegion(19, -3));
}
verify(saved).resolveCaveBase(19, -3);
verify(saved, times(2)).resolve(19, -64, -3, true);
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
assertNull(IrisContext.get());
}
@Test
public void scopedGettersResolveDetachedGenerationStateAndWorldServicesStayActive() throws Exception {
RuntimeFixture active = runtime(1, 1D, 2D, 3D);
@@ -426,6 +511,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
@@ -441,6 +527,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
@@ -457,6 +544,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
assertNull(engine.openGenerationHistoryCoordinateScope(0, 0));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
when(router.engine()).thenReturn(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
engine.detachGenerationHistoryRuntimeRouter(router);
@@ -540,6 +628,83 @@ public class IrisEngineGenerationRuntimeScopeTest {
verify(detached.data).close();
}
@Test
public void savedBiomesAnswerWithoutOpeningGenerationRoutes() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
when(active.target.getWorld().minHeight()).thenReturn(-64);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(router.biomes()).thenReturn(saved);
setField(engine, "generationHistoryRuntimeRouter", router);
BiomeEnvironment environment = new BiomeEnvironment(4L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
when(saved.resolve(19, -44, -3, false)).thenReturn(Optional.of(environment));
when(saved.resolve(19, -64, -3, true)).thenReturn(Optional.of(environment));
when(saved.resolveCaveBase(19, -3)).thenReturn(Optional.of(environment));
assertSame(environment, engine.getBiomeEnvironment(19, 20, -3));
assertSame(environment, engine.getSurfaceBiomeEnvironment(19, -3));
assertSame(environment.biome(), engine.getBiome(19, 20, -3));
assertSame(environment.biome(), engine.getBiomeOrMantle(19, 20, -3));
assertSame(environment.biome(), engine.getCaveOrMantleBiome(19, 20, -3));
assertSame(environment.biome(), engine.getCaveBiome(19, 20, -3));
assertSame(environment.biome(), engine.getCaveBiome(19, -3));
assertSame(environment.biome(), engine.getSurfaceBiome(19, -3));
assertSame(environment.region(), engine.getRegion(19, -3));
assertSame(environment.region(), engine.getRegion(19, 20, -3));
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
}
@Test
public void unavailableSavedBiomesNeverUseTheActiveGenerator() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(router.biomes()).thenReturn(saved);
setField(engine, "generationHistoryRuntimeRouter", router);
SavedBiomeUnavailableException failure = new SavedBiomeUnavailableException("Missing historical biome", false);
when(saved.resolve(19, 20, -3, false)).thenThrow(failure);
assertSame(failure, assertThrows(SavedBiomeUnavailableException.class,
() -> engine.getBiome(19, 20, -3)));
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
}
@Test
public void definitionScopesRestoreAndLeaveGenerationRuntimeUnchanged() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture selected = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
BiomeEnvironment first = new BiomeEnvironment(4L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
BiomeEnvironment second = new BiomeEnvironment(5L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
try (BiomeEnvironment.Scope outer = engine.openBiomeEnvironmentScope(first)) {
assertSame(first.data(), engine.getData());
assertSame(first.dimension(), engine.getDimension());
assertSame(active.complex, engine.getComplex());
try (BiomeEnvironment.Scope inner = engine.openBiomeEnvironmentScope(second)) {
assertSame(second.data(), engine.getData());
assertThrows(IllegalStateException.class, outer::close);
CompletableFuture.runAsync(() -> assertThrows(IllegalStateException.class, inner::close))
.get(5, TimeUnit.SECONDS);
}
assertSame(first.data(), engine.getData());
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(
detachedBinding(engine, selected.runtime))) {
assertSame(selected.data, engine.getData());
assertSame(selected.dimension, engine.getDimension());
assertSame(selected.complex, engine.getComplex());
}
assertSame(first.data(), engine.getData());
}
assertSame(active.data, engine.getData());
assertSame(active.dimension, engine.getDimension());
}
private static IrisEngine engine(
GenerationRuntime generationRuntime,
EngineEffects effects,
@@ -549,6 +714,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
setField(engine, "lifecycleLock", new Object());
setField(engine, "generationHistoryRuntimeRouterLock", new Object());
setField(engine, "runtimeAssembly", new ThreadLocal<EngineRuntimeBuilder.RuntimeAssembly>());
setField(engine, "biomeEnvironmentScopes", new ThreadLocal<Object>());
setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState());
Set<GenerationRuntime> detached = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<GenerationRuntime, Boolean>()));
@@ -4,6 +4,7 @@ import art.arcane.iris.engine.framework.EngineMode;
import art.arcane.iris.engine.framework.EnginePlatformHooks;
import art.arcane.iris.engine.framework.EngineStage;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.history.FloatingBiomeOverlay;
import art.arcane.iris.engine.history.BoundaryColumnGeometry;
import art.arcane.iris.engine.history.TerrainBoundarySignatureStore;
import art.arcane.iris.engine.history.TransitionGenerationPlan;
@@ -12,6 +13,9 @@ import art.arcane.iris.engine.history.TerrainBoundarySignature;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
@@ -19,6 +23,7 @@ import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.lang.reflect.Field;
@@ -46,12 +51,42 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
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 final class ResolvedTerrainProviderTest {
@Test
public void cachedSpeculativeFloatingIdentityIsPublishedOnlyByActualGeneration() throws Exception {
Fixture fixture = new Fixture();
fixture.floatingIdentity = new FloatingBiomeOverlay.Identity("floating-child", "region");
TransitionGenerationPlan plan = mock(TransitionGenerationPlan.class);
when(plan.hasTransitionAtChunk(0, 0)).thenReturn(true);
when(plan.newEpochWeightAt(anyInt(), anyInt())).thenReturn(1D);
when(fixture.complex.getTransitionGenerationPlan()).thenReturn(plan);
fixture.provider.column(0, 0);
verify(fixture.router, never()).recordFloatingBiomes(anyInt(), anyInt(), any());
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.blockOrNull("minecraft:stone")).thenReturn(fixture.stone);
when(registries.blockOrNull("minecraft:air")).thenReturn(fixture.air);
when(registries.biome("minecraft:plains")).thenReturn(fixture.biome);
ChunkContext context = new ChunkContext(0, 0, fixture.complex, false, ChunkContext.PrefillPlan.NONE, null);
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
IrisContext.Scope ignored = IrisContext.open(fixture.engine, 5L, context)) {
platforms.when(IrisPlatforms::get).thenReturn(platform);
fixture.provider.generate(fixture.mode, 0, 0, Hunk.newArrayHunk(16, 16, 16),
Hunk.newArrayHunk(16, 16, 16), false, context);
}
assertEquals(Integer.valueOf(1), fixture.computations.get("0,0"));
assertEquals(fixture.floatingIdentity, context.getFloatingBiomes().volumeAt(0, 0, 0));
assertEquals(fixture.floatingIdentity, context.getFloatingBiomes().surfaceAt(0, 0));
verify(fixture.router).recordFloatingBiomes(0, 0, context.getFloatingBiomes());
}
@Test
public void columnRequestsAreDeterministicAcrossOrderAndNegativeCoordinates() {
Fixture first = new Fixture();
@@ -352,6 +387,7 @@ public final class ResolvedTerrainProviderTest {
private final ResolvedTerrainProvider provider;
private long lastSessionId;
private EngineStage terrainOverride;
private FloatingBiomeOverlay.Identity floatingIdentity;
@SuppressWarnings("unchecked")
private Fixture() {
@@ -411,6 +447,10 @@ public final class ResolvedTerrainProviderTest {
}
}
}
if (floatingIdentity != null) {
context.floatingBiomes(16).record(0, 0, 0, floatingIdentity);
context.floatingBiomes(16).record(0, 2, 0, floatingIdentity);
}
}
private static PlatformBlockState block(String key, boolean air) {
@@ -0,0 +1,117 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.MarkerSpawnScanner.PreparedMarkerSpawn;
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.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.math.RNG;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
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.when;
public class WorldEntitySpawnerBiomeTest {
@Test
public void loadingSurfaceDefinitionsDoNotConsumeInitialSpawnFlag() throws Exception {
Engine engine = mock(Engine.class);
IrisWorldManager manager = manager(engine);
WorldChunkMaintenance maintenance = manager.chunkMaintenance;
World world = mock(World.class);
Chunk chunk = mock(Chunk.class);
when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(2);
when(chunk.getZ()).thenReturn(-1);
BiomeEnvironment environment = environment();
when(engine.getSurfaceBiomeEnvironment(40, -8))
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
.thenReturn(environment);
IrisSettings settings = new IrisSettings();
settings.getWorld().setAmbientEntitySpawningSystem(true);
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class)) {
configured.when(IrisSettings::get).thenReturn(settings);
WorldEntitySpawner spawner = new WorldEntitySpawner(manager);
spawner.prepareInitialSpawn(chunk, List.of());
verify(maintenance, never()).raiseInitialSpawnMarkerFlag(any(), anyInt(), anyInt(), any());
spawner.prepareInitialSpawn(chunk, List.of());
verify(maintenance).raiseInitialSpawnMarkerFlag(eq(world), eq(2), eq(-1), any());
}
}
@Test
public void preparedInitialMarkersKeepDefinitionsAfterSavedQueryEviction() throws Exception {
Engine engine = mock(Engine.class);
IrisWorldManager manager = manager(engine);
World world = mock(World.class);
Chunk chunk = mock(Chunk.class);
when(chunk.getWorld()).thenReturn(world);
BiomeEnvironment environment = environment();
BiomeEnvironment.Scope scope = mock(BiomeEnvironment.Scope.class);
when(engine.openBiomeEnvironmentScope(environment)).thenReturn(scope);
IrisSpawner definition = mock(IrisSpawner.class);
IrisEntitySpawn entity = mock(IrisEntitySpawn.class);
IrisPosition position = new IrisPosition(3, 90, 4);
when(definition.getInitialSpawns()).thenReturn(new KList<>(entity));
when(definition.canSpawn(engine, 0, 0)).thenReturn(true);
when(entity.getRarity()).thenReturn(1);
when(entity.getReferenceSpawner()).thenReturn(definition);
when(entity.spawn(eq(engine), eq(position), any(RNG.class))).thenReturn(1);
KSet<IrisSpawner> definitions = new KSet<>();
definitions.add(definition);
PreparedMarkerSpawn marker = new PreparedMarkerSpawn(position, definitions, environment);
IrisSettings settings = new IrisSettings();
settings.getWorld().setAmbientEntitySpawningSystem(false);
try (MockedStatic<IrisSettings> configured = mockStatic(IrisSettings.class)) {
configured.when(IrisSettings::get).thenReturn(settings);
new WorldEntitySpawner(manager).prepareInitialSpawn(chunk, List.of(marker));
ArgumentCaptor<Runnable> callback = ArgumentCaptor.forClass(Runnable.class);
verify(manager.chunkMaintenance).raiseInitialSpawnMarkerFlag(eq(world), eq(0), eq(0), callback.capture());
doThrow(new SavedBiomeUnavailableException("Evicted", true)).when(engine)
.getBiomeEnvironment(anyInt(), anyInt(), anyInt());
callback.getValue().run();
verify(entity).spawn(eq(engine), eq(position), any(RNG.class));
verify(engine).openBiomeEnvironmentScope(environment);
verify(scope).close();
verify(engine, never()).getBiomeEnvironment(anyInt(), anyInt(), anyInt());
}
}
private static IrisWorldManager manager(Engine engine) throws Exception {
IrisWorldManager manager = mock(IrisWorldManager.class);
when(manager.getEngine()).thenReturn(engine);
Field field = IrisWorldManager.class.getDeclaredField("chunkMaintenance");
field.setAccessible(true);
field.set(manager, mock(WorldChunkMaintenance.class));
return manager;
}
private static BiomeEnvironment environment() {
return new BiomeEnvironment(3L, mock(IrisBiome.class), mock(IrisRegion.class),
mock(IrisDimension.class), mock(IrisData.class));
}
}
@@ -1,6 +1,25 @@
package art.arcane.iris.engine.framework;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
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.iris.platform.bukkit.BukkitWorldBinding;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -13,4 +32,35 @@ public class EnginePlayerTest {
assertFalse(EnginePlayer.needsSample(true, 55L, 82D));
assertTrue(EnginePlayer.needsSample(true, 56L, 82D));
}
@Test
public void loadingSavedBiomesRetryWithoutMovementAndKeepOneDefinitionContext() {
Engine engine = mock(Engine.class);
IrisWorld irisWorld = mock(IrisWorld.class);
World world = mock(World.class);
Player player = mock(Player.class);
when(engine.getWorld()).thenReturn(irisWorld);
when(irisWorld.minHeight()).thenReturn(-64);
when(player.getLocation()).thenReturn(new Location(world, 19, 80, -3));
BiomeEnvironment environment = new BiomeEnvironment(4L, mock(IrisBiome.class),
mock(IrisRegion.class), mock(IrisDimension.class), mock(IrisData.class));
when(engine.getBiomeEnvironment(19, 144, -3))
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
.thenReturn(environment);
try (MockedStatic<BukkitWorldBinding> binding = mockStatic(BukkitWorldBinding.class)) {
binding.when(() -> BukkitWorldBinding.world(irisWorld)).thenReturn(world);
EnginePlayer sampled = new EnginePlayer(engine, player);
assertTrue(sampled.sample());
assertNull(sampled.getLastLocation());
assertNull(sampled.getEnvironment());
assertFalse(sampled.sample());
assertSame(environment, sampled.getEnvironment());
assertSame(environment.biome(), sampled.getBiome());
assertSame(environment.region(), sampled.getRegion());
assertFalse(sampled.sample());
verify(engine, times(2)).getBiomeEnvironment(19, 144, -3);
}
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.IrisDatapackCompiler;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.datapack.v1217.DataFixerV1217;
import art.arcane.iris.engine.object.IrisDimension;
@@ -47,7 +48,8 @@ public class ArchivedRegistryCompilerTest {
String oldEpoch = history.activeEpoch().epochId();
history.stageUpdate(secondPack, second.fingerprint(), second.dimension(), second.registry(), 256);
history.promotePending(List.of());
new GenerationPackRepository(world).releaseArchivedPacks(history.manifest());
assertTrue(Files.isDirectory(history.paths().packRoot(oldEpoch)));
AtomicDirectoryPublisher.deleteTree(history.paths().packRoot(oldEpoch));
assertFalse(Files.exists(history.paths().packRoot(oldEpoch)));
List<File> packs = List.of(history.activePackRoot().toFile());
@@ -3,6 +3,7 @@ package art.arcane.iris.engine.history;
import art.arcane.iris.core.nms.container.BlockPos;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -50,6 +51,8 @@ public final class ArchivedSemanticQueryTest {
updated.stageUpdate(newPack, fingerprint(newPack), dimension, GenerationRegistryContract.empty(), 32);
updated.prepareCurrentGenerator(32);
GenerationHistory archived = GenerationHistory.open(world);
assertTrue(Files.exists(archived.paths().packRoot(oldEpoch)));
AtomicDirectoryPublisher.deleteTree(archived.paths().packRoot(oldEpoch));
assertFalse(Files.exists(archived.paths().packRoot(oldEpoch)));
IrisEngine engine = engine(archived);
@@ -0,0 +1,30 @@
package art.arcane.iris.engine.history;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
public class FloatingBiomeOverlayTest {
@Test
public void volumeFollowsTheQuartAnchorAndLastWriterWhileSurfaceUsesTheHighestWrite() {
FloatingBiomeOverlay overlay = new FloatingBiomeOverlay(16);
FloatingBiomeOverlay.Identity first = new FloatingBiomeOverlay.Identity("first", "region");
FloatingBiomeOverlay.Identity second = new FloatingBiomeOverlay.Identity("second", "region");
overlay.record(0, 8, 0, first);
overlay.record(0, 9, 0, second);
overlay.record(1, 8, 1, second);
assertEquals(first, overlay.volumeAt(3, 11, 3));
overlay.record(0, 8, 0, second);
assertEquals(second, overlay.volumeAt(3, 11, 3));
assertNull(overlay.volumeAt(4, 8, 0));
overlay.retainHighestSurfaces((x, z) -> x == 0 ? 9 : 12);
assertEquals(second, overlay.surfaceAt(0, 0));
assertEquals(9, overlay.surfaceYAt(0, 0));
assertNull(overlay.surfaceAt(1, 1));
assertEquals(-1, overlay.surfaceYAt(1, 1));
assertThrows(IllegalArgumentException.class, () -> overlay.record(16, 8, 0, first));
assertThrows(IllegalArgumentException.class, () -> overlay.volumeAt(0, 16, 0));
}
}
@@ -79,6 +79,33 @@ public class GenerationEpochContractFactoryTest {
assertEquals(164, contract.internalFluidHeight());
}
@Test
public void recordedEpochKeepsItsUpperTerrainFingerprintVersion() throws Exception {
File pack = temporary.newFolder("recorded-upper-pack");
Files.createDirectories(pack.toPath().resolve("objects"));
Files.writeString(pack.toPath().resolve("objects/.DS_Store"), "recorded metadata");
IrisData data = mock(IrisData.class);
IrisDimension dimension = dimension(IrisEnvironment.NORMAL).setUpperDimension("overworld");
dimension.setLoader(data);
when(data.getDataFolder()).thenReturn(pack);
GenerationEpoch.DimensionContract current = GenerationEpochContractFactory.create(
dimension, "overworld", "iris:overworld");
GenerationEpoch epoch = mock(GenerationEpoch.class);
when(epoch.dimensionContract()).thenReturn(current);
when(epoch.packFingerprintVersion()).thenReturn(1);
GenerationEpoch.DimensionContract recorded = GenerationEpochContractFactory.createForEpoch(
dimension, "iris:overworld", epoch);
assertEquals(GenerationPackFingerprint.compute(pack.toPath(), 1), recorded.upperTerrainPackFingerprint());
assertNotEquals(current.upperTerrainPackFingerprint(), recorded.upperTerrainPackFingerprint());
when(epoch.dimensionContract()).thenReturn(recorded);
assertEquals(recorded, GenerationEpochContractFactory.createForEpoch(dimension, "iris:overworld", epoch));
Files.writeString(pack.toPath().resolve("objects/.DS_Store"), "changed metadata");
assertNotEquals(recorded, GenerationEpochContractFactory.createForEpoch(dimension, "iris:overworld", epoch));
assertEquals(current, GenerationEpochContractFactory.create(dimension, "overworld", "iris:overworld"));
}
@Test
public void upperTerrainMustBeLocalAndPackIdentityDoesNotChangeItsLayout() throws Exception {
File pack = temporary.newFolder("upper-pack");
@@ -127,7 +154,7 @@ public class GenerationEpochContractFactoryTest {
dimension,
"overworld",
"iris:overworld"
)
)
);
}
@@ -1,7 +1,10 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.util.common.data.B;
@@ -40,6 +43,7 @@ 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.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -177,10 +181,14 @@ public final class GenerationHistoryRuntimeRouterTest {
}
@Test
public void historicalRoutesWorkAfterArchivedPacksAreReleased() throws Exception {
public void historicalTerrainRoutesWorkWhenFrozenPackFixturesAreMissing() throws Exception {
Path world = temporaryFolder.newFolder("router-archived-world").toPath();
GenerationHistory history = createThreeActivationHistory(world, "router-archived");
new GenerationPackRepository(world).releaseArchivedPacks(history.manifest());
for (long activationId : List.of(1L, 2L)) {
Path archivedPack = history.paths().packRoot(history.manifest().activation(activationId).orElseThrow().epochId());
assertTrue(Files.isDirectory(archivedPack));
AtomicDirectoryPublisher.deleteTree(archivedPack);
}
assertFalse(Files.exists(history.paths().packRoot(history.manifest().activation(1L).orElseThrow().epochId())));
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
@@ -568,6 +576,18 @@ public final class GenerationHistoryRuntimeRouterTest {
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
IrisBiome biome = new IrisBiome();
biome.setLoadKey("alpha");
IrisRegion region = new IrisRegion();
region.setLoadKey("alpha");
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(384);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(biome);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(biome);
when(engine.getBiome(anyInt(), anyInt(), anyInt())).thenReturn(biome);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenReturn(biome);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(region);
when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenReturn(region);
EngineMantle engineMantle = mock(EngineMantle.class);
@SuppressWarnings("unchecked")
Mantle<Matter> mantle = mock(Mantle.class);
@@ -596,6 +616,7 @@ public final class GenerationHistoryRuntimeRouterTest {
ChunkGenerationSemantics semantics = history.semantics(4, -3).orElseThrow();
assertTrue(semantics.sealed());
assertEquals(1L, semantics.activationId());
assertEquals("alpha", history.savedBiomes().get(4, -3).orElseThrow().biomeAt(0, 0, 0).biomeKey());
router.close();
}
@@ -669,7 +669,7 @@ public final class GenerationHistoryTest {
}
@Test
public void coldRecoveryKeepsStoredClaimsAndHistoricalMetadataButReleasesOldPacks() throws Exception {
public void coldRecoveryKeepsStoredClaimsHistoricalMetadataAndFrozenPacks() throws Exception {
Path world = temporaryFolder.newFolder("cold-recovery-world").toPath();
Path packA = createPack("cold-recovery-a", "alpha");
Path packB = createPack("cold-recovery-b", "beta");
@@ -695,7 +695,9 @@ public final class GenerationHistoryTest {
assertTrue(recovered.semantics(0, 0).isPresent());
assertTrue(recovered.semantics(1, 0).isPresent());
assertTrue(recovered.semantics(2, 0).isEmpty());
assertFalse(Files.exists(recovered.paths().packRoot(oldEpoch)));
assertTrue(Files.isDirectory(recovered.paths().packRoot(oldEpoch)));
assertEquals(GenerationPackFingerprint.compute(packA, GenerationPackFingerprint.CURRENT_VERSION),
GenerationPackFingerprint.compute(recovered.paths().packRoot(oldEpoch), GenerationPackFingerprint.CURRENT_VERSION));
assertTrue(Files.isDirectory(recovered.activePackRoot()));
assertTrue(Files.isDirectory(packA));
GenerationHistory reopened = GenerationHistory.open(world);
@@ -30,10 +30,46 @@ public class GenerationPackFingerprintTest {
String fingerprint = GenerationPackFingerprint.compute(
pack,
GenerationPackFingerprint.CURRENT_VERSION
1
);
assertEquals("ad7d39c2b95bd3001248778911ce6bbd6b4cbdcece28145838c2b923a0bf73b3", fingerprint);
Files.writeString(pack.resolve("objects/.DS_Store"), "finder");
assertEquals("b36c630e300a9939fabe3fbd02f546f2a76b31d2ebccbf9106a13fb7d8b8eb98",
GenerationPackFingerprint.compute(pack, 1));
assertEquals(fingerprint, GenerationPackFingerprint.compute(pack, 2));
}
@Test
public void versionTwoIgnoresFinderMetadataAtEveryDepth() throws Exception {
Path source = temporaryFolder.newFolder("metadata-source").toPath();
Files.createDirectories(source.resolve("objects/trees"));
Files.writeString(source.resolve("objects/trees/oak.json"), "{}");
Files.writeString(source.resolve("objects/.DS_Store"), "source metadata");
Path target = temporaryFolder.getRoot().toPath().resolve("metadata-copy");
GenerationPackRepository.copyPackTree(source, target);
String expected = GenerationPackFingerprint.compute(source, 2);
Files.writeString(target.resolve(".DS_Store"), "root metadata");
Files.writeString(target.resolve("objects/.DS_Store"), "destination metadata");
Files.writeString(target.resolve("objects/trees/.DS_Store"), "nested metadata");
assertEquals(expected, GenerationPackFingerprint.compute(target, 2));
assertNotEquals(GenerationPackFingerprint.compute(source, 1),
GenerationPackFingerprint.compute(target, 1));
Files.writeString(target.resolve("objects/trees/oak.json"), "{\"changed\":true}");
assertNotEquals(expected, GenerationPackFingerprint.compute(target, 2));
}
@Test
public void versionTwoStillIncludesOtherNestedHiddenResources() throws Exception {
Path pack = temporaryFolder.newFolder("nested-hidden-pack").toPath();
Files.createDirectories(pack.resolve("objects"));
Files.writeString(pack.resolve("objects/.hidden"), "first");
String first = GenerationPackFingerprint.compute(pack, 2);
Files.writeString(pack.resolve("objects/.hidden"), "second");
assertNotEquals(first, GenerationPackFingerprint.compute(pack, 2));
}
@Test
@@ -51,7 +87,7 @@ public class GenerationPackFingerprintTest {
@Test
public void unsupportedVersionsAndSymbolicLinksFailClosed() throws Exception {
Path pack = temporaryFolder.newFolder("unsafe-pack").toPath();
assertThrows(IOException.class, () -> GenerationPackFingerprint.compute(pack, 2));
assertThrows(IOException.class, () -> GenerationPackFingerprint.compute(pack, 3));
Path target = temporaryFolder.newFile("outside.json").toPath();
try {
@@ -4,6 +4,7 @@ import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Files;
@@ -12,11 +13,13 @@ import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mockStatic;
public class GenerationPackRepositoryTest {
@Rule
@@ -69,6 +72,50 @@ public class GenerationPackRepositoryTest {
));
}
@Test
public void finderMetadataCreatedDuringPublicationDoesNotChangeVersionTwoIdentity() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("finder-world").toPath();
Path source = createPack("finder-source", "{}");
String fingerprint = fingerprint(source);
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
String epochId = digest('b');
AtomicBoolean injected = new AtomicBoolean();
try (MockedStatic<GenerationPackFingerprint> ignored = mockStatic(GenerationPackFingerprint.class, invocation -> {
if (invocation.getMethod().getName().equals("compute")) {
Path pack = invocation.getArgument(0);
if (pack.getFileName().toString().startsWith(".pack-")) {
Files.writeString(pack.resolve(".DS_Store"), "root metadata");
Files.writeString(pack.resolve("dimensions/.DS_Store"), "nested metadata");
injected.set(true);
}
}
return invocation.callRealMethod();
})) {
Path published = repository.publish(epochId, fingerprint, 2, source);
assertTrue(injected.get());
assertTrue(Files.isRegularFile(published.resolve("dimensions/.DS_Store")));
assertEquals(published, repository.requireExactPack(epochId, fingerprint, 2));
Files.writeString(published.resolve("dimensions/main.json"), "{\"changed\":true}");
assertThrows(IOException.class, () -> repository.requireExactPack(epochId, fingerprint, 2));
}
}
@Test
public void versionOnePublicationsKeepTheirOriginalMetadataContract() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("version-one-world").toPath();
Path source = createPack("version-one-source", "{}");
Files.writeString(source.resolve("dimensions/.DS_Store"), "original metadata");
String fingerprint = GenerationPackFingerprint.compute(source, 1);
GenerationPackRepository repository = new GenerationPackRepository(dimensionRoot);
String epochId = digest('a');
Path published = repository.publish(epochId, fingerprint, 1, source);
assertEquals(published, repository.requireExactPack(epochId, fingerprint, 1));
Files.writeString(published.resolve("dimensions/.DS_Store"), "changed metadata");
assertThrows(IOException.class, () -> repository.requireExactPack(epochId, fingerprint, 1));
}
@Test
public void rejectsNestedSymbolicLinks() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("linked-world").toPath();
@@ -149,7 +196,7 @@ public class GenerationPackRepositoryTest {
}
@Test
public void releasesOnlyArchivedPacksAndCanRepublishTheirSnapshot() throws Exception {
public void publicationAndActivationRetainEveryFrozenPack() throws Exception {
Path dimensionRoot = temporaryFolder.newFolder("retention-world").toPath();
Path firstSource = createPack("retention-first", "{\"version\":1}");
Path secondSource = createPack("retention-second", "{\"version\":2}");
@@ -167,18 +214,17 @@ public class GenerationPackRepositoryTest {
repository.publish(pending.epochId(), pending.packFingerprint(), pending.packFingerprintVersion(), pendingSource);
store.preparePendingActivation(pending, 256);
repository.releaseArchivedPacks(store.manifest());
repository.releaseArchivedPacks(store.manifest());
assertFalse(Files.exists(repository.packRoot(first.epochId())));
assertTrue(Files.isDirectory(repository.packRoot(first.epochId())));
assertEquals(first.packFingerprint(), fingerprint(repository.packRoot(first.epochId())));
assertTrue(Files.isRegularFile(repository.epochRoot(first.epochId()).resolve("epoch.json")));
assertTrue(Files.isDirectory(repository.packRoot(second.epochId())));
assertTrue(Files.isDirectory(repository.packRoot(pending.epochId())));
assertTrue(Files.isRegularFile(firstSource.resolve("dimensions/main.json")));
assertEquals(store.manifest(), GenerationHistoryStore.open(repository.generationRoot()).manifest());
Path republished = repository.publish(first.epochId(), first.packFingerprint(), first.packFingerprintVersion(), firstSource);
assertEquals(Files.readString(firstSource.resolve("dimensions/main.json")),
Files.readString(republished.resolve("dimensions/main.json")));
Files.writeString(firstSource.resolve("dimensions/main.json"), "{\"version\":99}");
assertEquals("{\"version\":1}", Files.readString(repository.packRoot(first.epochId()).resolve("dimensions/main.json")));
assertEquals(repository.packRoot(first.epochId()), repository.requireExactPack(
first.epochId(), first.packFingerprint(), first.packFingerprintVersion()));
}
private static GenerationEpoch epoch(String fingerprint) {
@@ -0,0 +1,95 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.Map;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
public class NativeBiomeSpawnSelectionTest {
@Test
public void savedSelectionUsesTheActualPhysicalHolderAndOwningRuntime() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.biomes()).thenReturn(saved);
NativeBiomeSpawnSelection retained = new NativeBiomeSpawnSelection(
NativeBiomeSpawnSelection.Mode.RETAINED, "minecraft:plains");
when(saved.nativeSpawnSelection(-17, 62, 31, "iris:old-physical")).thenReturn(retained);
assertEquals(retained, NativeBiomeSpawnSelection.at(engine, -17, 62, 31, "iris:old-physical"));
verify(saved).nativeSpawnSelection(-17, 62, 31, "iris:old-physical");
}
@Test
public void generationScopesUseCurrentPredictionWithoutReadingSavedBiomes() {
IrisEngine engine = mock(IrisEngine.class);
when(engine.hasGenerationRuntimeScope()).thenReturn(true);
assertEquals(NativeBiomeSpawnSelection.Mode.CURRENT,
NativeBiomeSpawnSelection.at(engine, 0, 64, 0, "iris:new-physical").mode());
verify(engine, never()).getGenerationHistoryRuntimeRouter();
}
@Test
public void loadingSkipsTheAttemptAndUnsupportedNeverUsesCurrentInheritance() {
IrisEngine engine = mock(IrisEngine.class);
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
SavedBiomeRuntime saved = mock(SavedBiomeRuntime.class);
when(engine.getGenerationHistoryRuntimeRouter()).thenReturn(Optional.of(router));
when(router.biomes()).thenReturn(saved);
when(saved.nativeSpawnSelection(0, 64, 0, "iris:old"))
.thenThrow(new SavedBiomeUnavailableException("loading", true))
.thenThrow(new SavedBiomeUnavailableException("unsupported", false));
assertEquals(NativeBiomeSpawnSelection.Mode.LOADING,
NativeBiomeSpawnSelection.at(engine, 0, 64, 0, "iris:old").mode());
assertEquals(NativeBiomeSpawnSelection.Mode.NONE,
NativeBiomeSpawnSelection.at(engine, 0, 64, 0, "iris:old").mode());
}
@Test
public void retainedPhysicalMappingsAreIndependentOfLaterParentDerivativeChanges() {
IrisData previous = retainedData("iris:same-physical", "minecraft:plains");
IrisData current = retainedData("iris:same-physical", "minecraft:desert");
Map<String, String> oldMappings = NativeBiomeSpawnSelection.retainedDerivatives(previous);
Map<String, String> newMappings = NativeBiomeSpawnSelection.retainedDerivatives(current);
assertEquals("minecraft:plains", oldMappings.get("iris:same-physical"));
assertEquals("minecraft:desert", newMappings.get("iris:same-physical"));
assertEquals(null, oldMappings.get("iris:unrelated-physical"));
assertTrue(oldMappings.keySet().contains("iris:same-physical"));
}
private static IrisData retainedData(String physicalKey, String vanillaDerivative) {
IrisData data = mock(IrisData.class, RETURNS_DEEP_STUBS);
IrisDimension dimension = mock(IrisDimension.class);
IrisBiome biome = mock(IrisBiome.class);
IrisBiomeCustom custom = new IrisBiomeCustom();
custom.setId("custom");
when(data.getDimensionLoader().getPossibleKeys()).thenReturn(new String[]{"main"});
when(data.getDimensionLoader().load("main")).thenReturn(dimension);
when(dimension.getReachableBiomes(any(DataProvider.class))).thenReturn(new KList<IrisBiome>().qadd(biome));
when(biome.isCustom()).thenReturn(true);
when(biome.getCustomDerivitives()).thenReturn(new KList<IrisBiomeCustom>().qadd(custom));
when(biome.getVanillaDerivativeKey()).thenReturn(vanillaDerivative);
when(data.customBiomeResourceKey(dimension, custom)).thenReturn(physicalKey);
return data;
}
}
@@ -0,0 +1,106 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class SavedBiomeCaptureTest {
@Test
public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
IrisBiome host = biome("host");
IrisBiome child = biome("floating-child");
host.setDerivative("minecraft:plains");
child.setDerivative("minecraft:plains");
IrisRegion region = region("host-region");
when(stage.activation()).thenReturn(GenerationActivation.initial("a".repeat(64), 1L));
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(16);
when(engine.getHeight(anyInt(), anyInt())).thenReturn(2);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(region);
when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenReturn(region);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(host);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(host);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenReturn(host);
FloatingBiomeOverlay floating = new FloatingBiomeOverlay(16);
FloatingBiomeOverlay.Identity childIdentity = new FloatingBiomeOverlay.Identity(child.getLoadKey(), region.getLoadKey());
floating.record(0, 8, 0, childIdentity);
floating.record(0, 9, 0, childIdentity);
floating.record(4, 8, 0, childIdentity);
floating.retainHighestSurfaces((x, z) -> x == 0 ? 9 : 12);
SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage, historical, floating);
assertEquals(host.getVanillaDerivativeKey(), child.getVanillaDerivativeKey());
assertEquals("floating-child", chunk.surfaceAt(0, 0).biomeKey());
assertEquals("host", chunk.surfaceAt(4, 0).biomeKey());
assertEquals("floating-child", chunk.biomeAt(0, -56, 0).biomeKey());
assertEquals("floating-child", chunk.biomeAt(3, -53, 3).biomeKey());
assertEquals("host", chunk.biomeAt(0, -52, 0).biomeKey());
assertEquals("host", chunk.caveBaseAt(0, 0).biomeKey());
assertEquals(1L, chunk.biomeAt(0, -56, 0).activationId());
verifyNoInteractions(historical);
}
@Test
public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
IrisBiome left = biome("left");
IrisBiome right = biome("right");
IrisBiome cave = biome("base-cave");
IrisBiome lower = biome("lower-volume");
IrisBiome upper = biome("upper-volume");
IrisRegion surfaceRegion = region("surface-region");
IrisRegion caveRegion = region("volume-region");
when(stage.chunkX()).thenReturn(-1);
when(stage.chunkZ()).thenReturn(2);
when(stage.activation()).thenReturn(GenerationActivation.initial("a".repeat(64), 1L));
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(16);
when(engine.getComplex().getTransitionGenerationPlan()).thenReturn(null);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(surfaceRegion);
when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenReturn(caveRegion);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenAnswer(call -> (int) call.getArgument(0) == -15 ? right : left);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(cave);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenAnswer(call -> (int) call.getArgument(1) < 8 ? lower : upper);
SavedBiomeChunk chunk = SavedBiomeCapture.capture(engine, stage, historical, null);
assertEquals(new SavedBiomeChunk.Header(-1, 2, 1L, -64, 16), chunk.header());
assertEquals("left", chunk.surfaceAt(0, 0).biomeKey());
assertEquals("right", chunk.surfaceAt(1, 0).biomeKey());
assertEquals("surface-region", chunk.surfaceAt(1, 0).regionKey());
assertEquals("base-cave", chunk.caveBaseAt(1, 0).biomeKey());
assertEquals("lower-volume", chunk.biomeAt(1, -57, 0).biomeKey());
assertEquals("upper-volume", chunk.biomeAt(1, -56, 0).biomeKey());
assertEquals("volume-region", chunk.biomeAt(1, -56, 0).regionKey());
assertEquals(2, chunk.column(0, 0).vertical().size());
assertEquals(chunk.column(0, 0).vertical(), chunk.column(3, 3).vertical());
verifyNoInteractions(historical);
}
private static IrisBiome biome(String key) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
return biome;
}
private static IrisRegion region(String key) {
IrisRegion region = new IrisRegion();
region.setLoadKey(key);
return region;
}
}
@@ -0,0 +1,219 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingEntry;
import art.arcane.iris.engine.object.IrisDimensionStack;
import art.arcane.iris.engine.object.IrisFloatingChildBiomes;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class SavedBiomeRecoveryTest {
private static final String BIOME = "transition-demo";
private static final String REGION = "transition-demo";
private static final int CHUNK_X = -2;
private static final int CHUNK_Z = 3;
private final GenerationHistory history = mock(GenerationHistory.class);
private final IrisData data = mock(IrisData.class, RETURNS_DEEP_STUBS);
private final IrisDimension dimension = new IrisDimension();
private final IrisRegion region = new IrisRegion();
private final IrisBiome biome = new IrisBiome();
private final SavedBiomeRecovery.Input input =
new SavedBiomeRecovery.Input(history, CHUNK_X, CHUNK_Z, data, dimension);
@Before
public void prepareSingletonPack() {
dimension.setLoadKey("overworld");
dimension.setRegions(new KList<String>().qadd(REGION));
region.setLoadKey(REGION);
region.setLandBiomes(new KList<String>().qadd(BIOME));
region.setSeaBiomes(new KList<String>().qadd(BIOME));
region.setShoreBiomes(new KList<String>().qadd(BIOME));
region.setCaveBiomes(new KList<String>().qadd(BIOME));
biome.setLoadKey(BIOME);
when(data.getRegionLoader().load(REGION)).thenReturn(region);
when(data.getBiomeLoader().load(BIOME)).thenReturn(biome);
when(history.resolveActivation(CHUNK_X, CHUNK_Z))
.thenReturn(GenerationActivation.initial("a".repeat(64), 1L));
GenerationEpoch epoch = mock(GenerationEpoch.class);
GenerationEpoch.DimensionContract contract = mock(GenerationEpoch.DimensionContract.class);
when(contract.dimensionKey()).thenReturn("overworld");
when(contract.minHeight()).thenReturn(-64);
when(contract.maxHeight()).thenReturn(320);
when(contract.height()).thenReturn(384);
when(epoch.dimensionContract()).thenReturn(contract);
when(history.resolveEpoch(CHUNK_X, CHUNK_Z)).thenReturn(epoch);
record(semantics(1L).seal().build());
}
@Test
public void recoversAllColumnsAndEntireHeightFromTheOwningActivation() throws IOException {
SavedBiomeChunk recovered = SavedBiomeRecovery.recover(input).orElseThrow();
SavedBiomeChunk.Cell expected = new SavedBiomeChunk.Cell(1L, BIOME, REGION);
assertEquals(new SavedBiomeChunk.Header(CHUNK_X, CHUNK_Z, 1L, -64, 384), recovered.header());
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
assertEquals(expected, recovered.surfaceAt(x, z));
assertEquals(expected, recovered.caveBaseAt(x, z));
assertEquals(expected, recovered.biomeAt(x, -64, z));
assertEquals(expected, recovered.biomeAt(x, 319, z));
assertEquals(1, recovered.column(x, z).vertical().size());
}
}
}
@Test
public void rejectsMissingOrUnsealedRecordedFacts() throws IOException {
when(history.semantics(CHUNK_X, CHUNK_Z)).thenReturn(Optional.empty());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
record(semantics(1L).build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsMultipleSurfaceOrRegionIdentities() throws IOException {
record(semantics(1L).addSurfaceBiome("other").seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
record(semantics(1L).addRegion("other").seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsInconsistentOwnershipAndDifferentCaveIdentity() throws IOException {
record(semantics(2L).seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
record(semantics(1L).addCaveBiome("other").seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void acceptsRecordedCavesWithTheSameIdentity() throws IOException {
record(semantics(1L).addCaveBiome(BIOME).seal().build());
assertTrue(SavedBiomeRecovery.recover(input).isPresent());
}
@Test
public void rejectsMissingDefinitions() throws IOException {
when(data.getBiomeLoader().load(BIOME)).thenReturn(null);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
when(data.getBiomeLoader().load(BIOME)).thenReturn(biome);
when(data.getRegionLoader().load(REGION)).thenReturn(null);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsPoolsWithUnrecordedChoicesOrNoVerticalChoice() throws IOException {
region.getCaveBiomes().add("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
region.getCaveBiomes().clear();
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
region.getCaveBiomes().add(BIOME);
region.getSeaBiomes().add("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsChildrenAndFloatingBiomes() throws IOException {
biome.getChildren().add("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
biome.getChildren().clear();
biome.getFloatingChildBiomes().add(new IrisFloatingChildBiomes());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsStackedUpperAndCarvingOverrides() throws IOException {
dimension.setDimensionStack(new IrisDimensionStack());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setDimensionStack(null);
dimension.setUpperDimension("upper");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setUpperDimension("none");
dimension.getCarving().add(new IrisDimensionCarvingEntry());
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsDifferingRiverOverridesAtEachScope() throws IOException {
IrisRiverPolicy policy = new IrisRiverPolicy().setFloodedCaveBiomes(new KList<String>().qadd("other"));
dimension.setRiverPolicy(policy);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setRiverPolicy(null);
region.setRiverPolicy(policy);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
region.setRiverPolicy(null);
biome.setRiverPolicy(policy);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsDifferentDimensionAndFocusSelectors() throws IOException {
dimension.setLoadKey("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setLoadKey("overworld");
dimension.setFocus("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
dimension.setFocus("");
dimension.getRegions().add("other");
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
}
@Test
public void rejectsOneBlendedColumnEvenWhenAllOtherColumnsAreNew() throws IOException {
TransitionGenerationPlan transition = nextActivation();
when(transition.newEpochWeightAt(-17, 63)).thenReturn(0.999D);
assertTrue(SavedBiomeRecovery.recover(input).isEmpty());
verify(transition).newEpochWeightAt(-17, 63);
}
@Test
public void retainsLaterActivationIdentityForUnblendedChunks() throws IOException {
nextActivation();
SavedBiomeChunk recovered = SavedBiomeRecovery.recover(input).orElseThrow();
assertEquals(2L, recovered.activationId());
assertEquals(2L, recovered.biomeAt(15, 319, 15).activationId());
}
@Test
public void propagatesUnreadableTransitionEvidence() throws IOException {
nextActivation();
when(history.transitionPlan(2L)).thenThrow(new IOException("Unreadable frozen transition"));
assertThrows(IOException.class, () -> SavedBiomeRecovery.recover(input));
}
private TransitionGenerationPlan nextActivation() throws IOException {
when(history.resolveActivation(CHUNK_X, CHUNK_Z))
.thenReturn(GenerationActivation.next(2L, "b".repeat(64), 1L, 2L, 64));
record(semantics(2L).seal().build());
TransitionGenerationPlan transition = mock(TransitionGenerationPlan.class);
when(transition.newEpochWeightAt(anyInt(), anyInt())).thenReturn(1D);
when(history.transitionPlan(2L)).thenReturn(transition);
return transition;
}
private void record(ChunkGenerationSemantics semantics) {
when(history.semantics(CHUNK_X, CHUNK_Z)).thenReturn(Optional.of(semantics));
}
private static ChunkGenerationSemantics.Builder semantics(long activationId) {
return ChunkGenerationSemantics.builder(CHUNK_X, CHUNK_Z, activationId)
.addSurfaceBiome(BIOME).addRegion(REGION);
}
}
@@ -0,0 +1,252 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SavedBiomeRuntimeTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final IrisEngine engine = mock(IrisEngine.class);
private final GenerationHistory history = mock(GenerationHistory.class);
private final SavedBiomeStore store = mock(SavedBiomeStore.class);
private IrisSettings previousSettings;
@Before
public void prepareServices() throws Exception {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
when(history.savedBiomes()).thenReturn(store);
when(history.paths()).thenReturn(GenerationHistoryPaths.forDimension(temporaryFolder.newFolder().toPath()));
String epochId = "a".repeat(64);
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationEpoch epoch = mock(GenerationEpoch.class);
when(history.manifest()).thenReturn(manifest);
when(manifest.activation(1L)).thenReturn(Optional.of(GenerationActivation.initial(epochId, 1L)));
when(manifest.activation(2L)).thenReturn(Optional.of(GenerationActivation.next(2L, epochId, 1L, 2L, 64)));
when(manifest.epoch(epochId)).thenReturn(Optional.of(epoch));
when(epoch.epochId()).thenReturn(epochId);
}
@After
public void restoreServices() {
IrisServices.clear();
IrisSettings.settings = previousSettings;
}
@Test
public void everyCallerReceivesLoadingWithoutWaitingForDisk() throws Exception {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
when(store.get(0, 0)).thenAnswer(invocation -> {
entered.countDown();
assertTrue(release.await(5L, TimeUnit.SECONDS));
return Optional.of(unresolvedChunk(1));
});
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
try (ExecutorService caller = Executors.newSingleThreadExecutor()) {
Future<SavedBiomeUnavailableException> response = caller.submit(() -> assertThrows(
SavedBiomeUnavailableException.class, () -> runtime.resolve(0, 0, 0, true)));
assertTrue(entered.await(5L, TimeUnit.SECONDS));
assertTrue(response.get(1L, TimeUnit.SECONDS).isLoading());
} finally {
release.countDown();
runtime.close();
}
}
@Test
public void chunkReadinessDoesNotRequireEveryCellToHaveAResolvedIdentity() throws Exception {
when(store.get(0, 0)).thenReturn(Optional.of(unresolvedChunk(1)));
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.prepareChunk(0, 0)).isLoading());
awaitIdle(runtime);
runtime.prepareChunk(0, 0);
SavedBiomeUnavailableException unavailable = assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(0, 0, 0, true));
assertEquals(false, unavailable.isLoading());
}
}
@Test
public void closeWaitsForInvalidatedWorkersAndRestoresInterruptStatus() throws Exception {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
SavedBiomeChunk chunk = unresolvedChunk(1);
when(store.get(0, 0)).thenAnswer(invocation -> {
entered.countDown();
assertTrue(release.await(5L, TimeUnit.SECONDS));
return Optional.of(chunk);
});
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(0, 0, 0, true)).isLoading());
assertTrue(entered.await(5L, TimeUnit.SECONDS));
runtime.capture(chunk);
assertEquals(1, runtime.pendingQueryCount());
try (ExecutorService closer = Executors.newSingleThreadExecutor()) {
Future<Boolean> closed = closer.submit(() -> {
Thread.currentThread().interrupt();
runtime.close();
return Thread.currentThread().isInterrupted();
});
try {
assertThrows(TimeoutException.class, () -> closed.get(100L, TimeUnit.MILLISECONDS));
} finally {
release.countDown();
}
assertTrue(closed.get(5L, TimeUnit.SECONDS));
assertEquals(0, runtime.pendingQueryCount());
assertEquals(0, runtime.cachedQueryCount());
} finally {
release.countDown();
runtime.close();
}
}
@Test
public void completedSnapshotCacheRespectsItsByteBudgetAndNegativeCacheIsBounded() throws Exception {
when(store.get(anyInt(), anyInt())).thenReturn(Optional.of(unresolvedChunk(16)));
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
for (int chunkX = 0; chunkX < 110; chunkX++) {
int blockX = chunkX * 16;
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(blockX, 0, 0, true)).isLoading());
awaitIdle(runtime);
assertTrue(runtime.cachedQueryBytes() <= SavedBiomeChunk.MAXIMUM_ESTIMATED_BYTES);
}
assertTrue(runtime.cachedQueryCount() < 110);
}
when(store.get(anyInt(), anyInt())).thenReturn(Optional.empty());
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(true);
when(history.semantics(anyInt(), anyInt())).thenReturn(Optional.empty());
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
for (int chunkX = 0; chunkX < 150; chunkX++) {
int blockX = chunkX * 16;
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(blockX, 0, 0, true)).isLoading());
awaitIdle(runtime);
assertEquals(Optional.empty(), runtime.resolve(blockX, 0, 0, true));
assertTrue(runtime.cachedQueryCount() <= 128);
}
}
}
@Test
public void preparedEnvironmentSurvivesLoaderEvictionWithoutReadingPackFilesAgain() throws Exception {
String epochId = "a".repeat(64);
Path pack = history.paths().packRoot(epochId);
Files.createDirectories(pack.resolve("dimensions"));
Files.createDirectories(pack.resolve("biomes"));
Files.createDirectories(pack.resolve("regions"));
Files.writeString(pack.resolve("dimensions/main.json"), "{}");
Files.writeString(pack.resolve("biomes/forest.json"), "{}");
Files.writeString(pack.resolve("regions/main.json"), "{}");
GenerationManifest manifest = mock(GenerationManifest.class);
GenerationEpoch epoch = mock(GenerationEpoch.class);
GenerationEpoch.DimensionContract contract = mock(GenerationEpoch.DimensionContract.class);
when(history.manifest()).thenReturn(manifest);
when(history.packRoot(1L)).thenReturn(pack);
when(history.packRoot(2L)).thenReturn(pack);
when(manifest.activation(1L)).thenReturn(Optional.of(GenerationActivation.initial(epochId, 1L)));
when(manifest.activation(2L)).thenReturn(Optional.of(GenerationActivation.next(2L, epochId, 1L, 2L, 64)));
when(manifest.epoch(epochId)).thenReturn(Optional.of(epoch));
when(epoch.epochId()).thenReturn(epochId);
when(epoch.registryContract()).thenReturn(GenerationRegistryContract.empty());
when(epoch.dimensionContract()).thenReturn(contract);
when(contract.dimensionKey()).thenReturn("main");
SavedBiomeChunk.Cell cell = new SavedBiomeChunk.Cell(1L, "forest", "main");
SavedBiomeChunk.Cell repeatedActivation = new SavedBiomeChunk.Cell(2L, "forest", "main");
SavedBiomeChunk.Cell removed = new SavedBiomeChunk.Cell(2L, "removed", "main");
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(0, 0, 2L, -64, 384));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
SavedBiomeChunk.Cell selected = x < 8 ? cell : x < 12 ? repeatedActivation : removed;
builder.column(x, z, new SavedBiomeChunk.Column(selected, cell,
List.of(new SavedBiomeChunk.Span(-64, 320, selected))));
}
}
when(store.get(0, 0)).thenReturn(Optional.of(builder.build()));
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(0, 0, 0, true)).isLoading());
awaitIdle(runtime);
runtime.prepareChunk(0, 0);
BiomeEnvironment environment = runtime.resolve(0, 0, 0, true).orElseThrow();
BiomeEnvironment repeated = runtime.resolve(8, 0, 0, true).orElseThrow();
assertEquals(2L, repeated.activationId());
assertSame(environment.data(), repeated.data());
SavedBiomeUnavailableException missing = assertThrows(SavedBiomeUnavailableException.class,
() -> runtime.resolve(12, 0, 0, true));
assertEquals(false, missing.isLoading());
assertTrue(missing.getMessage().contains("removed"));
assertSame(environment, runtime.resolveCaveBase(12, 0).orElseThrow());
environment.data().getBiomeLoader().unload("forest");
environment.data().getRegionLoader().unload("main");
Files.delete(pack.resolve("biomes/forest.json"));
Files.delete(pack.resolve("regions/main.json"));
assertSame(environment, runtime.resolve(0, 0, 0, true).orElseThrow());
assertEquals("forest", environment.biome().getLoadKey());
}
}
private static void awaitIdle(SavedBiomeRuntime runtime) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (runtime.pendingQueryCount() != 0 && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
assertEquals(0, runtime.pendingQueryCount());
}
private static SavedBiomeChunk unresolvedChunk(int runCount) {
SavedBiomeChunk.Cell first = SavedBiomeChunk.Cell.unresolved(1L);
SavedBiomeChunk.Cell second = SavedBiomeChunk.Cell.unresolved(2L);
ArrayList<SavedBiomeChunk.Span> spans = new ArrayList<>(runCount);
for (int index = 0; index < runCount; index++) {
int start = -64 + index * 384 / runCount;
int end = -64 + (index + 1) * 384 / runCount;
spans.add(new SavedBiomeChunk.Span(start, end, (index & 1) == 0 ? first : second));
}
return chunk(new SavedBiomeChunk.Column(first, second, spans));
}
private static SavedBiomeChunk chunk(SavedBiomeChunk.Column column) {
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(0, 0, 2L, -64, 384));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
builder.column(x, z, column);
}
}
return builder.build();
}
}
@@ -0,0 +1,389 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.CRC32;
import java.util.zip.DeflaterOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class SavedBiomeStoreTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void preservesEveryColumnHeightRegionAndOwningActivationAcrossReload() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeChunk original = chunk(-33, 31, 8L);
SavedBiomeStore writer = SavedBiomeStore.open(root);
assertTrue(writer.claimAndPersist(original));
assertFalse(writer.claimAndPersist(original));
SavedBiomeChunk loaded = SavedBiomeStore.open(root).get(-33, 31).orElseThrow();
assertEquals(original, loaded);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
assertEquals(original.surfaceAt(x, z), loaded.surfaceAt(x, z));
assertEquals(original.caveBaseAt(x, z), loaded.caveBaseAt(x, z));
for (int y = -64; y < 320; y++) {
assertEquals(original.biomeAt(x, y, z), loaded.biomeAt(x, y, z));
}
}
}
assertEquals(7L, loaded.biomeAt(0, -64, 0).activationId());
assertEquals("cave/base-0", loaded.caveBaseAt(0, 0).biomeKey());
assertEquals(8L, loaded.caveBaseAt(0, 0).activationId());
assertFalse(loaded.caveBaseAt(0, 0).equals(loaded.biomeAt(0, -64, 0)));
assertEquals("region/0", loaded.surfaceAt(0, 0).regionKey());
assertEquals("region/1", loaded.surfaceAt(0, 1).regionKey());
assertThrows(IndexOutOfBoundsException.class, () -> loaded.biomeAt(0, -65, 0));
assertThrows(IndexOutOfBoundsException.class, () -> loaded.biomeAt(0, 320, 0));
}
@Test
public void preservesUnresolvedHistoricalCellsAlongsideKnownBiomeAssignments() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeChunk.Cell unresolved = SavedBiomeChunk.Cell.unresolved(1L);
SavedBiomeChunk.Cell resolved = new SavedBiomeChunk.Cell(2L, "biome/current", "region/current");
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(-1, -1, 2L, -64, 384));
List<SavedBiomeChunk.Span> spans = List.of(new SavedBiomeChunk.Span(-64, 16, unresolved),
new SavedBiomeChunk.Span(16, 320, resolved));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
builder.column(x, z, new SavedBiomeChunk.Column(x < 8 ? unresolved : resolved, unresolved, spans));
}
}
SavedBiomeChunk original = builder.build();
SavedBiomeStore.open(root).claimAndPersist(original);
SavedBiomeChunk loaded = SavedBiomeStore.open(root).get(-1, -1).orElseThrow();
assertEquals(original, loaded);
assertFalse(loaded.surfaceAt(0, 0).isResolved());
assertEquals(unresolved, loaded.caveBaseAt(15, 15));
assertEquals(1L, loaded.surfaceAt(0, 0).activationId());
assertEquals(SavedBiomeChunk.Resolution.UNRESOLVED, loaded.biomeAt(15, -64, 15).resolution());
assertEquals(null, loaded.biomeAt(15, -64, 15).biomeKey());
assertEquals(null, loaded.biomeAt(15, -64, 15).regionKey());
assertTrue(loaded.surfaceAt(8, 0).isResolved());
assertEquals(resolved, loaded.biomeAt(0, 16, 0));
assertThrows(IllegalArgumentException.class, () -> SavedBiomeChunk.Cell.unresolved(0L));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, "biome/known", null));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, null, "region/known"));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, "biome/known", null,
SavedBiomeChunk.Resolution.UNRESOLVED));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, null, "region/known",
SavedBiomeChunk.Resolution.UNRESOLVED));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(1L, null, null,
SavedBiomeChunk.Resolution.RESOLVED));
}
@Test
public void rejectsConflictingClaimsWithoutReplacingSavedContent() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
SavedBiomeChunk original = chunk(1, 2, 3L);
store.claimAndPersist(original);
byte[] bytes = Files.readAllBytes(regionPath(root, 0, 0));
assertThrows(IOException.class, () -> store.claimAndPersist(chunk(1, 2, 4L)));
assertTrue(Arrays.equals(bytes, Files.readAllBytes(regionPath(root, 0, 0))));
assertEquals(Optional.of(original), SavedBiomeStore.open(root).get(1, 2));
}
@Test
public void missingSnapshotsLeaveExistingHistoryUntouchedAndCachedReadsAvoidDisk() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
Files.createDirectories(root.resolve("iris/generation/semantics"));
Path semantics = root.resolve("iris/generation/semantics/existing.isem");
Files.writeString(semantics, "unchanged format six data");
SavedBiomeStore store = SavedBiomeStore.open(root);
assertTrue(store.get(0, 0).isEmpty());
assertFalse(Files.exists(store.storageDirectory()));
SavedBiomeChunk chunk = chunk(0, 0, 3L);
store.claimAndPersist(chunk);
Files.delete(regionPath(root, 0, 0));
assertEquals(Optional.of(chunk), store.get(0, 0));
assertEquals(Optional.of(chunk), store.cached(0, 0));
assertEquals("unchanged format six data", Files.readString(semantics));
}
@Test
public void prunesOnlyUnstoredSelectedClaimsAndAllowsReplacementAfterCacheInvalidation() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
SavedBiomeChunk stored = chunk(0, 0, 3L);
SavedBiomeChunk orphan = chunk(1, 0, 3L);
SavedBiomeChunk previous = chunk(2, 0, 2L);
SavedBiomeChunk unrelatedRegion = chunk(32, 0, 2L);
store.claimAndPersist(stored);
store.claimAndPersist(orphan);
store.claimAndPersist(previous);
store.claimAndPersist(unrelatedRegion);
byte[] untouched = Files.readAllBytes(regionPath(root, 1, 0));
WorldChunkInventory inventory = WorldChunkInventory.ofPackedChunks(ChunkGenerationOwnership.packChunk(0, 0));
assertEquals(1, store.discardUnstoredClaims(inventory, Set.of(3L)));
assertTrue(store.cached(1, 0).isEmpty());
assertTrue(store.get(1, 0).isEmpty());
assertEquals(Optional.of(stored), store.get(0, 0));
assertEquals(Optional.of(previous), store.get(2, 0));
assertEquals(Optional.of(unrelatedRegion), store.get(32, 0));
assertTrue(Arrays.equals(untouched, Files.readAllBytes(regionPath(root, 1, 0))));
SavedBiomeChunk replacement = chunk(1, 0, 4L);
assertTrue(store.claimAndPersist(replacement));
assertEquals(0, store.discardUnstoredClaims(inventory, Set.of(3L)));
SavedBiomeStore loaded = SavedBiomeStore.open(root);
assertEquals(Optional.of(replacement), loaded.get(1, 0));
assertEquals(Optional.of(stored), loaded.get(0, 0));
assertEquals(Optional.of(previous), loaded.get(2, 0));
assertEquals(1, loaded.discardUnstoredClaims(WorldChunkInventory.empty(), Set.of(4L)));
assertTrue(SavedBiomeStore.open(root).get(1, 0).isEmpty());
assertThrows(IllegalArgumentException.class, () -> loaded.discardUnstoredClaims(inventory, Set.of(0L)));
}
@Test
public void tornLastAppendTruncatesOnlyIncompleteClaimAndAllowsNewAppend() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
SavedBiomeChunk first = chunk(0, 0, 3L);
store.claimAndPersist(first);
Path path = regionPath(root, 0, 0);
long firstLength = Files.size(path);
store.claimAndPersist(chunk(1, 0, 3L));
byte[] bytes = Files.readAllBytes(path);
Files.write(path, Arrays.copyOf(bytes, bytes.length - 3));
SavedBiomeStore loaded = SavedBiomeStore.open(root);
assertEquals(Optional.of(first), loaded.get(0, 0));
assertEquals(firstLength, Files.size(path));
assertTrue(loaded.get(1, 0).isEmpty());
assertTrue(loaded.claimAndPersist(chunk(2, 0, 3L)));
assertTrue(SavedBiomeStore.open(root).get(2, 0).isPresent());
}
@Test
public void rejectsChecksumCorruptionAndDuplicateCompleteRecords() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
store.claimAndPersist(chunk(0, 0, 3L));
Path path = regionPath(root, 0, 0);
byte[] original = Files.readAllBytes(path);
byte[] corrupt = original.clone();
corrupt[corrupt.length - 1] ^= 1;
Files.write(path, corrupt);
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
Files.write(path, original);
Files.write(path, Arrays.copyOfRange(original, 20, original.length), StandardOpenOption.APPEND);
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
}
@Test
public void rejectsUnknownFormatEvenWithValidHeaderChecksum() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore.open(root).claimAndPersist(chunk(0, 0, 3L));
Path path = regionPath(root, 0, 0);
byte[] bytes = Files.readAllBytes(path);
ByteBuffer.wrap(bytes).putInt(4, 2);
CRC32 crc = new CRC32();
crc.update(bytes, 0, 16);
ByteBuffer.wrap(bytes).putInt(16, (int) crc.getValue());
Files.write(path, bytes);
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
}
@Test
public void rejectsOversizedDecodedHeadersPalettesRunsAndStringsBeforeAllocation() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore.open(root).claimAndPersist(chunk(0, 0, 3L));
Path path = regionPath(root, 0, 0);
rewritePayload(path, new byte[24], Integer.MAX_VALUE);
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
for (int mode = 0; mode < 3; mode++) {
ByteArrayOutputStream raw = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(raw)) {
output.writeLong(3L);
output.writeInt(-64);
output.writeInt(384);
output.writeInt(mode == 0 ? Integer.MAX_VALUE : 1);
if (mode == 0) {
output.writeInt(0);
} else {
output.writeLong(3L);
output.writeByte(mode == 1 ? 1 : 0);
if (mode == 1) {
output.writeInt(1);
output.writeByte(0);
output.writeByte(0);
output.write(new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x07});
} else {
output.writeShort(65_535);
}
}
}
rewritePayload(path, raw.toByteArray(), raw.size());
assertThrows(IOException.class, () -> SavedBiomeStore.open(root).get(0, 0));
}
}
@Test
public void modelRejectsDenseColumnsBeyondItsDecodedFootprintLimit() {
SavedBiomeChunk.Cell first = new SavedBiomeChunk.Cell(3L, "biome/first", "region/main");
SavedBiomeChunk.Cell second = new SavedBiomeChunk.Cell(3L, "biome/second", "region/main");
ArrayList<SavedBiomeChunk.Span> spans = new ArrayList<>(4096);
for (int y = 0; y < 4096; y++) {
spans.add(new SavedBiomeChunk.Span(y, y + 1, (y & 1) == 0 ? first : second));
}
SavedBiomeChunk.Column column = new SavedBiomeChunk.Column(first, second, spans);
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(0, 0, 3L, 0, 4096));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
builder.column(x, z, column);
}
}
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, builder::build);
assertTrue(failure.getMessage().contains("footprint limit"));
}
@Test
public void boundedCachesReloadEvictedRegionsAndChunks() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
for (int index = 0; index < 140; index++) {
store.claimAndPersist(chunk(index * 32, -32, 3L));
assertTrue(store.cachedChunkCount() <= 128);
assertTrue(store.cachedRegionCount() <= 64);
assertTrue(store.cachedByteCount() <= 64L * 1024L * 1024L);
}
assertTrue(store.cached(0, -32).isEmpty());
assertEquals(Optional.of(chunk(0, -32, 3L)), store.get(0, -32));
assertEquals(Optional.of(chunk(139 * 32, -32, 3L)), SavedBiomeStore.open(root).get(139 * 32, -32));
}
@Test
public void compressesRepeatedVerticalColumnsAndKeepsDistinctExactSurfaceColumns() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeChunk.Header header = new SavedBiomeChunk.Header(0, 0, 5L, -64, 384);
SavedBiomeChunk.Cell cave = new SavedBiomeChunk.Cell(5L, "biome/cave", "region/main");
List<SavedBiomeChunk.Span> vertical = List.of(new SavedBiomeChunk.Span(-64, 320, cave));
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(header);
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
SavedBiomeChunk.Cell surface = new SavedBiomeChunk.Cell(5L, "biome/surface-" + x, "region/" + z);
builder.column(x, z, new SavedBiomeChunk.Column(surface, cave, vertical));
}
}
SavedBiomeChunk original = builder.build();
SavedBiomeStore.open(root).claimAndPersist(original);
assertTrue(Files.size(regionPath(root, 0, 0)) < 4096);
assertEquals(Optional.of(original), SavedBiomeStore.open(root).get(0, 0));
}
@Test
public void concurrentlyPublishesSeparateRegionsAndSingleImmutableClaims() throws Exception {
Path root = temporaryFolder.newFolder().toPath();
SavedBiomeStore store = SavedBiomeStore.open(root);
try (ExecutorService workers = Executors.newFixedThreadPool(4)) {
ArrayList<Future<Boolean>> futures = new ArrayList<>();
for (int index = 0; index < 12; index++) {
int chunkX = (index % 6) * 32;
futures.add(workers.submit(() -> store.claimAndPersist(chunk(chunkX, 0, 3L))));
}
int added = 0;
for (Future<Boolean> future : futures) {
if (future.get()) {
added++;
}
}
assertEquals(6, added);
}
SavedBiomeStore loaded = SavedBiomeStore.open(root);
for (int index = 0; index < 6; index++) {
assertEquals(Optional.of(chunk(index * 32, 0, 3L)), loaded.get(index * 32, 0));
}
}
@Test
public void modelCopiesInputsCompactsRunsAndRejectsGapsMissingColumnsAndInvalidOwners() {
SavedBiomeChunk.Cell cell = new SavedBiomeChunk.Cell(3L, "biome/main", "region/main");
ArrayList<SavedBiomeChunk.Span> spans = new ArrayList<>(List.of(
new SavedBiomeChunk.Span(-64, 0, cell), new SavedBiomeChunk.Span(0, 320, cell)));
SavedBiomeChunk.Column column = new SavedBiomeChunk.Column(cell, cell, spans);
spans.clear();
assertEquals(List.of(new SavedBiomeChunk.Span(-64, 320, cell)), column.vertical());
assertThrows(UnsupportedOperationException.class, () -> column.vertical().clear());
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(0, 0, 3L, -64, 384));
assertThrows(NullPointerException.class, builder::build);
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Cell(0L, "biome/main", "region/main"));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Header(0, 0, 3L, Integer.MAX_VALUE, 1));
assertThrows(IllegalArgumentException.class, () -> new SavedBiomeChunk.Column(cell, cell, List.of(
new SavedBiomeChunk.Span(-64, 0, cell), new SavedBiomeChunk.Span(1, 320, cell))));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
builder.column(x, z, column);
}
}
SavedBiomeChunk saved = builder.build();
SavedBiomeChunk.Column shorter = new SavedBiomeChunk.Column(cell, cell, List.of(new SavedBiomeChunk.Span(-64, 319, cell)));
builder.column(0, 0, shorter);
assertEquals(column, saved.column(0, 0));
assertThrows(IllegalArgumentException.class, builder::build);
}
private static void rewritePayload(Path path, byte[] raw, int declaredSize) throws IOException {
byte[] header = Arrays.copyOf(Files.readAllBytes(path), 20);
ByteArrayOutputStream body = new ByteArrayOutputStream();
DataOutputStream coordinates = new DataOutputStream(body);
coordinates.writeInt(0);
coordinates.writeInt(0);
coordinates.writeInt(declaredSize);
try (DeflaterOutputStream compressed = new DeflaterOutputStream(coordinates)) {
compressed.write(raw);
}
byte[] encoded = body.toByteArray();
CRC32 crc = new CRC32();
crc.update(encoded);
ByteArrayOutputStream file = new ByteArrayOutputStream();
try (DataOutputStream output = new DataOutputStream(file)) {
output.write(header);
output.writeInt(encoded.length);
output.write(encoded);
output.writeInt((int) crc.getValue());
}
Files.write(path, file.toByteArray());
}
private static Path regionPath(Path root, int regionX, int regionZ) {
return root.resolve("iris/generation/biomes/r." + regionX + "." + regionZ + ".ibio");
}
private static SavedBiomeChunk chunk(int chunkX, int chunkZ, long activation) {
SavedBiomeChunk.Builder builder = SavedBiomeChunk.builder(new SavedBiomeChunk.Header(chunkX, chunkZ, activation, -64, 384));
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
SavedBiomeChunk.Cell cave = new SavedBiomeChunk.Cell(activation - 1L, "cave/" + (x / 4), "region/" + (z / 4));
SavedBiomeChunk.Cell surface = new SavedBiomeChunk.Cell(activation, "surface/" + x, "region/" + z);
builder.column(x, z, new SavedBiomeChunk.Column(surface,
new SavedBiomeChunk.Cell(activation, "cave/base-" + z, "region/" + z), List.of(
new SavedBiomeChunk.Span(-64, 16 + z, cave), new SavedBiomeChunk.Span(16 + z, 320, surface))));
}
}
return builder.build();
}
}
@@ -0,0 +1,74 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.HeightMap;
import org.bukkit.Location;
import org.bukkit.Particle;
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.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
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.when;
public class IrisEffectSurfaceTest {
@Test
public void samplesLiveHeightOnTargetRegionThenEmitsOnPlayerThread() {
World world = mock(World.class);
Player player = mock(Player.class);
Location location = new Location(world, -17, 100, 32);
when(player.getWorld()).thenReturn(world);
when(world.isChunkLoaded(-2, 2)).thenReturn(true);
when(world.getHighestBlockYAt(-17, 32, HeightMap.OCEAN_FLOOR)).thenReturn(23);
AtomicReference<Runnable> regionTask = new AtomicReference<>();
AtomicReference<Runnable> playerTask = new AtomicReference<>();
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
scheduler.when(() -> J.runAt(eq(location), any(Runnable.class))).thenAnswer(call -> {
regionTask.set(call.getArgument(1));
return true;
});
scheduler.when(() -> J.runEntity(eq(player), any(Runnable.class))).thenAnswer(call -> {
playerTask.set(call.getArgument(1));
return true;
});
new IrisEffect().applyParticles(player, Particle.FLAME, location);
assertNotNull(regionTask.get());
assertNull(playerTask.get());
verify(world, never()).getHighestBlockYAt(anyInt(), anyInt(), any(HeightMap.class));
regionTask.get().run();
assertEquals(24D, location.getY(), 0D);
assertNotNull(playerTask.get());
playerTask.get().run();
verify(player).spawnParticle(eq(Particle.FLAME), anyDouble(), eq(24D), anyDouble(),
anyInt(), anyDouble(), anyDouble(), anyDouble());
}
}
@Test
public void unloadedParticleColumnsDoNotLoadTerrainOrEmit() {
World world = mock(World.class);
Player player = mock(Player.class);
Location location = new Location(world, 0, 100, 0);
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
scheduler.when(() -> J.isOwnedByCurrentRegion(world, 0, 0)).thenReturn(true);
new IrisEffect().applyParticles(player, Particle.FLAME, location);
verify(world, never()).getHighestBlockYAt(anyInt(), anyInt(), any(HeightMap.class));
scheduler.verify(() -> J.runEntity(eq(player), any(Runnable.class)), never());
}
}
}