Skip to content

Commit

Permalink
Work around whatever changed in 24w07a w.r.t. chunk availability duri…
Browse files Browse the repository at this point in the history
…ng worldgen.

- Fix crashes when using BiomePerimeters since 24w07a
  • Loading branch information
gniftygnome committed Feb 28, 2024
1 parent 911e103 commit 26a3ee2
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
import com.google.common.cache.LoadingCache;
import com.terraformersmc.biolith.api.biomeperimeters.BiomePerimeters;
import com.terraformersmc.biolith.impl.Biolith;
import com.terraformersmc.biolith.impl.compat.VanillaCompat;
import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.*;
import net.minecraft.world.ChunkRegion;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.source.BiomeAccess;
import net.minecraft.world.biome.source.BiomeCoords;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Hashtable;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
* BiomePerimetersImpl
Expand All @@ -38,12 +44,11 @@ public class BiomePerimetersImpl implements BiomePerimeters {
private final int checkDistance;

public static final int MAX_HORIZON = 256;
private static final long COMPACTION_TIMER_TICKS = 300;

private final LoadingCache<ChunkPos, CacheRecord> caches =
CacheBuilder.newBuilder()
.maximumSize(4096)
.expireAfterAccess(30, TimeUnit.SECONDS)
.expireAfterAccess(300, TimeUnit.SECONDS)
.weakValues()
.build(new CacheLoader<>() {
@Override
Expand All @@ -52,7 +57,7 @@ public class BiomePerimetersImpl implements BiomePerimeters {
}
});

private static final int MAX_THREAD_LOCAL_CACHE_SIZE = 128;
private static final int MAX_THREAD_LOCAL_CACHE_SIZE = 1024;
private final ThreadLocal<Object2ObjectLinkedOpenHashMap<ChunkPos, CacheRecord>> threadLocalCaches =
ThreadLocal.withInitial(Object2ObjectLinkedOpenHashMap::new);

Expand Down Expand Up @@ -116,9 +121,20 @@ public int getPerimeterDistance(BiomeAccess biomeAccess, BlockPos pos) {

final var threadLocalCache = threadLocalCaches.get();

// Work around Mojang making it illegal to access nascent chunks for biome lookup.
Function<BlockPos, RegistryEntry<Biome>> getBiomeFunction = biomeAccess::getBiome;
if (biomeAccess.storage instanceof ChunkRegion chunkRegion) {
ServerWorld world = chunkRegion.world;
getBiomeFunction = (blockPos) -> world.getGeneratorStoredBiome(
BiomeCoords.fromBlock(blockPos.getX()),
BiomeCoords.fromBlock(blockPos.getY()),
BiomeCoords.fromBlock(blockPos.getZ())
);
}

// If we are on the perimeter, avoid some difficult "edge" cases (har har) by short-circuiting.
for (EightWayDirection direction : EightWayDirection.values()) {
if (!checkBiome(biomeAccess, pos.add(direction.getOffsetX(), 0, direction.getOffsetZ()), threadLocalCache)) {
if (!checkBiome(getBiomeFunction, pos.add(direction.getOffsetX(), 0, direction.getOffsetZ()), threadLocalCache)) {
return 0;
}
}
Expand All @@ -144,15 +160,15 @@ public int getPerimeterDistance(BiomeAccess biomeAccess, BlockPos pos) {
for (int radius = 0; radius < horizon; radius++) {
iterPos = pos.add(dx * radius, 0, dz * radius);
final CacheRecord cache = getCache(threadLocalCache, new ChunkPos(iterPos));
if (cache.perimeters.containsKey(CacheRecord.getIndex(iterPos)) || !checkBiome(biomeAccess, iterPos.add(dx, 0, dz), threadLocalCache)) {
int localMinimum = this.checkPerimeter(biomeAccess, pos, iterPos, direction, threadLocalCache);
if (cache.perimeters.containsKey(CacheRecord.getIndex(iterPos)) || !checkBiome(getBiomeFunction, iterPos.add(dx, 0, dz), threadLocalCache)) {
int localMinimum = this.checkPerimeter(getBiomeFunction, pos, iterPos, direction, threadLocalCache);
if (localMinimum >= 0) {
minimum = Math.min(minimum, localMinimum);
break;
} else {
// Power on through a small biome inclusion we found.
for (++radius; radius < horizon; radius++) {
if (checkBiome(biomeAccess, pos.add(dx * radius, 0, dz * radius), threadLocalCache)) {
if (checkBiome(getBiomeFunction, pos.add(dx * radius, 0, dz * radius), threadLocalCache)) {
++radius;
break;
}
Expand All @@ -166,7 +182,7 @@ public int getPerimeterDistance(BiomeAccess biomeAccess, BlockPos pos) {
return rationalizeDistance(pos, minimum, threadLocalCache);
}

private int checkPerimeter(BiomeAccess biomeAccess, BlockPos centerPos, BlockPos perimeterPos, EightWayDirection direction, Object2ObjectLinkedOpenHashMap<ChunkPos, CacheRecord> threadLocalCache) {
private int checkPerimeter(Function<BlockPos, RegistryEntry<Biome>> getBiomeFunction, BlockPos centerPos, BlockPos perimeterPos, EightWayDirection direction, Object2ObjectLinkedOpenHashMap<ChunkPos, CacheRecord> threadLocalCache) {
BiomePerimeterPoint current;
EightWayDirection orientation;
double minimum;
Expand All @@ -189,7 +205,7 @@ private int checkPerimeter(BiomeAccess biomeAccess, BlockPos centerPos, BlockPos
orientation = getEightWayClockwiseRotation(orientation, 5);
for (int rotation = 2; rotation < 8; rotation++) {
orientation = getEightWayClockwiseRotation(orientation, 1);
if (!checkBiome(biomeAccess, current.pos.add(orientation.getOffsetX(), 0, orientation.getOffsetZ()), threadLocalCache)) {
if (!checkBiome(getBiomeFunction, current.pos.add(orientation.getOffsetX(), 0, orientation.getOffsetZ()), threadLocalCache)) {
orientation = getEightWayClockwiseRotation(orientation, -1);
BlockPos prospect = current.pos.add(orientation.getOffsetX(), 0, orientation.getOffsetZ());
BiomePerimeterPoint prospectPoint = getCache(threadLocalCache, new ChunkPos(prospect)).perimeters.get(CacheRecord.getIndex(prospect));
Expand Down Expand Up @@ -235,7 +251,7 @@ private int checkPerimeter(BiomeAccess biomeAccess, BlockPos centerPos, BlockPos
orientation = getEightWayClockwiseRotation(orientation, 3);
for (int rotation = 2; rotation < 8; rotation++) {
orientation = getEightWayClockwiseRotation(orientation, -1);
if (!checkBiome(biomeAccess, current.pos.add(orientation.getOffsetX(), 0, orientation.getOffsetZ()), threadLocalCache)) {
if (!checkBiome(getBiomeFunction, current.pos.add(orientation.getOffsetX(), 0, orientation.getOffsetZ()), threadLocalCache)) {
orientation = getEightWayClockwiseRotation(orientation, 1);
BlockPos prospect = current.pos.add(orientation.getOffsetX(), 0, orientation.getOffsetZ());
BiomePerimeterPoint prospectPoint = getCache(threadLocalCache, new ChunkPos(prospect)).perimeters.get(CacheRecord.getIndex(prospect));
Expand Down Expand Up @@ -298,13 +314,13 @@ private EightWayDirection getEightWayRelation(BlockPos posA, BlockPos posB) {
}
}

private boolean checkBiome(BiomeAccess biomeAccess, BlockPos pos, Object2ObjectLinkedOpenHashMap<ChunkPos, CacheRecord> threadLocalCache) {
private boolean checkBiome(Function<BlockPos, RegistryEntry<Biome>> getBiomeFunction, BlockPos pos, Object2ObjectLinkedOpenHashMap<ChunkPos, CacheRecord> threadLocalCache) {
/* Distance values in biomeCache:
* -1: special value indicating pos is in-biome but the perimeter distance is unknown
* 0: value indicating pos is not in-biome
* >0: pos is in-biome and the value indicates the distance to the perimeter
*/
return (getCache(threadLocalCache, new ChunkPos(pos)).biomeCache.computeIfAbsent(CacheRecord.getIndex(pos), (key) -> biomeAccess.getBiome(pos).value().equals(biome) ? -1 : 0) != 0);
return (getCache(threadLocalCache, new ChunkPos(pos)).biomeCache.computeIfAbsent(CacheRecord.getIndex(pos), (key) -> getBiomeFunction.apply(pos).value().equals(biome) ? -1 : 0) != 0);
}

private int rationalizeDistance(BlockPos pos, float proposed, Object2ObjectLinkedOpenHashMap<ChunkPos, CacheRecord> threadLocalCache) {
Expand All @@ -329,6 +345,22 @@ private int rationalizeDistance(BlockPos pos, float proposed, Object2ObjectLinke
return distance;
}

private static Function<BlockPos, RegistryEntry<Biome>> selectGetBiomeFunction(BiomeAccess biomeAccess) {
/*
* When the BiomeAccess uses a ChunkRegion, it will deny lookup rather than creating a Chunk.
* This implementation bypasses the BiomeAccess to use direct biome lookups.
* We are forced to reimplement the getBiome smoothing function.
*/
if (biomeAccess.storage instanceof ChunkRegion chunkRegion) {
ServerWorld world = chunkRegion.world;

return (pos) -> VanillaCompat.callFunctionWithSmoothedBiomeCoords(world::getGeneratorStoredBiome, pos, world.getSeed());
}

// Fall back to the vanilla getBiome, which may work with some other biome access implementations...
return biomeAccess::getBiome;
}

/**
* BiomePerimetersImpl.getOrCreateInstance()
* <p></p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import com.terraformersmc.biolith.impl.biome.BiolithFittestNodes;
import com.terraformersmc.biolith.impl.biome.BiomeCoordinator;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.BiomeKeys;
import net.minecraft.world.biome.source.BiomeAccess;
import net.minecraft.world.biome.source.BiomeCoords;
import net.minecraft.world.biome.source.util.MultiNoiseUtil;
import net.minecraft.world.gen.densityfunction.DensityFunction;
import org.apache.commons.lang3.function.TriFunction;

public class VanillaCompat {
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -92,4 +95,52 @@ private static MultiNoiseUtil.NoiseHypercube createNoiseHypercube(MultiNoiseUtil
assert parameters.length == 6;
return MultiNoiseUtil.createNoiseHypercube(parameters[0], parameters[1], parameters[2], parameters[3], parameters[4], parameters[5], 0L);
}

/**
* This method duplicates the processing of {@link BiomeAccess#getBiome} which smooths out the 4x4x4
* biome pixels using the world seed as a source of "randomness". It then calls the provided getBiome
* function and returns whatever the function does. Thus, use of other getBiome implementations is
* possible without reimplementing chunk storage.
*
* @param function A getBiome tri-function mapping biome coordinates (x, y, z) to RegistryEntry of Biome
* @param pos The BlockPos to target with function
* @param seed The seed of the relevant world
* @return RegistryEntry of Biome returned for the smoothed biome coordinates
*/
public static RegistryEntry<Biome> callFunctionWithSmoothedBiomeCoords(TriFunction<Integer, Integer, Integer, RegistryEntry<Biome>> function, BlockPos pos, long seed) {
int centerX = pos.getX() - 2;
int centerY = pos.getY() - 2;
int centerZ = pos.getZ() - 2;
int biomeX = centerX >> 2;
int biomeY = centerY >> 2;
int biomeZ = centerZ >> 2;
double fractionalX = (double) (centerX & 3) / 4.0;
double fractionalY = (double) (centerY & 3) / 4.0;
double fractionalZ = (double) (centerZ & 3) / 4.0;

int offsets = 0;
double minimum = Double.POSITIVE_INFINITY;
for (int option = 0; option < 8; ++option) {
boolean offsetX = (option & 4) == 0;
boolean offsetY = (option & 2) == 0;
boolean offsetZ = (option & 1) == 0;
double preference = BiomeAccess.method_38106(
seed,
offsetX ? biomeX : biomeX + 1,
offsetY ? biomeY : biomeY + 1,
offsetZ ? biomeZ : biomeZ + 1,
offsetX ? fractionalX : fractionalX - 1.0,
offsetY ? fractionalY : fractionalY - 1.0,
offsetZ ? fractionalZ : fractionalZ - 1.0);
if (minimum > preference) {
minimum = preference;
offsets = option;
}
}

return function.apply(
(offsets & 4) == 0 ? biomeX : biomeX + 1,
(offsets & 2) == 0 ? biomeY : biomeY + 1,
(offsets & 1) == 0 ? biomeZ : biomeZ + 1);
}
}
3 changes: 3 additions & 0 deletions common/src/main/resources/biolith.accesswidener
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
accessWidener v1 named

accessible field net/minecraft/server/world/ThreadedAnvilChunkStorage chunkGenerator Lnet/minecraft/world/gen/chunk/ChunkGenerator;
accessible method net/minecraft/world/biome/source/BiomeAccess method_38106 (JIIIDDD)D
accessible field net/minecraft/world/biome/source/BiomeAccess storage Lnet/minecraft/world/biome/source/BiomeAccess$Storage;
accessible class net/minecraft/world/biome/source/MultiNoiseBiomeSourceParameterList$Preset$BiomeSourceFunction
accessible method net/minecraft/world/biome/source/util/MultiNoiseUtil$Entries getValue (Lnet/minecraft/world/biome/source/util/MultiNoiseUtil$NoiseValuePoint;Lnet/minecraft/world/biome/source/util/MultiNoiseUtil$NodeDistanceFunction;)Ljava/lang/Object;
accessible field net/minecraft/world/biome/source/util/MultiNoiseUtil$Entries tree Lnet/minecraft/world/biome/source/util/MultiNoiseUtil$SearchTree;
Expand All @@ -17,5 +19,6 @@ extendable class net/minecraft/world/biome/source/util/MultiNoiseUtil$SearchT
accessible method net/minecraft/world/biome/source/util/MultiNoiseUtil$SearchTree$TreeLeafNode <init> (Lnet/minecraft/world/biome/source/util/MultiNoiseUtil$NoiseHypercube;Ljava/lang/Object;)V
accessible field net/minecraft/world/biome/source/util/MultiNoiseUtil$SearchTree$TreeLeafNode value Ljava/lang/Object;
accessible class net/minecraft/world/gen/surfacebuilder/MaterialRules$BlockStateRule
accessible field net/minecraft/world/ChunkRegion world Lnet/minecraft/server/world/ServerWorld;
accessible class net/minecraft/world/gen/surfacebuilder/MaterialRules$MaterialRuleContext
accessible method net/minecraft/world/gen/surfacebuilder/MaterialRules$MaterialRuleContext estimateSurfaceHeight ()I
1 change: 0 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ mod_version=dev
maven_group=com.terraformersmc
#enabled_platforms=fabric,forge,neoforge
enabled_platforms=fabric
#fabric.loom.multiProjectOptimisation=true

# Common
minecraft_version=24w07a
Expand Down

0 comments on commit 26a3ee2

Please sign in to comment.