diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java index 74dab18fb..f0739750e 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java @@ -347,6 +347,13 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, return planner == null ? SubterrainCell.OUTSIDE : planner.sample(x, y + getWorld().minHeight(), z); } + @ChunkCoordinates + default boolean subterrainAbsentInChunk(int chunkX, int chunkZ) { + IrisComplex complex = getComplex(); + SubterrainPlanner planner = complex == null ? null : SubterrainChunkCells.activePlanner(complex, getDimension()); + return planner == null || !planner.hasCandidates(chunkX, chunkZ); + } + @BlockCoordinates default IrisBiome getSubterrainBiome(int x, int y, int z) { SubterrainCell cell = getSubterrainCell(x, y, z); @@ -406,6 +413,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, return; } IrisComplex complex = getComplex(); + boolean subterrain = !subterrainAbsentInChunk(x >> 4, z >> 4); int caveTop = getHeight(x, z) - 2; IrisRegion region = getRegion(x, 0, z); IrisBiome surface = null; @@ -419,7 +427,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, for (int index = 0; index < biomes.length; index++) { int y = index * step; regions[index] = region; - IrisBiome featureBiome = getSubterrainBiome(x, y, z); + IrisBiome featureBiome = subterrain ? getSubterrainBiome(x, y, z) : null; if (featureBiome != null) { biomes[index] = featureBiome; continue; diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index d9ef62f33..7145da0b2 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -668,6 +668,15 @@ public class IrisEngine implements Engine { return gate.admitted() ? complex.getSubterrainPlanner() : null; } + @ChunkCoordinates + @Override + public boolean subterrainAbsentInChunk(int chunkX, int chunkZ) { + if (!hasGenerationRuntimeScope() && generationHistoryRuntimeRouter != null) { + return false; + } + return Engine.super.subterrainAbsentInChunk(chunkX, chunkZ); + } + @BlockCoordinates @Override public IrisBiome getSubterrainBiome(int x, int y, int z) { diff --git a/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java b/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java index 3edb86168..48cc95585 100644 --- a/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java +++ b/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java @@ -107,6 +107,10 @@ public final class SubterrainPlanner { return List.copyOf(result); } + public boolean hasCandidates(int chunkX, int chunkZ) { + return !isEmpty() && !candidates(chunkX, chunkZ).isEmpty(); + } + public SubterrainCell sample(int x, int y, int z) { if (isEmpty() || y < minY || y >= maxYExclusive) { return SubterrainCell.OUTSIDE; diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java index 91d6354f3..e57dd6095 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java @@ -8,6 +8,7 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.generation.runtime.DimensionStackContext; import art.arcane.iris.generation.runtime.DimensionStackLayout; import art.arcane.iris.generation.runtime.DimensionTerrainContext; +import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisEngine; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.stream.ProvisionalSampling; @@ -27,6 +28,7 @@ import org.bukkit.block.Biome; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; @@ -46,7 +48,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112; private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4; - private final long seed; + private final RNG biomeRng; private final Engine engine; private final BukkitChunkGenerator platformGenerator; private final NativeBiomeRegistry biomeCustomRegistry; @@ -64,7 +66,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy this.engine = engine; this.platformGenerator = options.platformGenerator(); this.possibleBiomeRegistry = options.possibleBiomeRegistry(); - this.seed = options.seed(); + this.biomeRng = new RNG(options.seed()); this.biomeCustomRegistry = registry; this.biomeRegistry = biomeCustomRegistry; this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry); @@ -826,7 +828,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy int z, IrisDimensionCarvingResolver.State resolverState ) { - return resolveVisibleBiomeHolder(x, y, z, resolverState, null); + return resolveVisibleBiomeHolder(x, y, z, resolverState, null, null); } private H resolveVisibleBiomeHolder( @@ -835,32 +837,44 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy int z, IrisDimensionCarvingResolver.State resolverState, DimensionStackLayout stackLayout + ) { + return resolveVisibleBiomeHolder(x, y, z, resolverState, stackLayout, null); + } + + private H resolveVisibleBiomeHolder( + int x, + int y, + int z, + IrisDimensionCarvingResolver.State resolverState, + DimensionStackLayout stackLayout, + ChunkResolution chunk ) { int blockX = x << 2; int blockY = y << 2; int blockZ = z << 2; - Optional historicalKey = engine.getComplex().historicalPhysicalBiomeKeyAt( + IrisComplex complex = chunk == null ? engine.getComplex() : chunk.complex; + Optional historicalKey = complex.historicalPhysicalBiomeKeyAt( blockX, blockY, blockZ); if (historicalKey.isPresent()) { return resolvePhysicalBiomeHolder(historicalKey.get()); } BiomeResolution resolution = resolveBiomeResolution( - x, y, z, resolverState, true, stackLayout); + x, y, z, resolverState, true, stackLayout, chunk); if (resolution == null) { return getFallbackBiome(); } - return resolveVisibleBiomeHolder(resolution); + return resolveVisibleBiomeHolder(resolution, chunk); } - private H resolveVisibleBiomeHolder(BiomeResolution resolution) { + private H resolveVisibleBiomeHolder(BiomeResolution resolution, ChunkResolution chunk) { if (resolution.irisBiome.isCustom()) { - return resolveCustomHolder(resolution); + return resolveCustomHolder(resolution, chunk); } Biome vanillaBiome = resolution.underground - ? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ) - : resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); + ? resolution.irisBiome.getGroundBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ) + : resolution.irisBiome.getSkyBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); H holder = biomeRegistry.lookup(vanillaBiome); if (holder != null) { return holder; @@ -869,11 +883,12 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy return getFallbackBiome(); } - private H resolveCustomHolder(BiomeResolution resolution) { - IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); + private H resolveCustomHolder(BiomeResolution resolution, ChunkResolution chunk) { + IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); if (customBiome != null) { - H holder = runtimeBiomeState().customBiomes().get(customBiomeKey( - resolution.dimension, customBiome.getId())); + H holder = chunk == null + ? runtimeBiomeState().customBiomes().get(customBiomeKey(resolution.dimension, customBiome.getId())) + : chunk.customHolder(resolution.dimension, customBiome); if (holder != null) { return holder; } @@ -883,7 +898,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy } private BiomeResolution resolveStructureBiomeResolution(int x, int y, int z) { - return resolveBiomeResolution(x, y, z, null, false); + return resolveBiomeResolution(x, y, z, null, false, null, null); } private H resolvePhysicalBiomeHolder(String physicalKey) { @@ -898,65 +913,50 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy return holder; } - private BiomeResolution resolveBiomeResolution( - int x, - int y, - int z, - IrisDimensionCarvingResolver.State resolverState - ) { - return resolveBiomeResolution(x, y, z, resolverState, true); - } - - private BiomeResolution resolveBiomeResolution( - int x, - int y, - int z, - IrisDimensionCarvingResolver.State resolverState, - boolean includeDimensionStack - ) { - return resolveBiomeResolution( - x, y, z, resolverState, includeDimensionStack, null); - } - private BiomeResolution resolveBiomeResolution( int x, int y, int z, IrisDimensionCarvingResolver.State resolverState, boolean includeDimensionStack, - DimensionStackLayout stackLayout + DimensionStackLayout stackLayout, + ChunkResolution chunk ) { if (engine == null || engine.isClosed()) { return null; } - if (engine.getComplex() == null) { + if ((chunk == null ? engine.getComplex() : chunk.complex) == null) { return null; } int blockX = x << 2; int blockZ = z << 2; int blockY = y << 2; - int worldMinHeight = engine.getWorld().minHeight(); + int worldMinHeight = chunk == null ? engine.getWorld().minHeight() : chunk.worldMinHeight; int internalY = blockY - worldMinHeight; int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); - IrisBiome featureBiome = engine.getSubterrainBiome(blockX, internalY, blockZ); + IrisBiome featureBiome = chunk != null && !chunk.subterrain + ? null : engine.getSubterrainBiome(blockX, internalY, blockZ); if (featureBiome != null) { - return createBiomeResolution(featureBiome, true, engine.getDimension(), blockX, blockY, blockZ); + return new BiomeResolution(featureBiome, true, chunk == null ? engine.getDimension() : chunk.dimension, + blockX, blockY, blockZ); } DimensionStackLayout.Layer stackLayer = includeDimensionStack ? stackLayout == null - ? resolveDimensionStackLayer(engine, blockX, internalY, blockZ) + ? resolveDimensionStackLayer( + chunk == null ? engine.getDimensionStackContext() : chunk.stackContext, + blockX, internalY, blockZ) : stackLayout.layerAt(internalY) : null; IrisDimension owningDimension = stackLayer == null - ? engine.getDimension() + ? chunk == null ? engine.getDimension() : chunk.dimension : stackLayer.terrainContext().getDimension(); if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) { IrisBiome stackedBiome = stackLayer.biome(); return stackedBiome == null ? null - : createBiomeResolution( + : new BiomeResolution( stackedBiome, false, owningDimension, blockX, blockY, blockZ); } boolean deepUnderground = internalY <= caveSwitchInternalY; @@ -976,41 +976,31 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy ) : stackLayer.biome(); } else if (deepUnderground) { - int surfaceInternalY = Engine.hostHeight(engine, blockX, blockZ, true); + int surfaceInternalY = chunk == null + ? Engine.hostHeight(engine, blockX, blockZ, true) + : chunk.hostHeight(blockX, blockZ); underground = internalY <= surfaceInternalY - 8; irisBiome = underground ? engine.getCaveBiome(blockX, internalY, blockZ, resolverState) - : engine.getHostSurfaceBiome(blockX, blockZ); + : hostSurfaceBiome(chunk, blockX, blockZ); } else { - irisBiome = engine.getHostSurfaceBiome(blockX, blockZ); + irisBiome = hostSurfaceBiome(chunk, blockX, blockZ); } if (irisBiome == null && underground) { irisBiome = stackLayer == null - ? engine.getHostSurfaceBiome(blockX, blockZ) + ? hostSurfaceBiome(chunk, blockX, blockZ) : stackLayer.biome(); } if (irisBiome == null) { return null; } - return createBiomeResolution( + return new BiomeResolution( irisBiome, underground, owningDimension, blockX, blockY, blockZ); } - private BiomeResolution createBiomeResolution( - IrisBiome irisBiome, - boolean underground, - IrisDimension dimension, - int blockX, - int blockY, - int blockZ - ) { - RNG noiseRng = new RNG(seed - ^ (((long) blockX) * 341873128712L) - ^ (((long) blockY) * 132897987541L) - ^ (((long) blockZ) * 42317861L)); - return new BiomeResolution( - irisBiome, underground, dimension, blockX, blockY, blockZ, noiseRng); + private IrisBiome hostSurfaceBiome(ChunkResolution chunk, int blockX, int blockZ) { + return chunk == null ? engine.getHostSurfaceBiome(blockX, blockZ) : chunk.hostSurfaceBiome(blockX, blockZ); } private H getFallbackBiome() { @@ -1094,12 +1084,11 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy } private static DimensionStackLayout.Layer resolveDimensionStackLayer( - Engine engine, + DimensionStackContext stackContext, int blockX, int internalY, int blockZ ) { - DimensionStackContext stackContext = engine.getDimensionStackContext(); return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ); } @@ -1136,7 +1125,6 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy private final int blockX; private final int blockY; private final int blockZ; - private final RNG rng; private BiomeResolution( IrisBiome irisBiome, @@ -1144,8 +1132,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy IrisDimension dimension, int blockX, int blockY, - int blockZ, - RNG rng + int blockZ ) { this.irisBiome = irisBiome; this.underground = underground; @@ -1153,7 +1140,100 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy this.blockX = blockX; this.blockY = blockY; this.blockZ = blockZ; - this.rng = rng; + } + } + + private final class ChunkVisibleResolver implements VisibleResolver { + private final IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + private final boolean flat = platformGenerator != null && platformGenerator.usesFlatStudioTerrain(); + private ChunkResolution chunk; + + @Override + public H biome(int quartX, int quartY, int quartZ) { + if (flat) { + return fallbackBiome; + } + int chunkX = quartX >> 2; + int chunkZ = quartZ >> 2; + ChunkResolution current = chunk; + if (current == null || current.chunkX != chunkX || current.chunkZ != chunkZ) { + current = new ChunkResolution(chunkX, chunkZ); + chunk = current; + } + return resolveVisibleBiomeHolder(quartX, quartY, quartZ, resolverState, null, current); + } + } + + private final class ChunkResolution { + private static final int COLUMNS = 16; + + private final int chunkX; + private final int chunkZ; + private final IrisComplex complex; + private final IrisDimension dimension; + private final DimensionStackContext stackContext; + private final int worldMinHeight; + private final boolean subterrain; + private final RuntimeBiomeState runtimeState; + private final int[] hostHeights = new int[COLUMNS]; + private final boolean[] hostHeightResolved = new boolean[COLUMNS]; + private final IrisBiome[] hostSurfaces = new IrisBiome[COLUMNS]; + private final boolean[] hostSurfaceResolved = new boolean[COLUMNS]; + private IrisDimension[] customDimensions = new IrisDimension[4]; + private IrisBiomeCustom[] customBiomes = new IrisBiomeCustom[4]; + private Object[] customHolders = new Object[4]; + private int customCount; + + private ChunkResolution(int chunkX, int chunkZ) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.complex = engine.getComplex(); + this.dimension = engine.getDimension(); + this.stackContext = engine.getDimensionStackContext(); + this.worldMinHeight = engine.getWorld().minHeight(); + this.subterrain = !engine.subterrainAbsentInChunk(chunkX, chunkZ); + this.runtimeState = runtimeBiomeState(); + } + + private int hostHeight(int blockX, int blockZ) { + int column = column(blockX, blockZ); + if (!hostHeightResolved[column]) { + hostHeights[column] = Engine.hostHeight(engine, blockX, blockZ, true); + hostHeightResolved[column] = true; + } + return hostHeights[column]; + } + + private IrisBiome hostSurfaceBiome(int blockX, int blockZ) { + int column = column(blockX, blockZ); + if (!hostSurfaceResolved[column]) { + hostSurfaces[column] = engine.getHostSurfaceBiome(blockX, blockZ); + hostSurfaceResolved[column] = true; + } + return hostSurfaces[column]; + } + + @SuppressWarnings("unchecked") + private H customHolder(IrisDimension owner, IrisBiomeCustom custom) { + for (int index = 0; index < customCount; index++) { + if (customBiomes[index] == custom && customDimensions[index] == owner) { + return (H) customHolders[index]; + } + } + H holder = runtimeState.customBiomes().get(customBiomeKey(owner, custom.getId())); + if (customCount == customBiomes.length) { + customDimensions = Arrays.copyOf(customDimensions, customCount * 2); + customBiomes = Arrays.copyOf(customBiomes, customCount * 2); + customHolders = Arrays.copyOf(customHolders, customCount * 2); + } + customDimensions[customCount] = owner; + customBiomes[customCount] = custom; + customHolders[customCount++] = holder; + return holder; + } + + private static int column(int blockX, int blockZ) { + return ((blockX & 15) >> 2) << 2 | ((blockZ & 15) >> 2); } } @@ -1181,8 +1261,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy } @Override public VisibleResolver visibleResolver() { - IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); - return (x, y, z) -> getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, state); + return new ChunkVisibleResolver(); } } diff --git a/core/src/main/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndex.java b/core/src/main/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndex.java index 39da749e9..618023b76 100644 --- a/core/src/main/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndex.java +++ b/core/src/main/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndex.java @@ -13,12 +13,13 @@ import art.arcane.volmlib.util.collection.KList; import java.io.IOException; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.BooleanSupplier; import java.util.function.IntConsumer; import java.util.function.Supplier; @@ -38,8 +39,8 @@ public final class NativeStructureVolumeIndex { Collections.synchronizedMap(new WeakHashMap<>()); private final OriginResolver origins; - private final Map> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS); - private final Map> queryCache = lru(MAX_CACHED_QUERY_CHUNKS); + private final VolumeCache originCache = new VolumeCache(MAX_CACHED_ORIGIN_CHUNKS); + private final VolumeCache queryCache = new VolumeCache(MAX_CACHED_QUERY_CHUNKS); private final ConcurrentHashMap>> originBuilds = new ConcurrentHashMap<>(); private final ConcurrentHashMap>> queryBuilds = @@ -180,12 +181,10 @@ public final class NativeStructureVolumeIndex { Engine engine, int chunkX, int chunkZ, Integer pinnedRuntimeId ) { if (pinnedRuntimeId != null) { - synchronized (originCache) { - KList cached = originCache.get( - new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ))); - if (cached != null) { - return cached; - } + KList cached = originCache.get( + new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ))); + if (cached != null) { + return cached; } } return originVolumes(engine, chunkX, chunkZ); @@ -210,27 +209,21 @@ public final class NativeStructureVolumeIndex { } void evictRuntime(int runtimeId) { - synchronized (originCache) { - originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); - originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); - } - synchronized (queryCache) { - queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); - queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); - } + originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); + originCache.evictRuntime(runtimeId); + queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); + queryCache.evictRuntime(runtimeId); } private KList cached( - Map> cache, + VolumeCache volumes, ConcurrentHashMap>> builds, RuntimeChunkKey key, Supplier> loader ) { - synchronized (cache) { - KList hit = cache.get(key); - if (hit != null) { - return hit; - } + KList hit = volumes.get(key); + if (hit != null) { + return hit; } CompletableFuture> future = new CompletableFuture<>(); @@ -241,22 +234,16 @@ public final class NativeStructureVolumeIndex { // Close the check-then-claim window: a racer whose cache check missed before the // previous builder cached could claim the build slot after it retired and rebuild. - synchronized (cache) { - KList published = cache.get(key); - if (published != null) { - future.complete(published); - builds.remove(key, future); - return published; - } + KList published = volumes.get(key); + if (published != null) { + future.complete(published); + builds.remove(key, future); + return published; } try { KList built = loader.get(); - synchronized (cache) { - if (builds.get(key) == future) { - cache.put(key, built); - } - } + volumes.publish(key, built, () -> builds.get(key) == future); future.complete(built); return built; } catch (RuntimeException | Error error) { @@ -303,15 +290,45 @@ public final class NativeStructureVolumeIndex { } } - private static Map> lru(int capacity) { - return new LinkedHashMap<>(16, 0.75F, true) { - @Override - protected boolean removeEldestEntry( - Map.Entry> eldest - ) { - return size() > capacity; + private static final class VolumeCache { + private final int capacity; + private final ConcurrentHashMap> entries = new ConcurrentHashMap<>(); + private final ConcurrentLinkedQueue insertionOrder = new ConcurrentLinkedQueue<>(); + + private VolumeCache(int capacity) { + this.capacity = capacity; + } + + private KList get(RuntimeChunkKey key) { + return entries.get(key); + } + + private void publish(RuntimeChunkKey key, KList volumes, BooleanSupplier current) { + boolean[] inserted = new boolean[1]; + entries.compute(key, (ignored, previous) -> { + if (!current.getAsBoolean()) { + return previous; + } + inserted[0] = previous == null; + return volumes; + }); + if (!inserted[0]) { + return; } - }; + insertionOrder.add(key); + while (entries.size() > capacity) { + RuntimeChunkKey eldest = insertionOrder.poll(); + if (eldest == null) { + return; + } + entries.remove(eldest); + } + } + + private void evictRuntime(int runtimeId) { + entries.keySet().removeIf(key -> key.runtimeId() == runtimeId); + insertionOrder.removeIf(key -> key.runtimeId() == runtimeId); + } } static int runtimeChunkHash(int runtimeId, long chunkKey) { diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java index 016ba9054..3b6b5b076 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java @@ -99,6 +99,7 @@ public final class GenerationSemanticIndex { private volatile IOException journalFailure; private long appendingRegionKey; private RegionShard appendingBase; + private boolean journalStorageReady; private GenerationSemanticIndex( Path dimensionRoot, @@ -264,12 +265,13 @@ public final class GenerationSemanticIndex { if (updates.isEmpty()) { return; } - ensureStorageDirectory(); + ensureJournalStorage(); + boolean existingJournal = journalEntries.containsKey(regionKey); appendingRegionKey = regionKey; appendingBase = baseRegion; publicationLock.unlock(); try { - SemanticJournal.append(directory, regionX, regionZ, updates, unpublished, durability); + SemanticJournal.append(directory, regionX, regionZ, updates, unpublished, durability, existingJournal); } catch (JournalAppendFailure failure) { journalFailure = failure; throw failure; @@ -1193,6 +1195,14 @@ public final class GenerationSemanticIndex { return true; } + private void ensureJournalStorage() throws IOException { + if (journalStorageReady) { + return; + } + ensureStorageDirectory(); + journalStorageReady = true; + } + private void ensureStorageDirectory() throws IOException { validateDimensionRoot(); Path irisDirectory = ensureChildDirectory(dimensionRoot, "iris"); @@ -1958,18 +1968,17 @@ public final class GenerationSemanticIndex { int regionZ, List claims, boolean unpublished, - DeferredDurability durability + DeferredDurability durability, + boolean existing ) throws IOException { Path file = directory.resolve(journalFileName(regionX, regionZ)); - boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS); - if (!created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) { + boolean created = !existing && !Files.exists(file, LinkOption.NOFOLLOW_LINKS); + if (!existing && !created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) { throw invalid(file, "path is not a regular file"); } - FileChannel opened = FileChannel.open( - file, - StandardOpenOption.CREATE, - StandardOpenOption.WRITE - ); + FileChannel opened = existing + ? FileChannel.open(file, StandardOpenOption.WRITE) + : FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); boolean uncertain = created; boolean restored = false; try (FileChannel channel = opened) { diff --git a/core/src/main/java/art/arcane/iris/world/history/NativeTerrainReceipt.java b/core/src/main/java/art/arcane/iris/world/history/NativeTerrainReceipt.java index 45245b9e8..4d84d4791 100644 --- a/core/src/main/java/art/arcane/iris/world/history/NativeTerrainReceipt.java +++ b/core/src/main/java/art/arcane/iris/world/history/NativeTerrainReceipt.java @@ -6,11 +6,16 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.OptionalInt; +import java.util.zip.CRC32; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -18,6 +23,8 @@ public final class NativeTerrainReceipt { private static final int MAGIC = 0x4952544E; private static final int VERSION = 1; private static final int MAXIMUM_BYTES = 64 * 1024 * 1024; + private static final ThreadLocal DEFLATERS = + ThreadLocal.withInitial(() -> new Deflater(Deflater.DEFAULT_COMPRESSION, true)); private NativeTerrainReceipt() { } @@ -25,7 +32,9 @@ public final class NativeTerrainReceipt { public static byte[] encode(SavedTerrainChunk chunk, long activationId, String epochId) throws IOException { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); UtfCache strings = new UtfCache(); - try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(bytes)))) { + Deflater deflater = DEFLATERS.get(); + deflater.reset(); + try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GzipStream(bytes, deflater)))) { output.writeInt(MAGIC); output.writeInt(VERSION); output.writeInt(chunk.chunkX()); @@ -196,6 +205,60 @@ public final class NativeTerrainReceipt { public record Decoded(SavedTerrainChunk terrain, long activationId, String epochId) { } + private static final class GzipStream extends DeflaterOutputStream { + private static final int BUFFER_BYTES = 512; + private static final int TRAILER_BYTES = 8; + private static final byte[] HEADER = gzipHeader(); + + private final CRC32 checksum = new CRC32(); + + private GzipStream(OutputStream out, Deflater deflater) throws IOException { + super(out, deflater, BUFFER_BYTES); + out.write(HEADER); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + super.write(buffer, offset, length); + checksum.update(buffer, offset, length); + } + + @Override + public void finish() throws IOException { + if (def.finished()) { + return; + } + def.finish(); + while (!def.finished()) { + int length = def.deflate(buf, 0, buf.length); + if (length > 0) { + out.write(buf, 0, length); + } + } + byte[] trailer = new byte[TRAILER_BYTES]; + writeLittleEndian(trailer, 0, (int) checksum.getValue()); + writeLittleEndian(trailer, 4, (int) def.getBytesRead()); + out.write(trailer); + } + + private static void writeLittleEndian(byte[] target, int offset, int value) { + target[offset] = (byte) value; + target[offset + 1] = (byte) (value >>> 8); + target[offset + 2] = (byte) (value >>> 16); + target[offset + 3] = (byte) (value >>> 24); + } + + private static byte[] gzipHeader() { + ByteArrayOutputStream empty = new ByteArrayOutputStream(); + try (GZIPOutputStream gzip = new GZIPOutputStream(empty)) { + gzip.finish(); + } catch (IOException failure) { + throw new IllegalStateException("Unable to resolve the platform gzip header", failure); + } + return Arrays.copyOf(empty.toByteArray(), 10); + } + } + private static final class UtfCache { private static final int MAXIMUM_ENTRIES = 256; private static final int MAXIMUM_ENCODED_BYTES = 64 * 1024; diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java index 1964053f1..c71427cd8 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java @@ -36,7 +36,8 @@ public final class SavedBiomeCapture { Math.addExact(startX, 15), Math.addExact(startZ, 15)).isEmpty(); Map> vertical = new HashMap<>(exactVolume ? 256 : 16); Cells cells = new Cells(activationId); - Column column = new Column(engine, minimumY, height, exactVolume ? 1 : 4); + Column column = new Column(engine, minimumY, height, exactVolume ? 1 : 4, + !engine.subterrainAbsentInChunk(stage.chunkX(), stage.chunkZ())); for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { int blockX = startX + localX; @@ -82,7 +83,8 @@ public final class SavedBiomeCapture { for (int index = 0; index < column.biomes().length; index++) { int worldY = minimumY + index * column.step(); SavedBiomeChunk.Cell current = cells.cell(column.biomes()[index], column.regions()[index]); - if (floating != null && !column.engine().getSubterrainCell(blockX, index * column.step(), blockZ).occupied()) { + if (floating != null && (!column.subterrain() + || !column.engine().getSubterrainCell(blockX, index * column.step(), blockZ).occupied())) { current = overlay(current, floating.volumeAt(blockX & 15, worldY - minimumY, blockZ & 15)); } if (transition != null) { @@ -139,9 +141,10 @@ public final class SavedBiomeCapture { Objects.requireNonNull(region, "region").getLoadKey()); } - private record Column(Engine engine, int minimumY, int height, int step, IrisBiome[] biomes, IrisRegion[] regions) { - private Column(Engine engine, int minimumY, int height, int step) { - this(engine, minimumY, height, step, new IrisBiome[(height + step - 1) / step], + private record Column(Engine engine, int minimumY, int height, int step, boolean subterrain, IrisBiome[] biomes, + IrisRegion[] regions) { + private Column(Engine engine, int minimumY, int height, int step, boolean subterrain) { + this(engine, minimumY, height, step, subterrain, new IrisBiome[(height + step - 1) / step], new IrisRegion[(height + step - 1) / step]); } } diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java index 713dc7905..78459d1dc 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java @@ -49,6 +49,7 @@ public final class SavedBiomeStore { private static final int MAXIMUM_CACHED_CHUNKS = 128; private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L; private static final int MAXIMUM_WRITE_BATCH = 64; + private static final ThreadLocal DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED)); private final Path dimensionRoot; private final Path directory; @@ -464,12 +465,16 @@ public final class SavedBiomeStore { } private void ensureRegionFile(RegionIndex region) throws IOException { + if (region.fileReady) { + return; + } 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); } + region.fileReady = true; return; } ByteBuffer header = ByteBuffer.allocate(HEADER_BYTES); @@ -486,6 +491,7 @@ public final class SavedBiomeStore { } Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE); forceDirectory(directory); + region.fileReady = true; } finally { Files.deleteIfExists(temporary); } @@ -674,11 +680,10 @@ public final class SavedBiomeStore { output.writeInt(chunk.chunkX()); output.writeInt(chunk.chunkZ()); output.writeInt(raw.size()); - Deflater deflater = new Deflater(Deflater.BEST_SPEED); + Deflater deflater = DEFLATERS.get(); + deflater.reset(); 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"); @@ -847,6 +852,7 @@ public final class SavedBiomeStore { private final int regionZ; private final long[] offsets = new long[1024]; private final int[] lengths = new int[1024]; + private boolean fileReady; private RegionIndex(Path path, int regionX, int regionZ) { this.path = path; diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeVisibleResolverTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeVisibleResolverTest.java new file mode 100644 index 000000000..abf24ea01 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeVisibleResolverTest.java @@ -0,0 +1,111 @@ +package art.arcane.iris.platform.bukkit.nms; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisBiomeCustom; +import art.arcane.iris.generation.mantle.EngineMantle; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.GenerationSessionLease; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.world.IrisWorld; +import art.arcane.volmlib.nativelib.terrain.NativeBiomeRegistry; +import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourcePolicy; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BukkitBiomeVisibleResolverTest { + @Test + public void chunkResolverMatchesThePerQuartResolution() throws Exception { + IrisBiome surface = customBiome("surface", "meadow"); + IrisBiome cave = customBiome("cave", "grotto"); + IrisBiome custom = customBiome("custom", "mirage"); + IrisBiome feature = customBiome("feature", "bloom"); + IrisDimension dimension = mock(IrisDimension.class); + when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(surface, cave, custom, feature)); + when(dimension.getCustomBiomeKey(anyString())).thenAnswer(invocation -> "pack:" + invocation.getArgument(0)); + IrisData data = mock(IrisData.class); + when(data.customBiomeResourceKey(any(IrisDimension.class), anyString())) + .thenAnswer(invocation -> "pack:" + invocation.getArgument(1)); + IrisComplex complex = mock(IrisComplex.class); + when(complex.historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt())).thenReturn(Optional.empty()); + EngineMantle mantle = mock(EngineMantle.class); + when(mantle.trueHeight(anyInt(), anyInt())).thenAnswer(invocation -> + 120 + Math.floorMod(invocation.getArgument(0) * 7 + invocation.getArgument(1) * 3, 160)); + IrisEngine engine = mock(IrisEngine.class); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getData()).thenReturn(data); + when(engine.getComplex()).thenReturn(complex); + when(engine.getMantle()).thenReturn(mantle); + when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()); + when(engine.getMinHeight()).thenReturn(-64); + when(engine.getCacheID()).thenReturn(1); + when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop()); + when(engine.answersFromNaturalTerrain(anyInt(), anyInt())).thenReturn(false); + when(engine.getHostSurfaceBiome(anyInt(), anyInt())).thenAnswer(invocation -> + Math.floorMod(invocation.getArgument(0) + invocation.getArgument(1), 8) == 0 ? custom : surface); + when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class))) + .thenAnswer(invocation -> invocation.getArgument(1) % 3 == 0 ? custom : cave); + when(engine.getSubterrainBiome(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> + invocation.getArgument(0) == 4 && invocation.getArgument(1) == 40 ? feature : null); + when(engine.subterrainAbsentInChunk(1, 0)).thenReturn(true); + @SuppressWarnings("unchecked") + NativeBiomeRegistry registry = mock(NativeBiomeRegistry.class); + when(registry.lookupCustom(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + BukkitBiomePolicy policy = policy(engine, registry, "fallback"); + + for (int chunkX = 0; chunkX <= 1; chunkX++) { + NativeBiomeSourcePolicy.VisibleResolver resolver = policy.visibleResolver(); + for (int quartY = -16; quartY < 80; quartY++) { + for (int localX = 0; localX < 4; localX++) { + for (int localZ = 0; localZ < 4; localZ++) { + int quartX = (chunkX << 2) + localX; + String expected = policy.getVisibleNoiseBiomeWithActiveGenerationLease( + quartX, quartY, localZ, new IrisDimensionCarvingResolver.State()); + assertEquals("quart " + quartX + "," + quartY + "," + localZ, expected, + resolver.biome(quartX, quartY, localZ)); + } + } + } + } + } + + private static IrisBiome customBiome(String key, String customId) { + IrisBiome biome = new IrisBiome().setCustomDerivitives(new KList<>(new IrisBiomeCustom().setId(customId))); + biome.setLoadKey(key); + return biome; + } + + @SuppressWarnings("unchecked") + private static BukkitBiomePolicy policy(Engine engine, NativeBiomeRegistry registry, + String fallback) throws Exception { + BukkitBiomePolicy policy = mock(BukkitBiomePolicy.class, CALLS_REAL_METHODS); + set(policy, "engine", engine); + set(policy, "biomeCustomRegistry", registry); + set(policy, "biomeRegistry", registry); + set(policy, "fallbackBiome", fallback); + set(policy, "runtimeBiomeStates", new ConcurrentHashMap<>()); + set(policy, "noiseBiomeCache", new ConcurrentHashMap<>()); + return policy; + } + + private static void set(Object target, String name, Object value) throws Exception { + Field field = BukkitBiomePolicy.class.getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitSubterrainBiomePrecedenceTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitSubterrainBiomePrecedenceTest.java index 3020de2d8..9730ac225 100644 --- a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitSubterrainBiomePrecedenceTest.java +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitSubterrainBiomePrecedenceTest.java @@ -36,10 +36,12 @@ public class BukkitSubterrainBiomePrecedenceTest { owner.setAccessible(true); owner.set(policy, engine); DimensionStackLayout stack = mock(DimensionStackLayout.class); + Class chunkResolution = Class.forName(BukkitBiomePolicy.class.getName() + "$ChunkResolution"); Method resolve = BukkitBiomePolicy.class.getDeclaredMethod("resolveBiomeResolution", int.class, int.class, - int.class, IrisDimensionCarvingResolver.State.class, boolean.class, DimensionStackLayout.class); + int.class, IrisDimensionCarvingResolver.State.class, boolean.class, DimensionStackLayout.class, + chunkResolution); resolve.setAccessible(true); - Object resolved = resolve.invoke(policy, 3, 40, -5, null, true, stack); + Object resolved = resolve.invoke(policy, 3, 40, -5, null, true, stack, null); assertSame(feature, field(resolved, "irisBiome")); assertSame(host, field(resolved, "dimension")); assertEquals(true, field(resolved, "underground")); diff --git a/core/src/test/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndexCacheTest.java b/core/src/test/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndexCacheTest.java new file mode 100644 index 000000000..2796a23f3 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndexCacheTest.java @@ -0,0 +1,69 @@ +package art.arcane.iris.structure.nativegen; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NativeStructureVolumeIndexCacheTest { + @Test + public void concurrentQueriesBuildEachOriginOnceAndAnswerIdentically() throws Exception { + Map builds = new ConcurrentHashMap<>(); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> { + builds.computeIfAbsent(((long) chunkX << 32) ^ (chunkZ & 0xffffffffL), key -> new AtomicInteger()).incrementAndGet(); + return volumesAt(chunkX, chunkZ); + }); + Engine engine = mock(Engine.class); + when(engine.getCacheID()).thenReturn(7); + KList expected = NativeStructureVolumeIndex.forTesting( + (ignored, chunkX, chunkZ) -> volumesAt(chunkX, chunkZ)).resolve(engine, 0, 0, 47, 47); + ExecutorService pool = Executors.newFixedThreadPool(8); + try { + List>> results = new ArrayList<>(); + for (int task = 0; task < 32; task++) { + results.add(pool.submit(() -> index.resolve(engine, 0, 0, 47, 47))); + } + for (Future> result : results) { + assertEquals(expected, result.get(30, TimeUnit.SECONDS)); + } + } finally { + pool.shutdown(); + assertTrue(pool.awaitTermination(30, TimeUnit.SECONDS)); + } + for (AtomicInteger count : builds.values()) { + assertEquals(1, count.get()); + } + int distinct = builds.size(); + index.evictRuntime(7); + assertEquals(expected, index.resolve(engine, 0, 0, 47, 47)); + assertEquals(distinct, builds.size()); + for (AtomicInteger count : builds.values()) { + assertEquals(2, count.get()); + } + } + + private static KList volumesAt(int chunkX, int chunkZ) { + if (Math.floorMod(chunkX * 31 + chunkZ * 17, 5) != 0) { + return NativeStructureVolume.NONE; + } + int minX = chunkX << 4; + int minZ = chunkZ << 4; + return new KList<>(new NativeStructureVolume("test:" + chunkX + "_" + chunkZ, + minX - 20, -10, minZ - 20, minX + 36, 40, minZ + 36, null)); + } +} diff --git a/core/src/test/java/art/arcane/iris/world/history/NativeTerrainReceiptTest.java b/core/src/test/java/art/arcane/iris/world/history/NativeTerrainReceiptTest.java index 3d6dbf755..1c34615fd 100644 --- a/core/src/test/java/art/arcane/iris/world/history/NativeTerrainReceiptTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/NativeTerrainReceiptTest.java @@ -60,6 +60,15 @@ public class NativeTerrainReceiptTest { } } + @Test + public void repeatedEncodingOnOneThreadKeepsLegacyBytes() throws Exception { + for (int round = 0; round < 6; round++) { + SavedTerrainChunk chunk = chunk(round % 2 == 0); + String epoch = "epoch-" + round; + assertArrayEquals(legacyEncode(chunk, round, epoch), NativeTerrainReceipt.encode(chunk, round, epoch)); + } + } + @Test public void saturatedStringCachesPreserveModifiedUtfBytes() throws Exception { for (int padding : new int[]{0, 300}) { diff --git a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeStoreWriteTest.java b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeStoreWriteTest.java index d7eaa570e..15c2d6011 100644 --- a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeStoreWriteTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeStoreWriteTest.java @@ -60,6 +60,16 @@ public class SavedBiomeStoreWriteTest { assertArrayEquals(expectedRegion(), Files.readAllBytes(regionPath(root))); } + @Test + public void consecutiveRecordsKeepIndependentlyCompressedBytes() throws Exception { + Path root = temporaryFolder.newFolder().toPath(); + SavedBiomeStore store = SavedBiomeStore.open(root); + for (int x = 0; x < 4; x++) { + store.claimAndPersist(chunk(x, 3L)); + } + assertArrayEquals(expectedRegion(4), Files.readAllBytes(regionPath(root))); + } + @Test public void relaxedDurabilityPreservesRecordsWithoutForcingEachAppend() throws Exception { String previousMode = System.getProperty(Durability.MODE_PROPERTY); @@ -379,6 +389,10 @@ public class SavedBiomeStoreWriteTest { } private static byte[] expectedRegion() throws IOException { + return expectedRegion(1); + } + + private static byte[] expectedRegion(int records) throws IOException { ByteArrayOutputStream raw = new ByteArrayOutputStream(); DataOutputStream data = new DataOutputStream(raw); data.writeLong(3L); @@ -392,17 +406,6 @@ public class SavedBiomeStoreWriteTest { data.writeInt(1); data.write(new byte[]{0, 0, 1, (byte) 0x80, 3, 0}); data.write(new byte[256]); - ByteArrayOutputStream body = new ByteArrayOutputStream(); - DataOutputStream coordinates = new DataOutputStream(body); - coordinates.writeInt(0); - coordinates.writeInt(0); - coordinates.writeInt(raw.size()); - Deflater deflater = new Deflater(Deflater.BEST_SPEED); - try (DeflaterOutputStream compressed = new DeflaterOutputStream(coordinates, deflater)) { - raw.writeTo(compressed); - } finally { - deflater.end(); - } ByteArrayOutputStream region = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(region); output.writeInt(0x4942494F); @@ -410,9 +413,22 @@ public class SavedBiomeStoreWriteTest { output.writeInt(0); output.writeInt(0); output.writeInt(checksum(region.toByteArray())); - output.writeInt(body.size()); - output.write(body.toByteArray()); - output.writeInt(checksum(body.toByteArray())); + for (int x = 0; x < records; x++) { + ByteArrayOutputStream body = new ByteArrayOutputStream(); + DataOutputStream coordinates = new DataOutputStream(body); + coordinates.writeInt(x); + coordinates.writeInt(0); + coordinates.writeInt(raw.size()); + Deflater deflater = new Deflater(Deflater.BEST_SPEED); + try (DeflaterOutputStream compressed = new DeflaterOutputStream(coordinates, deflater)) { + raw.writeTo(compressed); + } finally { + deflater.end(); + } + output.writeInt(body.size()); + output.write(body.toByteArray()); + output.writeInt(checksum(body.toByteArray())); + } return region.toByteArray(); }