package net.minecraft.world.level.biome; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; import net.minecraft.core.BlockPos; import net.minecraft.core.QuartPos; import net.minecraft.util.ExtraCodecs; import net.minecraft.util.Mth; import net.minecraft.world.level.biome.Climate.RTree.Leaf; import net.minecraft.world.level.biome.Climate.RTree.Node; import net.minecraft.world.level.biome.Climate.RTree.SubTree; import net.minecraft.world.level.biome.Climate.SpawnFinder.Result; import net.minecraft.world.level.levelgen.DensityFunction; import net.minecraft.world.level.levelgen.DensityFunctions; import net.minecraft.world.level.levelgen.DensityFunction.SinglePointContext; import org.jetbrains.annotations.Nullable; public class Climate { private static final boolean DEBUG_SLOW_BIOME_SEARCH = false; private static final float QUANTIZATION_FACTOR = 10000.0F; @VisibleForTesting protected static final int PARAMETER_COUNT = 7; public static Climate.TargetPoint target(float temperature, float humidity, float continentalness, float erosion, float depth, float weirdness) { return new Climate.TargetPoint( quantizeCoord(temperature), quantizeCoord(humidity), quantizeCoord(continentalness), quantizeCoord(erosion), quantizeCoord(depth), quantizeCoord(weirdness) ); } public static Climate.ParameterPoint parameters( float temperature, float humidity, float continentalness, float erosion, float depth, float weirdness, float offset ) { return new Climate.ParameterPoint( Climate.Parameter.point(temperature), Climate.Parameter.point(humidity), Climate.Parameter.point(continentalness), Climate.Parameter.point(erosion), Climate.Parameter.point(depth), Climate.Parameter.point(weirdness), quantizeCoord(offset) ); } public static Climate.ParameterPoint parameters( Climate.Parameter temperature, Climate.Parameter humidity, Climate.Parameter continentalness, Climate.Parameter erosion, Climate.Parameter depth, Climate.Parameter weirdness, float offset ) { return new Climate.ParameterPoint(temperature, humidity, continentalness, erosion, depth, weirdness, quantizeCoord(offset)); } public static long quantizeCoord(float coord) { return (long)(coord * 10000.0F); } public static float unquantizeCoord(long coord) { return (float)coord / 10000.0F; } public static Climate.Sampler empty() { DensityFunction densityFunction = DensityFunctions.zero(); return new Climate.Sampler(densityFunction, densityFunction, densityFunction, densityFunction, densityFunction, densityFunction, List.of()); } public static BlockPos findSpawnPosition(List points, Climate.Sampler sampler) { return (new Climate.SpawnFinder(points, sampler)).result.location(); } interface DistanceMetric { long distance(Node node, long[] ls); } public record Parameter(long min, long max) { public static final Codec CODEC = ExtraCodecs.intervalCodec( Codec.floatRange(-2.0F, 2.0F), "min", "max", (float_, float2) -> float_.compareTo(float2) > 0 ? DataResult.error(() -> "Cannon construct interval, min > max (" + float_ + " > " + float2 + ")") : DataResult.success(new Climate.Parameter(Climate.quantizeCoord(float_), Climate.quantizeCoord(float2))), parameter -> Climate.unquantizeCoord(parameter.min()), parameter -> Climate.unquantizeCoord(parameter.max()) ); public static Climate.Parameter point(float value) { return span(value, value); } public static Climate.Parameter span(float min, float max) { if (min > max) { throw new IllegalArgumentException("min > max: " + min + " " + max); } else { return new Climate.Parameter(Climate.quantizeCoord(min), Climate.quantizeCoord(max)); } } public static Climate.Parameter span(Climate.Parameter min, Climate.Parameter max) { if (min.min() > max.max()) { throw new IllegalArgumentException("min > max: " + min + " " + max); } else { return new Climate.Parameter(min.min(), max.max()); } } public String toString() { return this.min == this.max ? String.format(Locale.ROOT, "%d", this.min) : String.format(Locale.ROOT, "[%d-%d]", this.min, this.max); } public long distance(long pointValue) { long l = pointValue - this.max; long m = this.min - pointValue; return l > 0L ? l : Math.max(m, 0L); } public long distance(Climate.Parameter parameter) { long l = parameter.min() - this.max; long m = this.min - parameter.max(); return l > 0L ? l : Math.max(m, 0L); } public Climate.Parameter span(@Nullable Climate.Parameter param) { return param == null ? this : new Climate.Parameter(Math.min(this.min, param.min()), Math.max(this.max, param.max())); } } public static class ParameterList { private final List> values; private final Climate.RTree index; public static Codec> codec(MapCodec codec) { return ExtraCodecs.nonEmptyList( RecordCodecBuilder.create( instance -> instance.group(Climate.ParameterPoint.CODEC.fieldOf("parameters").forGetter(Pair::getFirst), codec.forGetter(Pair::getSecond)) .apply(instance, Pair::of) ) .listOf() ) .xmap(Climate.ParameterList::new, Climate.ParameterList::values); } public ParameterList(List> values) { this.values = values; this.index = Climate.RTree.create(values); } public List> values() { return this.values; } public T findValue(Climate.TargetPoint targetPoint) { return this.findValueIndex(targetPoint); } @VisibleForTesting public T findValueBruteForce(Climate.TargetPoint targetPoint) { Iterator> iterator = this.values().iterator(); Pair pair = (Pair)iterator.next(); long l = pair.getFirst().fitness(targetPoint); T object = pair.getSecond(); while (iterator.hasNext()) { Pair pair2 = (Pair)iterator.next(); long m = pair2.getFirst().fitness(targetPoint); if (m < l) { l = m; object = pair2.getSecond(); } } return object; } public T findValueIndex(Climate.TargetPoint targetPoint) { return this.findValueIndex(targetPoint, Node::distance); } protected T findValueIndex(Climate.TargetPoint targetPoint, Climate.DistanceMetric distanceMetric) { return this.index.search(targetPoint, distanceMetric); } } public record ParameterPoint( Climate.Parameter temperature, Climate.Parameter humidity, Climate.Parameter continentalness, Climate.Parameter erosion, Climate.Parameter depth, Climate.Parameter weirdness, long offset ) { public static final Codec CODEC = RecordCodecBuilder.create( instance -> instance.group( Climate.Parameter.CODEC.fieldOf("temperature").forGetter(parameterPoint -> parameterPoint.temperature), Climate.Parameter.CODEC.fieldOf("humidity").forGetter(parameterPoint -> parameterPoint.humidity), Climate.Parameter.CODEC.fieldOf("continentalness").forGetter(parameterPoint -> parameterPoint.continentalness), Climate.Parameter.CODEC.fieldOf("erosion").forGetter(parameterPoint -> parameterPoint.erosion), Climate.Parameter.CODEC.fieldOf("depth").forGetter(parameterPoint -> parameterPoint.depth), Climate.Parameter.CODEC.fieldOf("weirdness").forGetter(parameterPoint -> parameterPoint.weirdness), Codec.floatRange(0.0F, 1.0F).fieldOf("offset").xmap(Climate::quantizeCoord, Climate::unquantizeCoord).forGetter(parameterPoint -> parameterPoint.offset) ) .apply(instance, Climate.ParameterPoint::new) ); long fitness(Climate.TargetPoint point) { return Mth.square(this.temperature.distance(point.temperature)) + Mth.square(this.humidity.distance(point.humidity)) + Mth.square(this.continentalness.distance(point.continentalness)) + Mth.square(this.erosion.distance(point.erosion)) + Mth.square(this.depth.distance(point.depth)) + Mth.square(this.weirdness.distance(point.weirdness)) + Mth.square(this.offset); } protected List parameterSpace() { return ImmutableList.of( this.temperature, this.humidity, this.continentalness, this.erosion, this.depth, this.weirdness, new Climate.Parameter(this.offset, this.offset) ); } } protected static final class RTree { private static final int CHILDREN_PER_NODE = 6; private final Node root; private final ThreadLocal> lastResult = new ThreadLocal(); private RTree(Node root) { this.root = root; } public static Climate.RTree create(List> nodes) { if (nodes.isEmpty()) { throw new IllegalArgumentException("Need at least one value to build the search tree."); } else { int i = ((Climate.ParameterPoint)((Pair)nodes.get(0)).getFirst()).parameterSpace().size(); if (i != 7) { throw new IllegalStateException("Expecting parameter space to be 7, got " + i); } else { List> list = (List>)nodes.stream() .map(pair -> new Leaf((Climate.ParameterPoint)pair.getFirst(), pair.getSecond())) .collect(Collectors.toCollection(ArrayList::new)); return new Climate.RTree<>(build(i, list)); } } } private static Node build(int paramSpaceSize, List> children) { if (children.isEmpty()) { throw new IllegalStateException("Need at least one child to build a node"); } else if (children.size() == 1) { return (Node)children.get(0); } else if (children.size() <= 6) { children.sort(Comparator.comparingLong(node -> { long lx = 0L; for (int jx = 0; jx < paramSpaceSize; jx++) { Climate.Parameter parameter = node.parameterSpace[jx]; lx += Math.abs((parameter.min() + parameter.max()) / 2L); } return lx; })); return new SubTree(children); } else { long l = Long.MAX_VALUE; int i = -1; List> list = null; for (int j = 0; j < paramSpaceSize; j++) { sort(children, paramSpaceSize, j, false); List> list2 = bucketize(children); long m = 0L; for (SubTree subTree : list2) { m += cost(subTree.parameterSpace); } if (l > m) { l = m; i = j; list = list2; } } sort(list, paramSpaceSize, i, true); return new SubTree((List)list.stream().map(subTreex -> build(paramSpaceSize, Arrays.asList(subTreex.children))).collect(Collectors.toList())); } } private static void sort(List> children, int paramSpaceSize, int size, boolean absolute) { Comparator> comparator = comparator(size, absolute); for (int i = 1; i < paramSpaceSize; i++) { comparator = comparator.thenComparing(comparator((size + i) % paramSpaceSize, absolute)); } children.sort(comparator); } private static Comparator> comparator(int size, boolean absolute) { return Comparator.comparingLong(node -> { Climate.Parameter parameter = node.parameterSpace[size]; long l = (parameter.min() + parameter.max()) / 2L; return absolute ? Math.abs(l) : l; }); } private static List> bucketize(List> nodes) { List> list = Lists.>newArrayList(); List> list2 = Lists.>newArrayList(); int i = (int)Math.pow(6.0, Math.floor(Math.log(nodes.size() - 0.01) / Math.log(6.0))); for (Node node : nodes) { list2.add(node); if (list2.size() >= i) { list.add(new SubTree(list2)); list2 = Lists.>newArrayList(); } } if (!list2.isEmpty()) { list.add(new SubTree(list2)); } return list; } private static long cost(Climate.Parameter[] parameters) { long l = 0L; for (Climate.Parameter parameter : parameters) { l += Math.abs(parameter.max() - parameter.min()); } return l; } static List buildParameterSpace(List> children) { if (children.isEmpty()) { throw new IllegalArgumentException("SubTree needs at least one child"); } else { int i = 7; List list = Lists.newArrayList(); for (int j = 0; j < 7; j++) { list.add(null); } for (Node node : children) { for (int k = 0; k < 7; k++) { list.set(k, node.parameterSpace[k].span((Climate.Parameter)list.get(k))); } } return list; } } public T search(Climate.TargetPoint targetPoint, Climate.DistanceMetric distanceMetric) { long[] ls = targetPoint.toParameterArray(); Leaf leaf = this.root.search(ls, (Leaf)this.lastResult.get(), distanceMetric); this.lastResult.set(leaf); return (T)leaf.value; } } public record Sampler( DensityFunction temperature, DensityFunction humidity, DensityFunction continentalness, DensityFunction erosion, DensityFunction depth, DensityFunction weirdness, List spawnTarget ) { public Climate.TargetPoint sample(int x, int y, int z) { int i = QuartPos.toBlock(x); int j = QuartPos.toBlock(y); int k = QuartPos.toBlock(z); SinglePointContext singlePointContext = new SinglePointContext(i, j, k); return Climate.target( (float)this.temperature.compute(singlePointContext), (float)this.humidity.compute(singlePointContext), (float)this.continentalness.compute(singlePointContext), (float)this.erosion.compute(singlePointContext), (float)this.depth.compute(singlePointContext), (float)this.weirdness.compute(singlePointContext) ); } public BlockPos findSpawnPosition() { return this.spawnTarget.isEmpty() ? BlockPos.ZERO : Climate.findSpawnPosition(this.spawnTarget, this); } } static class SpawnFinder { private static final long MAX_RADIUS = 2048L; Result result; SpawnFinder(List points, Climate.Sampler sampler) { this.result = getSpawnPositionAndFitness(points, sampler, 0, 0); this.radialSearch(points, sampler, 2048.0F, 512.0F); this.radialSearch(points, sampler, 512.0F, 32.0F); } private void radialSearch(List point, Climate.Sampler sampler, float max, float min) { float f = 0.0F; float g = min; BlockPos blockPos = this.result.location(); while (g <= max) { int i = blockPos.getX() + (int)(Math.sin(f) * g); int j = blockPos.getZ() + (int)(Math.cos(f) * g); Result result = getSpawnPositionAndFitness(point, sampler, i, j); if (result.fitness() < this.result.fitness()) { this.result = result; } f += min / g; if (f > Math.PI * 2) { f = 0.0F; g += min; } } } private static Result getSpawnPositionAndFitness(List points, Climate.Sampler sampler, int x, int z) { Climate.TargetPoint targetPoint = sampler.sample(QuartPos.fromBlock(x), 0, QuartPos.fromBlock(z)); Climate.TargetPoint targetPoint2 = new Climate.TargetPoint( targetPoint.temperature(), targetPoint.humidity(), targetPoint.continentalness(), targetPoint.erosion(), 0L, targetPoint.weirdness() ); long l = Long.MAX_VALUE; for (Climate.ParameterPoint parameterPoint : points) { l = Math.min(l, parameterPoint.fitness(targetPoint2)); } long m = Mth.square((long)x) + Mth.square((long)z); long n = l * Mth.square(2048L) + m; return new Result(new BlockPos(x, 0, z), n); } } public record TargetPoint(long temperature, long humidity, long continentalness, long erosion, long depth, long weirdness) { @VisibleForTesting protected long[] toParameterArray() { return new long[]{this.temperature, this.humidity, this.continentalness, this.erosion, this.depth, this.weirdness, 0L}; } } }