Optimize world generation with version-gated runtime hooks
Select density shortcuts from compiler expressions, defer cave noise interpolation, and index biome and terrain searches. Reuse upstream feature and LOD methods, cache native noise and static light values, and synchronize fluid offset snapshots. Include upstream license notices and compatibility checks.
This commit is contained in:
parent
a45c574595
commit
84fccc92ab
75 changed files with 3062 additions and 9 deletions
|
|
@ -32,10 +32,22 @@ sourceSets {
|
|||
}
|
||||
}
|
||||
|
||||
configurations { c2me }
|
||||
|
||||
def dfcJar = providers.provider {
|
||||
zipTree(configurations.c2me.singleFile).matching {
|
||||
include 'META-INF/jars/*-opts-dfc-*.jar'
|
||||
}.files
|
||||
}
|
||||
|
||||
dependencies {
|
||||
c2me('curse.maven:c2me-533097:9344813') { transitive = false }
|
||||
compileOnly files(dfcJar)
|
||||
testImplementation files(dfcJar)
|
||||
compileOnly('mekanism:Mekanism:1.21.1-10.7.19.85') { transitive = false }
|
||||
compileOnly('curse.maven:functional-storage-556861:8459097') { transitive = false }
|
||||
compileOnly('maven.modrinth:refined-storage:lHHiI26k') { transitive = false }
|
||||
compileOnly('maven.modrinth:distanthorizons:9w34y8ai') { transitive = false }
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'org.apiguardian:apiguardian-api:1.1.2'
|
||||
compileOnly 'io.github.llamalad7:mixinextras-neoforge:0.5.3'
|
||||
|
|
|
|||
140
server/src/main/java/dev/quorate/tune/BiomeIndex.java
Normal file
140
server/src/main/java/dev/quorate/tune/BiomeIndex.java
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
public final class BiomeIndex {
|
||||
// Seven squared axis distances cannot overflow a long within this range.
|
||||
private static final long LIMIT = 1L << 28;
|
||||
|
||||
public record Result(Object first, long distance, Object second, long other) {}
|
||||
|
||||
private final Object[] nodes;
|
||||
private final Object[] keys;
|
||||
private final long[] bounds;
|
||||
private final int[] next;
|
||||
private final boolean[] branches;
|
||||
private final IdentityHashMap<Object, Integer> positions = new IdentityHashMap<>();
|
||||
private final Object nullKey;
|
||||
private final boolean narrow;
|
||||
|
||||
public BiomeIndex(
|
||||
Object root,
|
||||
Function<Object, Object[]> children,
|
||||
Function<Object, long[]> space,
|
||||
Function<Object, Object> key) {
|
||||
var order = new ArrayList<Object>();
|
||||
var ends = new ArrayList<Integer>();
|
||||
var inner = new ArrayList<Boolean>();
|
||||
flatten(root, children, order, ends, inner);
|
||||
nodes = order.toArray();
|
||||
keys = new Object[nodes.length];
|
||||
bounds = new long[nodes.length * 14];
|
||||
next = new int[nodes.length];
|
||||
branches = new boolean[nodes.length];
|
||||
nullKey = key.apply(null);
|
||||
boolean fits = true;
|
||||
for (int i = 0; i < nodes.length; i++) {
|
||||
positions.put(nodes[i], i);
|
||||
long[] extent = space.apply(nodes[i]);
|
||||
for (int axis = 0; axis < 7; axis++) {
|
||||
long low = extent[axis * 2];
|
||||
long high = extent[axis * 2 + 1];
|
||||
bounds[i * 14 + axis * 2] = low;
|
||||
bounds[i * 14 + axis * 2 + 1] = high;
|
||||
fits &= low >= -LIMIT && low <= LIMIT && high >= -LIMIT && high <= LIMIT;
|
||||
}
|
||||
next[i] = ends.get(i);
|
||||
branches[i] = inner.get(i);
|
||||
if (!branches[i]) keys[i] = key.apply(nodes[i]);
|
||||
}
|
||||
narrow = fits;
|
||||
}
|
||||
|
||||
private static void flatten(
|
||||
Object node,
|
||||
Function<Object, Object[]> children,
|
||||
ArrayList<Object> order,
|
||||
ArrayList<Integer> ends,
|
||||
ArrayList<Boolean> inner) {
|
||||
int index = order.size();
|
||||
var below = children.apply(node);
|
||||
order.add(node);
|
||||
ends.add(0);
|
||||
inner.add(below != null);
|
||||
if (below != null) for (var child : below) flatten(child, children, order, ends, inner);
|
||||
ends.set(index, order.size());
|
||||
}
|
||||
|
||||
public boolean single() {
|
||||
return !branches[0];
|
||||
}
|
||||
|
||||
private long distance(int index, long[] point, long limit, boolean capped) {
|
||||
if (index < 0) return Long.MAX_VALUE;
|
||||
long sum = 0;
|
||||
int offset = index * 14;
|
||||
for (int axis = 0; axis < 7; axis++) {
|
||||
long coordinate = point[axis];
|
||||
long above = coordinate - bounds[offset + axis * 2 + 1];
|
||||
long below = bounds[offset + axis * 2] - coordinate;
|
||||
long delta = above > 0 ? above : Math.max(below, 0L);
|
||||
sum += delta * delta;
|
||||
if (capped && sum >= limit) return sum;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private int position(Object node) {
|
||||
return node == null ? -1 : positions.get(node);
|
||||
}
|
||||
|
||||
public Result search(long[] point, Object previous, Object penultimate) {
|
||||
boolean capped = narrow;
|
||||
for (long coordinate : point) capped &= coordinate >= -LIMIT && coordinate <= LIMIT;
|
||||
return lookup(point, previous, penultimate, capped);
|
||||
}
|
||||
|
||||
private Result lookup(long[] point, Object previous, Object penultimate, boolean capped) {
|
||||
if (single())
|
||||
return new Result(nodes[0], distance(0, point, 0, false), null, Long.MAX_VALUE);
|
||||
int first = position(previous);
|
||||
int second = position(penultimate);
|
||||
long best = distance(first, point, 0, false);
|
||||
long other = distance(second, point, 0, false);
|
||||
if (best > other) {
|
||||
int swap = first;
|
||||
first = second;
|
||||
second = swap;
|
||||
long distance = best;
|
||||
best = other;
|
||||
other = distance;
|
||||
}
|
||||
int index = 1;
|
||||
while (index < nodes.length) {
|
||||
long candidate = distance(index, point, other, capped);
|
||||
if (branches[index]) {
|
||||
index = other > candidate ? index + 1 : next[index];
|
||||
continue;
|
||||
}
|
||||
if (other > candidate) {
|
||||
boolean different = !keys[index].equals(first < 0 ? nullKey : keys[first]);
|
||||
if (best > candidate) {
|
||||
if (different) {
|
||||
other = best;
|
||||
second = first;
|
||||
}
|
||||
best = candidate;
|
||||
first = index;
|
||||
} else if (different) {
|
||||
other = candidate;
|
||||
second = index;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return new Result(
|
||||
first < 0 ? null : nodes[first], best, second < 0 ? null : nodes[second], other);
|
||||
}
|
||||
}
|
||||
152
server/src/main/java/dev/quorate/tune/BiomePatch.java
Normal file
152
server/src/main/java/dev/quorate/tune/BiomePatch.java
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import org.objectweb.asm.Handle;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.commons.InstructionAdapter;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.TypeInsnNode;
|
||||
|
||||
public final class BiomePatch {
|
||||
private static final String CLIMATE = "net/minecraft/world/level/biome/Climate$";
|
||||
private static final String NODE = CLIMATE + "RTree$Node";
|
||||
private static final String LEAF = CLIMATE + "RTree$Leaf";
|
||||
private static final String BRANCH = CLIMATE + "RTree$SubTree";
|
||||
private static final String RESULT = "com/terraformersmc/biolith/api/biome/BiolithFittestNodes";
|
||||
private static final Type OBJECT = Type.getType(Object.class);
|
||||
|
||||
private BiomePatch() {}
|
||||
|
||||
public static int callers(ClassNode node) {
|
||||
int count = 0;
|
||||
for (var method : node.methods) {
|
||||
if (!method.name.equals("getBiome")) continue;
|
||||
for (var code : method.instructions.toArray()) {
|
||||
if (!(code instanceof InvokeDynamicInsnNode factory)
|
||||
|| factory.bsmArgs.length != 3
|
||||
|| !(factory.bsmArgs[1] instanceof Handle handle)
|
||||
|| !handle.getOwner().equals(NODE)
|
||||
|| !handle.getName().equals("distance")
|
||||
|| !handle.getDesc().equals("([J)J")) continue;
|
||||
var next = code.getNext();
|
||||
while (next != null && next.getOpcode() < 0) next = next.getNext();
|
||||
if (!(next instanceof MethodInsnNode call)
|
||||
|| !call.name.equals("biolith$searchTreeGet"))
|
||||
throw new IllegalStateException("Biome caller shape");
|
||||
method.instructions.set(
|
||||
code,
|
||||
new MethodInsnNode(
|
||||
Opcodes.INVOKEVIRTUAL,
|
||||
CLIMATE + "TargetPoint",
|
||||
"toParameterArray",
|
||||
"()[J",
|
||||
false));
|
||||
var search =
|
||||
new MethodInsnNode(
|
||||
Opcodes.INVOKEINTERFACE,
|
||||
"dev/quorate/tune/BiomeSearch",
|
||||
"saltatory$search",
|
||||
"([J)Ljava/lang/Object;",
|
||||
true);
|
||||
method.instructions.set(call, search);
|
||||
method.instructions.insert(search, new TypeInsnNode(Opcodes.CHECKCAST, RESULT));
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static InstructionAdapter edit(MethodNode method) {
|
||||
method.instructions.clear();
|
||||
method.localVariables = null;
|
||||
method.tryCatchBlocks.clear();
|
||||
return new InstructionAdapter(method);
|
||||
}
|
||||
|
||||
public static void tree(ClassNode node) {
|
||||
String key =
|
||||
node.methods.stream()
|
||||
.filter(method -> method.name.endsWith("biolith$keyOf"))
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.name;
|
||||
for (var method : node.methods) {
|
||||
String field =
|
||||
switch (method.name) {
|
||||
case "saltatory$root" -> "root";
|
||||
case "saltatory$first" -> "previousUltimateNode";
|
||||
case "saltatory$second" -> "previousPenultimateNode";
|
||||
default -> null;
|
||||
};
|
||||
if (field != null) {
|
||||
String descriptor =
|
||||
node.fields.stream()
|
||||
.filter(value -> value.name.equals(field))
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.desc;
|
||||
var emit = edit(method);
|
||||
emit.load(0, OBJECT);
|
||||
emit.getfield(node.name, field, descriptor);
|
||||
emit.areturn(OBJECT);
|
||||
emit.visitMaxs(1, 1);
|
||||
} else if (method.name.equals("saltatory$children")) {
|
||||
var emit = edit(method);
|
||||
var leaf = new Label();
|
||||
emit.load(1, OBJECT);
|
||||
emit.instanceOf(Type.getObjectType(BRANCH));
|
||||
emit.ifeq(leaf);
|
||||
emit.load(1, OBJECT);
|
||||
emit.checkcast(Type.getObjectType(BRANCH));
|
||||
emit.getfield(BRANCH, "children", "[L" + NODE + ";");
|
||||
emit.areturn(OBJECT);
|
||||
emit.mark(leaf);
|
||||
emit.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
|
||||
emit.aconst(null);
|
||||
emit.areturn(OBJECT);
|
||||
emit.visitMaxs(1, 2);
|
||||
} else if (method.name.equals("saltatory$bounds")) {
|
||||
var emit = edit(method);
|
||||
emit.load(1, OBJECT);
|
||||
emit.checkcast(Type.getObjectType(NODE));
|
||||
emit.getfield(NODE, "parameterSpace", "[L" + CLIMATE + "Parameter;");
|
||||
emit.areturn(OBJECT);
|
||||
emit.visitMaxs(1, 2);
|
||||
} else if (method.name.equals("saltatory$key")) {
|
||||
var emit = edit(method);
|
||||
emit.load(0, OBJECT);
|
||||
emit.load(1, OBJECT);
|
||||
emit.checkcast(Type.getObjectType(LEAF));
|
||||
emit.invokespecial(
|
||||
node.name,
|
||||
key,
|
||||
"(L" + LEAF + ";)Lnet/minecraft/resources/ResourceKey;",
|
||||
false);
|
||||
emit.invokevirtual(
|
||||
"net/minecraft/resources/ResourceKey",
|
||||
"location",
|
||||
"()Lnet/minecraft/resources/ResourceLocation;",
|
||||
false);
|
||||
emit.areturn(OBJECT);
|
||||
emit.visitMaxs(2, 2);
|
||||
} else if (method.name.equals("saltatory$result")) {
|
||||
var emit = edit(method);
|
||||
emit.anew(Type.getObjectType(RESULT));
|
||||
emit.dup();
|
||||
emit.load(1, OBJECT);
|
||||
emit.checkcast(Type.getObjectType(LEAF));
|
||||
emit.load(2, Type.LONG_TYPE);
|
||||
emit.load(4, OBJECT);
|
||||
emit.checkcast(Type.getObjectType(LEAF));
|
||||
emit.load(5, Type.LONG_TYPE);
|
||||
emit.invokespecial(RESULT, "<init>", "(L" + LEAF + ";JL" + LEAF + ";J)V", false);
|
||||
emit.areturn(OBJECT);
|
||||
emit.visitMaxs(8, 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
server/src/main/java/dev/quorate/tune/BiomeSearch.java
Normal file
5
server/src/main/java/dev/quorate/tune/BiomeSearch.java
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
public interface BiomeSearch {
|
||||
Object saltatory$search(long[] point);
|
||||
}
|
||||
46
server/src/main/java/dev/quorate/tune/CaveFrame.java
Normal file
46
server/src/main/java/dev/quorate/tune/CaveFrame.java
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
public final class CaveFrame {
|
||||
private final double[][] a;
|
||||
private final double[][] b;
|
||||
private final double[][] c;
|
||||
private final double[][] d;
|
||||
private final double[][] values;
|
||||
private final float[] start;
|
||||
private final float[] end;
|
||||
private final int last;
|
||||
|
||||
public CaveFrame(
|
||||
double[][] a,
|
||||
double[][] b,
|
||||
double[][] c,
|
||||
double[][] d,
|
||||
float[] start,
|
||||
float[] end,
|
||||
int last) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
this.c = c;
|
||||
this.d = d;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.last = last;
|
||||
values = new double[a.length][a[0].length];
|
||||
}
|
||||
|
||||
public double[][] column(int x, int z) {
|
||||
double sx = start[x];
|
||||
double ex = end[x];
|
||||
double sz = start[z];
|
||||
double ez = end[z];
|
||||
for (int y = 0; y < values.length; y++) {
|
||||
double[] row = values[y];
|
||||
for (int g = 0; g < row.length; g++) {
|
||||
double left = x == 0 ? a[y][g] : x == last ? c[y][g] : a[y][g] * sx + c[y][g] * ex;
|
||||
double right = x == 0 ? b[y][g] : x == last ? d[y][g] : b[y][g] * sx + d[y][g] * ex;
|
||||
row[g] = z == 0 ? left : z == last ? right : left * sz + right * ez;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
7
server/src/main/java/dev/quorate/tune/CaveNoise.java
Normal file
7
server/src/main/java/dev/quorate/tune/CaveNoise.java
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
public interface CaveNoise {
|
||||
CaveFrame caveFrame(BlockPos min, BlockPos max, int low, int high);
|
||||
}
|
||||
167
server/src/main/java/dev/quorate/tune/CavePatch.java
Normal file
167
server/src/main/java/dev/quorate/tune/CavePatch.java
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.commons.InstructionAdapter;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InsnList;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.VarInsnNode;
|
||||
|
||||
public final class CavePatch {
|
||||
private static final String BC = "com/yungnickyoung/minecraft/bettercaves/";
|
||||
private static final String RANGE = BC + "worldgen/carver/CarverNoiseRange";
|
||||
private static final String CARVER = BC + "worldgen/carver/AbstractCarver";
|
||||
private static final String NOISE = BC + "noise/NoiseGen";
|
||||
private static final String POS = "Lnet/minecraft/core/BlockPos;";
|
||||
private static final String CUBE = "(" + POS + POS + "II)[[[[D";
|
||||
private static final String FRAME = Type.getInternalName(CaveFrame.class);
|
||||
private static final String SOURCE = Type.getInternalName(CaveNoise.class);
|
||||
private static final String FACTORY = "(" + POS + POS + "II)L" + FRAME + ";";
|
||||
private static final String DEMAND =
|
||||
"(Ljava/util/Map;Ljava/lang/Object;" + POS + POS + "III)[[D";
|
||||
private static final String METHOD = "saltatory$noise";
|
||||
|
||||
private CavePatch() {}
|
||||
|
||||
public static void apply(ClassNode node) {
|
||||
if (node.name.equals(NOISE)) {
|
||||
node.interfaces.add(SOURCE);
|
||||
node.methods.add(factory());
|
||||
return;
|
||||
}
|
||||
if (node.name.endsWith("/CaveCarver")) {
|
||||
CaveRows.apply(node);
|
||||
return;
|
||||
}
|
||||
var body =
|
||||
node.methods.stream()
|
||||
.filter(method -> method.name.equals("carveChunk"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
int cubes = 0;
|
||||
int demands = 0;
|
||||
for (var instruction : body.instructions.toArray()) {
|
||||
if (!(instruction instanceof MethodInsnNode call)) continue;
|
||||
if (call.owner.equals(NOISE)
|
||||
&& call.name.equals("interpolateNoiseCube")
|
||||
&& call.desc.equals(CUBE)) {
|
||||
var empty = new InsnList();
|
||||
for (int argument = 0; argument < 5; argument++)
|
||||
empty.add(new InsnNode(Opcodes.POP));
|
||||
empty.add(new InsnNode(Opcodes.ACONST_NULL));
|
||||
body.instructions.insertBefore(call, empty);
|
||||
body.instructions.remove(call);
|
||||
cubes++;
|
||||
} else if (call.owner.equals("java/util/Map") && call.name.equals("get")) {
|
||||
var extra = new InsnList();
|
||||
extra.add(new VarInsnNode(Opcodes.ALOAD, local(body, "startPos")));
|
||||
extra.add(new VarInsnNode(Opcodes.ALOAD, local(body, "endPos")));
|
||||
extra.add(new VarInsnNode(Opcodes.ILOAD, local(body, "maxHeight")));
|
||||
extra.add(new VarInsnNode(Opcodes.ILOAD, local(body, "offsetX")));
|
||||
extra.add(new VarInsnNode(Opcodes.ILOAD, local(body, "offsetZ")));
|
||||
body.instructions.insertBefore(call, extra);
|
||||
AbstractInsnNode cursor = call;
|
||||
for (int index = 0; index < 5; index++) {
|
||||
AbstractInsnNode next = cursor.getNext();
|
||||
while (next.getOpcode() < 0) next = next.getNext();
|
||||
body.instructions.remove(next);
|
||||
}
|
||||
body.instructions.set(
|
||||
call,
|
||||
new MethodInsnNode(Opcodes.INVOKESTATIC, node.name, METHOD, DEMAND, false));
|
||||
demands++;
|
||||
}
|
||||
}
|
||||
if (cubes != 1 || demands != 1) throw new IllegalStateException("Cave layer shape changed");
|
||||
body.maxStack += 5;
|
||||
node.methods.add(demand());
|
||||
}
|
||||
|
||||
private static int local(MethodNode method, String name) {
|
||||
return method.localVariables.stream()
|
||||
.filter(variable -> variable.name.equals(name))
|
||||
.findFirst()
|
||||
.orElseThrow()
|
||||
.index;
|
||||
}
|
||||
|
||||
private static MethodNode demand() {
|
||||
var method =
|
||||
new MethodNode(
|
||||
Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, METHOD, DEMAND, null, null);
|
||||
var emit = new InstructionAdapter(method);
|
||||
var object = Type.getType(Object.class);
|
||||
var found = new Label();
|
||||
emit.load(0, object);
|
||||
emit.load(1, object);
|
||||
emit.invokeinterface("java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
emit.store(7, object);
|
||||
emit.load(7, object);
|
||||
emit.ifnonnull(found);
|
||||
emit.load(1, object);
|
||||
emit.checkcast(Type.getObjectType(RANGE));
|
||||
emit.invokevirtual(RANGE, "getCarver", "()L" + CARVER + ";", false);
|
||||
emit.store(8, object);
|
||||
emit.load(8, object);
|
||||
emit.invokevirtual(CARVER, "getNoiseGen", "()L" + NOISE + ";", false);
|
||||
emit.checkcast(Type.getObjectType(SOURCE));
|
||||
emit.load(2, object);
|
||||
emit.load(3, object);
|
||||
emit.load(8, object);
|
||||
emit.invokevirtual(CARVER, "getBottomY", "()I", false);
|
||||
emit.load(4, Type.INT_TYPE);
|
||||
emit.invokeinterface(SOURCE, "caveFrame", FACTORY);
|
||||
emit.store(7, object);
|
||||
emit.load(0, object);
|
||||
emit.load(1, object);
|
||||
emit.load(7, object);
|
||||
emit.invokeinterface(
|
||||
"java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
emit.pop();
|
||||
emit.mark(found);
|
||||
emit.visitFrame(Opcodes.F_APPEND, 1, new Object[] {"java/lang/Object"}, 0, null);
|
||||
emit.load(7, object);
|
||||
emit.checkcast(Type.getObjectType(FRAME));
|
||||
emit.load(5, Type.INT_TYPE);
|
||||
emit.load(6, Type.INT_TYPE);
|
||||
emit.invokevirtual(FRAME, "column", "(II)[[D", false);
|
||||
emit.areturn(object);
|
||||
emit.visitMaxs(5, 9);
|
||||
return method;
|
||||
}
|
||||
|
||||
private static MethodNode factory() {
|
||||
var method = new MethodNode(Opcodes.ACC_PUBLIC, "caveFrame", FACTORY, null, null);
|
||||
var emit = new InstructionAdapter(method);
|
||||
var object = Type.getType(Object.class);
|
||||
var pos = "net/minecraft/core/BlockPos";
|
||||
emit.anew(Type.getObjectType(FRAME));
|
||||
emit.dup();
|
||||
for (int corner = 0; corner < 4; corner++) {
|
||||
emit.load(0, object);
|
||||
emit.load(corner / 2 + 1, object);
|
||||
emit.invokevirtual(pos, "getX", "()I", false);
|
||||
emit.load(corner % 2 + 1, object);
|
||||
emit.invokevirtual(pos, "getZ", "()I", false);
|
||||
emit.load(3, Type.INT_TYPE);
|
||||
emit.load(4, Type.INT_TYPE);
|
||||
emit.invokespecial(NOISE, "generateNoiseColumn", "(IIII)[[D", false);
|
||||
}
|
||||
emit.getstatic(BC + "BCConstants", "START_COEFFS", "[F");
|
||||
emit.getstatic(BC + "BCConstants", "END_COEFFS", "[F");
|
||||
emit.load(2, object);
|
||||
emit.invokevirtual(pos, "getX", "()I", false);
|
||||
emit.load(1, object);
|
||||
emit.invokevirtual(pos, "getX", "()I", false);
|
||||
emit.sub(Type.INT_TYPE);
|
||||
emit.invokespecial(FRAME, "<init>", "([[D[[D[[D[[D[F[FI)V", false);
|
||||
emit.areturn(object);
|
||||
emit.visitMaxs(12, 5);
|
||||
return method;
|
||||
}
|
||||
}
|
||||
106
server/src/main/java/dev/quorate/tune/CaveRows.java
Normal file
106
server/src/main/java/dev/quorate/tune/CaveRows.java
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldInsnNode;
|
||||
import org.objectweb.asm.tree.FrameNode;
|
||||
import org.objectweb.asm.tree.InsnList;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.IntInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.TypeInsnNode;
|
||||
import org.objectweb.asm.tree.VarInsnNode;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class CaveRows {
|
||||
private static final Set<String> METHODS =
|
||||
Set.of("carveColumn", "generateThresholds", "preprocessCaveNoiseCol");
|
||||
|
||||
private CaveRows() {}
|
||||
|
||||
private static void frame(List<Object> values) {
|
||||
if (values == null) return;
|
||||
for (int i = 0; i < values.size(); i++)
|
||||
if ("java/util/Map".equals(values.get(i)) || "java/util/HashMap".equals(values.get(i)))
|
||||
values.set(i, "[F");
|
||||
}
|
||||
|
||||
private static InsnList length() {
|
||||
var code = new InsnList();
|
||||
code.add(new VarInsnNode(Opcodes.ILOAD, 1));
|
||||
code.add(new VarInsnNode(Opcodes.ILOAD, 2));
|
||||
code.add(new InsnNode(Opcodes.ISUB));
|
||||
code.add(new InsnNode(Opcodes.ICONST_1));
|
||||
code.add(new InsnNode(Opcodes.IADD));
|
||||
code.add(new InsnNode(Opcodes.ICONST_0));
|
||||
code.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/Math", "max", "(II)I", false));
|
||||
code.add(new IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_FLOAT));
|
||||
return code;
|
||||
}
|
||||
|
||||
private static InsnList offset(ClassNode owner, MethodNode method) {
|
||||
var code = new InsnList();
|
||||
switch (method.name) {
|
||||
case "generateThresholds" -> code.add(new VarInsnNode(Opcodes.ILOAD, 2));
|
||||
case "preprocessCaveNoiseCol" -> code.add(new VarInsnNode(Opcodes.ILOAD, 3));
|
||||
default -> {
|
||||
code.add(new VarInsnNode(Opcodes.ALOAD, 0));
|
||||
code.add(new FieldInsnNode(Opcodes.GETFIELD, owner.name, "bottomY", "I"));
|
||||
}
|
||||
}
|
||||
code.add(new InsnNode(Opcodes.ISUB));
|
||||
return code;
|
||||
}
|
||||
|
||||
public static void apply(ClassNode owner) {
|
||||
int reads = 0;
|
||||
int writes = 0;
|
||||
for (var method : owner.methods) {
|
||||
if (!METHODS.contains(method.name)) continue;
|
||||
method.desc = method.desc.replace("Ljava/util/Map;", "[F");
|
||||
method.signature = null;
|
||||
method.localVariables = null;
|
||||
method.maxStack += 2;
|
||||
for (var instruction : method.instructions.toArray()) {
|
||||
if (instruction instanceof FrameNode stack) {
|
||||
frame(stack.local);
|
||||
frame(stack.stack);
|
||||
} else if (instruction instanceof TypeInsnNode type
|
||||
&& type.desc.equals("java/util/HashMap")
|
||||
&& type.getOpcode() == Opcodes.NEW) {
|
||||
method.instructions.insertBefore(type, length());
|
||||
method.instructions.remove(type.getNext());
|
||||
method.instructions.remove(type);
|
||||
} else if (instruction instanceof TypeInsnNode type
|
||||
&& type.desc.equals("java/lang/Float")
|
||||
&& type.getOpcode() == Opcodes.CHECKCAST) {
|
||||
method.instructions.remove(type);
|
||||
} else if (instruction instanceof MethodInsnNode call) {
|
||||
if (call.owner.equals(owner.name) && METHODS.contains(call.name)) {
|
||||
call.desc = call.desc.replace("Ljava/util/Map;", "[F");
|
||||
} else if (call.owner.equals("java/lang/Integer")
|
||||
&& call.name.equals("valueOf")) {
|
||||
method.instructions.insertBefore(call, offset(owner, method));
|
||||
method.instructions.remove(call);
|
||||
} else if (call.owner.equals("java/util/Map") && call.name.equals("get")) {
|
||||
method.instructions.set(call, new InsnNode(Opcodes.FALOAD));
|
||||
reads++;
|
||||
} else if (call.owner.equals("java/util/Map") && call.name.equals("put")) {
|
||||
method.instructions.remove(call.getNext());
|
||||
method.instructions.set(call, new InsnNode(Opcodes.FASTORE));
|
||||
writes++;
|
||||
} else if (call.owner.equals("java/lang/Float")
|
||||
|| call.owner.equals("java/util/HashMap")
|
||||
&& call.name.equals("<init>")) {
|
||||
method.instructions.remove(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reads != 2 || writes != 1)
|
||||
throw new IllegalStateException("Cave threshold shape changed");
|
||||
}
|
||||
}
|
||||
152
server/src/main/java/dev/quorate/tune/ChunkQueue.java
Normal file
152
server/src/main/java/dev/quorate/tune/ChunkQueue.java
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData;
|
||||
import com.seibel.distanthorizons.core.pos.DhChunkPos;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ChunkQueue {
|
||||
private static final class Entry {
|
||||
final DhChunkPos pos;
|
||||
final ChunkUpdateData value;
|
||||
double distance;
|
||||
int near;
|
||||
int far;
|
||||
|
||||
Entry(DhChunkPos pos, ChunkUpdateData value, DhChunkPos center) {
|
||||
this.pos = pos;
|
||||
this.value = Objects.requireNonNull(value);
|
||||
distance = pos.squaredDistance(center);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Heap {
|
||||
private final boolean reverse;
|
||||
private Entry[] entries = new Entry[500];
|
||||
private int size;
|
||||
|
||||
Heap(boolean reverse) {
|
||||
this.reverse = reverse;
|
||||
}
|
||||
|
||||
int compare(Entry left, Entry right) {
|
||||
return reverse
|
||||
? Double.compare(right.distance, left.distance)
|
||||
: Double.compare(left.distance, right.distance);
|
||||
}
|
||||
|
||||
void put(int index, Entry entry) {
|
||||
entries[index] = entry;
|
||||
if (reverse) entry.far = index;
|
||||
else entry.near = index;
|
||||
}
|
||||
|
||||
void up(int index, Entry entry) {
|
||||
while (index != 0) {
|
||||
int parent = (index - 1) >>> 1;
|
||||
Entry previous = entries[parent];
|
||||
if (compare(entry, previous) >= 0) break;
|
||||
put(index, previous);
|
||||
index = parent;
|
||||
}
|
||||
put(index, entry);
|
||||
}
|
||||
|
||||
void down(int index, Entry entry) {
|
||||
int half = size >>> 1;
|
||||
while (index < half) {
|
||||
int child = (index << 1) + 1;
|
||||
int sibling = child + 1;
|
||||
if (sibling < size && compare(entries[child], entries[sibling]) > 0)
|
||||
child = sibling;
|
||||
if (compare(entry, entries[child]) <= 0) break;
|
||||
put(index, entries[child]);
|
||||
index = child;
|
||||
}
|
||||
put(index, entry);
|
||||
}
|
||||
|
||||
void add(Entry entry) {
|
||||
if (size == entries.length) entries = Arrays.copyOf(entries, size + (size >>> 1));
|
||||
up(size++, entry);
|
||||
}
|
||||
|
||||
Entry remove(int index) {
|
||||
Entry removed = entries[index];
|
||||
int last = --size;
|
||||
Entry replacement = entries[last];
|
||||
entries[last] = null;
|
||||
if (index != last) {
|
||||
down(index, replacement);
|
||||
if (entries[index] == replacement) up(index, replacement);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
Arrays.fill(entries, 0, size, null);
|
||||
size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private final HashMap<DhChunkPos, Entry> entries = new HashMap<>();
|
||||
private final Heap near = new Heap(false);
|
||||
private final Heap far = new Heap(true);
|
||||
private DhChunkPos center = new DhChunkPos(0, 0);
|
||||
|
||||
public synchronized void addItem(DhChunkPos pos, ChunkUpdateData value) {
|
||||
if (entries.containsKey(pos)) return;
|
||||
var entry = new Entry(pos, value, center);
|
||||
entries.put(pos, entry);
|
||||
near.add(entry);
|
||||
far.add(entry);
|
||||
}
|
||||
|
||||
public synchronized boolean contains(DhChunkPos pos) {
|
||||
return entries.containsKey(pos);
|
||||
}
|
||||
|
||||
public synchronized int getQueuedCount() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
public synchronized boolean isEmpty() {
|
||||
return entries.isEmpty();
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
entries.clear();
|
||||
near.clear();
|
||||
far.clear();
|
||||
}
|
||||
|
||||
public synchronized void setCenter(DhChunkPos value) {
|
||||
if (value.equals(center)) return;
|
||||
center = value;
|
||||
near.clear();
|
||||
far.clear();
|
||||
for (var entry : entries.values()) {
|
||||
entry.distance = entry.pos.squaredDistance(center);
|
||||
near.add(entry);
|
||||
far.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized ChunkUpdateData popClosest() {
|
||||
if (near.size == 0) return null;
|
||||
Entry entry = near.remove(0);
|
||||
far.remove(entry.far);
|
||||
entries.remove(entry.pos);
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
public synchronized ChunkUpdateData popFurthest() {
|
||||
if (far.size == 0) return null;
|
||||
Entry entry = far.remove(0);
|
||||
near.remove(entry.near);
|
||||
entries.remove(entry.pos);
|
||||
return entry.value;
|
||||
}
|
||||
}
|
||||
9
server/src/main/java/dev/quorate/tune/DensityLimit.java
Normal file
9
server/src/main/java/dev/quorate/tune/DensityLimit.java
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import net.minecraft.world.level.levelgen.DensityFunction;
|
||||
|
||||
public interface DensityLimit {
|
||||
int densityIndex();
|
||||
|
||||
double densityUpper(DensityFunction.FunctionContext point);
|
||||
}
|
||||
27
server/src/main/java/dev/quorate/tune/FeatureMath.java
Normal file
27
server/src/main/java/dev/quorate/tune/FeatureMath.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
||||
public final class FeatureMath {
|
||||
private FeatureMath() {}
|
||||
|
||||
public static int limit(int range, Object config) {
|
||||
return ((GrowthView) config)
|
||||
.placeBlock()
|
||||
.getClass()
|
||||
.getName()
|
||||
.equals(
|
||||
"net.mehvahdjukaar.supplementaries.common.block.blocks.BarnaclesBlock")
|
||||
? Math.min(range, 1)
|
||||
: range;
|
||||
}
|
||||
|
||||
public static boolean allow(Object context, Object value, BlockPos pos) {
|
||||
if (!(context instanceof StoneContext)) return true;
|
||||
var shape = (ShapeView) value;
|
||||
double x = (pos.getX() - shape.src().getX()) / shape.radius().x;
|
||||
double y = (pos.getY() - shape.src().getY()) / shape.radius().y;
|
||||
double z = (pos.getZ() - shape.src().getZ()) / shape.radius().z;
|
||||
return !(x * x + y * y + z * z > 1.);
|
||||
}
|
||||
}
|
||||
7
server/src/main/java/dev/quorate/tune/GrowthView.java
Normal file
7
server/src/main/java/dev/quorate/tune/GrowthView.java
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import net.minecraft.world.level.block.MultifaceBlock;
|
||||
|
||||
public interface GrowthView {
|
||||
MultifaceBlock placeBlock();
|
||||
}
|
||||
43
server/src/main/java/dev/quorate/tune/LightScan.java
Normal file
43
server/src/main/java/dev/quorate/tune/LightScan.java
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.EmptyBlockGetter;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public final class LightScan {
|
||||
// Keeping metadata above the int payload preserves negative static emission values.
|
||||
private static final long DYNAMIC = 1L << 32;
|
||||
private static final long UNKNOWN = 2L << 32;
|
||||
|
||||
private LightScan() {}
|
||||
|
||||
public static void find(ChunkAccess chunk, BiConsumer<BlockPos, BlockState> consumer) {
|
||||
var emitters = new Reference2LongOpenHashMap<BlockState>();
|
||||
emitters.defaultReturnValue(UNKNOWN);
|
||||
chunk.findBlocks(
|
||||
state -> sample(state, emitters) != 0,
|
||||
(state, pos) -> {
|
||||
long light = sample(state, emitters);
|
||||
return (light == DYNAMIC ? state.getLightEmission(chunk, pos) : (int) light)
|
||||
!= 0;
|
||||
},
|
||||
consumer);
|
||||
}
|
||||
|
||||
private static long sample(BlockState state, Reference2LongOpenHashMap<BlockState> emitters) {
|
||||
long light = emitters.getLong(state);
|
||||
if (light != UNKNOWN) return light;
|
||||
light =
|
||||
state.hasDynamicLightEmission()
|
||||
? DYNAMIC
|
||||
: Integer.toUnsignedLong(
|
||||
state.getLightEmission(EmptyBlockGetter.INSTANCE, BlockPos.ZERO));
|
||||
emitters.put(state, light);
|
||||
return light;
|
||||
}
|
||||
}
|
||||
5
server/src/main/java/dev/quorate/tune/LimitSource.java
Normal file
5
server/src/main/java/dev/quorate/tune/LimitSource.java
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
public interface LimitSource {
|
||||
DensityLimit densityLimit();
|
||||
}
|
||||
43
server/src/main/java/dev/quorate/tune/NoiseCache.java
Normal file
43
server/src/main/java/dev/quorate/tune/NoiseCache.java
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
public final class NoiseCache {
|
||||
private static final int SIZE = 16384;
|
||||
private static final ThreadLocal<NoiseCache> LOCAL = ThreadLocal.withInitial(NoiseCache::new);
|
||||
private final Object[] owners = new Object[SIZE];
|
||||
private final long[] xs = new long[SIZE];
|
||||
private final long[] ys = new long[SIZE];
|
||||
private final long[] zs = new long[SIZE];
|
||||
private final double[] values = new double[SIZE];
|
||||
|
||||
public static NoiseCache local() {
|
||||
return LOCAL.get();
|
||||
}
|
||||
|
||||
// A miss returns the complemented slot, as in Arrays.binarySearch.
|
||||
public int find(Object owner, long x, long y, long z) {
|
||||
long key =
|
||||
x
|
||||
^ Long.rotateLeft(y, 21)
|
||||
^ Long.rotateLeft(z, 42)
|
||||
^ System.identityHashCode(owner);
|
||||
key ^= key >>> 33;
|
||||
key *= 0xff51afd7ed558ccdL;
|
||||
key ^= key >>> 33;
|
||||
int slot = (int) (key ^ (key >>> 32)) & (SIZE - 1);
|
||||
return owners[slot] == owner && xs[slot] == x && ys[slot] == y && zs[slot] == z
|
||||
? slot
|
||||
: ~slot;
|
||||
}
|
||||
|
||||
public double value(int slot) {
|
||||
return values[slot];
|
||||
}
|
||||
|
||||
public void put(int slot, Object owner, long x, long y, long z, double value) {
|
||||
owners[slot] = owner;
|
||||
xs[slot] = x;
|
||||
ys[slot] = y;
|
||||
zs[slot] = z;
|
||||
values[slot] = value;
|
||||
}
|
||||
}
|
||||
33
server/src/main/java/dev/quorate/tune/NoiseCall.java
Normal file
33
server/src/main/java/dev/quorate/tune/NoiseCall.java
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
public final class NoiseCall {
|
||||
private static final MethodHandle SAMPLE = binding();
|
||||
|
||||
private NoiseCall() {}
|
||||
|
||||
private static MethodHandle binding() {
|
||||
try {
|
||||
var type = Class.forName("com.ishland.c2me.opts.natives_math.common.Bindings");
|
||||
return MethodHandles.publicLookup()
|
||||
.findStatic(
|
||||
type,
|
||||
"c2me_natives_noise_perlin_double",
|
||||
MethodType.methodType(
|
||||
double.class,
|
||||
long.class,
|
||||
double.class,
|
||||
double.class,
|
||||
double.class));
|
||||
} catch (ReflectiveOperationException failure) {
|
||||
throw new ExceptionInInitializerError(failure);
|
||||
}
|
||||
}
|
||||
|
||||
public static double sample(long data, double x, double y, double z) throws Throwable {
|
||||
return (double) SAMPLE.invokeExact(data, x, y, z);
|
||||
}
|
||||
}
|
||||
21
server/src/main/java/dev/quorate/tune/PostOffsets.java
Normal file
21
server/src/main/java/dev/quorate/tune/PostOffsets.java
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortListIterator;
|
||||
|
||||
public final class PostOffsets {
|
||||
private PostOffsets() {}
|
||||
|
||||
public static ShortListIterator snapshot(ShortList offsets) {
|
||||
synchronized (offsets) {
|
||||
return new ShortArrayList(offsets).iterator();
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(ShortList offsets, ShortListIterator iterator) {
|
||||
synchronized (offsets) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
10
server/src/main/java/dev/quorate/tune/ShapeView.java
Normal file
10
server/src/main/java/dev/quorate/tune/ShapeView.java
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public interface ShapeView {
|
||||
BlockPos src();
|
||||
|
||||
Vec3 radius();
|
||||
}
|
||||
3
server/src/main/java/dev/quorate/tune/StoneContext.java
Normal file
3
server/src/main/java/dev/quorate/tune/StoneContext.java
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
public interface StoneContext {}
|
||||
30
server/src/main/java/dev/quorate/tune/SurfaceMath.java
Normal file
30
server/src/main/java/dev/quorate/tune/SurfaceMath.java
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
public final class SurfaceMath {
|
||||
private SurfaceMath() {}
|
||||
|
||||
public static double value(double offset, double depth, double factor, int y) {
|
||||
double gradient = Mth.clampedMap(y, -2048., 2048., 17., -15.);
|
||||
double sum = offset + (gradient + depth);
|
||||
double product = sum == 0. ? 0. : sum * factor;
|
||||
double shaped = product > 0. ? product : product * .25;
|
||||
return Math.max(-64., Math.min(64., -.7 + 4. * shaped));
|
||||
}
|
||||
|
||||
public static int height(
|
||||
double offset, double depth, double factor, int top, int bottom, int step) {
|
||||
if (factor <= 0.)
|
||||
return value(offset, depth, factor, top) > .390625 ? top : Integer.MAX_VALUE;
|
||||
int count = (top - bottom) / step + 1;
|
||||
int low = 0;
|
||||
int high = count;
|
||||
while (low < high) {
|
||||
int mid = (low + high) >>> 1;
|
||||
if (value(offset, depth, factor, top - mid * step) > .390625) high = mid;
|
||||
else low = mid + 1;
|
||||
}
|
||||
return low == count ? Integer.MAX_VALUE : top - low * step;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue