mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Merged platform diet
- Per-chunk biome stage resolver, subterrain candidate gate, section-walk heightmaps, reused history compressors # Conflicts: # core/src/main/java/art/arcane/iris/generation/runtime/Engine.java
This commit is contained in:
commit
c62a31e16b
14 files changed
+554
-149
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<H, V> biomeCustomRegistry;
|
||||
@@ -64,7 +66,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<String> historicalKey = engine.getComplex().historicalPhysicalBiomeKeyAt(
|
||||
IrisComplex complex = chunk == null ? engine.getComplex() : chunk.complex;
|
||||
Optional<String> 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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
}
|
||||
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
)
|
||||
: 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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
}
|
||||
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
this.blockX = blockX;
|
||||
this.blockY = blockY;
|
||||
this.blockZ = blockZ;
|
||||
this.rng = rng;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChunkVisibleResolver implements VisibleResolver<H> {
|
||||
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<H, V> 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<H, V> implements NativeBiomeSourcePolicy<H>
|
||||
}
|
||||
@Override
|
||||
public VisibleResolver<H> visibleResolver() {
|
||||
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
|
||||
return (x, y, z) -> getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, state);
|
||||
return new ChunkVisibleResolver();
|
||||
}
|
||||
|
||||
}
|
||||
+60
-43
@@ -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<RuntimeChunkKey, KList<NativeStructureVolume>> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS);
|
||||
private final Map<RuntimeChunkKey, KList<NativeStructureVolume>> 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<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> originBuilds =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> queryBuilds =
|
||||
@@ -180,12 +181,10 @@ public final class NativeStructureVolumeIndex {
|
||||
Engine engine, int chunkX, int chunkZ, Integer pinnedRuntimeId
|
||||
) {
|
||||
if (pinnedRuntimeId != null) {
|
||||
synchronized (originCache) {
|
||||
KList<NativeStructureVolume> cached = originCache.get(
|
||||
new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ)));
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
KList<NativeStructureVolume> 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<NativeStructureVolume> cached(
|
||||
Map<RuntimeChunkKey, KList<NativeStructureVolume>> cache,
|
||||
VolumeCache volumes,
|
||||
ConcurrentHashMap<RuntimeChunkKey, CompletableFuture<KList<NativeStructureVolume>>> builds,
|
||||
RuntimeChunkKey key,
|
||||
Supplier<KList<NativeStructureVolume>> loader
|
||||
) {
|
||||
synchronized (cache) {
|
||||
KList<NativeStructureVolume> hit = cache.get(key);
|
||||
if (hit != null) {
|
||||
return hit;
|
||||
}
|
||||
KList<NativeStructureVolume> hit = volumes.get(key);
|
||||
if (hit != null) {
|
||||
return hit;
|
||||
}
|
||||
|
||||
CompletableFuture<KList<NativeStructureVolume>> 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<NativeStructureVolume> published = cache.get(key);
|
||||
if (published != null) {
|
||||
future.complete(published);
|
||||
builds.remove(key, future);
|
||||
return published;
|
||||
}
|
||||
KList<NativeStructureVolume> published = volumes.get(key);
|
||||
if (published != null) {
|
||||
future.complete(published);
|
||||
builds.remove(key, future);
|
||||
return published;
|
||||
}
|
||||
|
||||
try {
|
||||
KList<NativeStructureVolume> 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<RuntimeChunkKey, KList<NativeStructureVolume>> lru(int capacity) {
|
||||
return new LinkedHashMap<>(16, 0.75F, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(
|
||||
Map.Entry<RuntimeChunkKey, KList<NativeStructureVolume>> eldest
|
||||
) {
|
||||
return size() > capacity;
|
||||
private static final class VolumeCache {
|
||||
private final int capacity;
|
||||
private final ConcurrentHashMap<RuntimeChunkKey, KList<NativeStructureVolume>> entries = new ConcurrentHashMap<>();
|
||||
private final ConcurrentLinkedQueue<RuntimeChunkKey> insertionOrder = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private VolumeCache(int capacity) {
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
private KList<NativeStructureVolume> get(RuntimeChunkKey key) {
|
||||
return entries.get(key);
|
||||
}
|
||||
|
||||
private void publish(RuntimeChunkKey key, KList<NativeStructureVolume> 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) {
|
||||
|
||||
@@ -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<ChunkGenerationSemantics> 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) {
|
||||
|
||||
@@ -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<Deflater> 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;
|
||||
|
||||
@@ -36,7 +36,8 @@ public final class SavedBiomeCapture {
|
||||
Math.addExact(startX, 15), Math.addExact(startZ, 15)).isEmpty();
|
||||
Map<Integer, List<SavedBiomeChunk.Span>> 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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Deflater> 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;
|
||||
|
||||
+111
@@ -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.<Integer>getArgument(0) * 7 + invocation.<Integer>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.<Integer>getArgument(0) + invocation.<Integer>getArgument(1), 8) == 0 ? custom : surface);
|
||||
when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class)))
|
||||
.thenAnswer(invocation -> invocation.<Integer>getArgument(1) % 3 == 0 ? custom : cave);
|
||||
when(engine.getSubterrainBiome(anyInt(), anyInt(), anyInt())).thenAnswer(invocation ->
|
||||
invocation.<Integer>getArgument(0) == 4 && invocation.<Integer>getArgument(1) == 40 ? feature : null);
|
||||
when(engine.subterrainAbsentInChunk(1, 0)).thenReturn(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
NativeBiomeRegistry<String, String> registry = mock(NativeBiomeRegistry.class);
|
||||
when(registry.lookupCustom(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
BukkitBiomePolicy<String, String> policy = policy(engine, registry, "fallback");
|
||||
|
||||
for (int chunkX = 0; chunkX <= 1; chunkX++) {
|
||||
NativeBiomeSourcePolicy.VisibleResolver<String> 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<String, String> policy(Engine engine, NativeBiomeRegistry<String, String> registry,
|
||||
String fallback) throws Exception {
|
||||
BukkitBiomePolicy<String, String> 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);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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"));
|
||||
|
||||
+69
@@ -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<Long, AtomicInteger> 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<NativeStructureVolume> expected = NativeStructureVolumeIndex.forTesting(
|
||||
(ignored, chunkX, chunkZ) -> volumesAt(chunkX, chunkZ)).resolve(engine, 0, 0, 47, 47);
|
||||
ExecutorService pool = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
List<Future<KList<NativeStructureVolume>>> results = new ArrayList<>();
|
||||
for (int task = 0; task < 32; task++) {
|
||||
results.add(pool.submit(() -> index.resolve(engine, 0, 0, 47, 47)));
|
||||
}
|
||||
for (Future<KList<NativeStructureVolume>> 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<NativeStructureVolume> 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));
|
||||
}
|
||||
}
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user