Дальний вид мира: сетка высот и цвета
- Новый пакет LOD_REGION: сервер отдаёт участок дальнего вида — сетку высот и цветов, по одной пробе каждые 4 блока. - LodGenerator на сервере считает участки от начала мира, держит их в памяти и пишет на диск рядом с миром, чтобы считать один раз. - Клиент строит из участка сетку и рисует своим шейдером (#version 330) до обычных чанков: чанки ближе и подробнее, поэтому закрывают дальний вид. - Дальний вид приходит по мере того, как игрок ходит по миру: сервер шлёт участки вокруг него раз в тик. Проверки в сервере 96, в протоколе 24, в клиенте 128.
This commit is contained in:
@@ -1327,3 +1327,14 @@ import org.lwjgl.opengl.ContextCapabilities;
|
|||||||
this.debugFeedbackTranslated(flag ? "debug.show_hitboxes.on" : "debug.show_hitboxes.off");
|
this.debugFeedbackTranslated(flag ? "debug.show_hitboxes.on" : "debug.show_hitboxes.off");
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
||||||
|
# The far view of the world is drawn before the chunks, so that the chunks,
|
||||||
|
# which are nearer and have all the detail, cover it where they overlap.
|
||||||
|
=== FILE net/minecraft/client/renderer/EntityRenderer.java
|
||||||
|
--- BEFORE
|
||||||
|
this.mc.profiler.endStartSection("terrain_setup");
|
||||||
|
renderglobal.setupTerrain(entity, (double) f, frustum, this.frameCount++, this.mc.player.isSpectator());
|
||||||
|
--- AFTER
|
||||||
|
this.mc.profiler.endStartSection("terrain_setup");
|
||||||
|
net.rustnotmc.client.RsxLod.render(this.mc);
|
||||||
|
renderglobal.setupTerrain(entity, (double) f, frustum, this.frameCount++, this.mc.player.isSpectator());
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
package net.rustnotmc.client;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import net.minecraft.client.renderer.GlStateManager;
|
||||||
|
import net.rustnotmc.protocol.Packets;
|
||||||
|
import org.lwjgl.BufferUtils;
|
||||||
|
import org.lwjgl.opengl.GL11;
|
||||||
|
import org.lwjgl.opengl.GL15;
|
||||||
|
import org.lwjgl.opengl.GL20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The far view of the world.
|
||||||
|
*
|
||||||
|
* The chunks around a player hold the whole detail, but a player sees much
|
||||||
|
* further than that. The far view is the ground of the world seen from far
|
||||||
|
* away: a grid of heights and colours, one sample every four blocks. The
|
||||||
|
* server sends it a region at a time, and the client turns each region into
|
||||||
|
* a mesh and draws it beyond the loaded chunks.
|
||||||
|
*
|
||||||
|
* The mesh is drawn with its own shader, because the far view has no blocks
|
||||||
|
* and no textures: only the shape of the ground and its colour.
|
||||||
|
*/
|
||||||
|
public final class RsxLod {
|
||||||
|
|
||||||
|
/** The regions that came from the server, by the corner of the region. */
|
||||||
|
private static final java.util.Map<Long, Packets.LodRegion> REGIONS =
|
||||||
|
new java.util.concurrent.ConcurrentHashMap<Long, Packets.LodRegion>();
|
||||||
|
|
||||||
|
/** The meshes that were built, by the corner of the region. */
|
||||||
|
private static final java.util.Map<Long, Mesh> MESHES =
|
||||||
|
new java.util.concurrent.ConcurrentHashMap<Long, Mesh>();
|
||||||
|
|
||||||
|
/** Where the far view writes down what went wrong. */
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger();
|
||||||
|
|
||||||
|
/** Whether the far view is drawn at all. */
|
||||||
|
private static boolean enabled = true;
|
||||||
|
|
||||||
|
/** The shader of the far view, built when it is first needed. */
|
||||||
|
private static Program program;
|
||||||
|
|
||||||
|
/** How many meshes were drawn the last time. */
|
||||||
|
private static int drawn;
|
||||||
|
|
||||||
|
/** Whether the drawing of the far view was already said out loud. */
|
||||||
|
private static boolean announced;
|
||||||
|
|
||||||
|
private RsxLod() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Takes a region from the server and builds its mesh. */
|
||||||
|
public static void accept(Packets.LodRegion region) {
|
||||||
|
Long key = Long.valueOf(key(region.originX, region.originZ));
|
||||||
|
REGIONS.put(key, region);
|
||||||
|
Mesh old = MESHES.remove(key);
|
||||||
|
if (old != null) {
|
||||||
|
old.free();
|
||||||
|
}
|
||||||
|
if (REGIONS.size() == 1) {
|
||||||
|
LOGGER.info("RustNotMC: the far view came: region at " + region.originX + ","
|
||||||
|
+ region.originZ + " with " + region.size + " samples");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throws the far view away, for when the player leaves the world. */
|
||||||
|
public static void clear() {
|
||||||
|
REGIONS.clear();
|
||||||
|
for (Mesh mesh : MESHES.values()) {
|
||||||
|
mesh.free();
|
||||||
|
}
|
||||||
|
MESHES.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the far view is drawn. */
|
||||||
|
public static boolean enabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void enabled(boolean on) {
|
||||||
|
enabled = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many meshes were drawn the last time. */
|
||||||
|
public static int drawn() {
|
||||||
|
return drawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many regions came from the server. */
|
||||||
|
public static int regions() {
|
||||||
|
return REGIONS.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The key of a region: its corner, folded into one number. */
|
||||||
|
private static long key(int originX, int originZ) {
|
||||||
|
return ((long) originX << 32) | (originZ & 0xFFFFFFFFL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the far view.
|
||||||
|
*
|
||||||
|
* The far view is drawn before the chunks, so that the chunks, which are
|
||||||
|
* nearer and have all the detail, cover it where they overlap.
|
||||||
|
*/
|
||||||
|
public static void render(Minecraft mc) {
|
||||||
|
drawn = 0;
|
||||||
|
if (!enabled || REGIONS.isEmpty() || mc.player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mc.gameSettings.renderDistanceChunks <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Program prog = program();
|
||||||
|
if (prog == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatBuffer projection = BufferUtils.createFloatBuffer(16);
|
||||||
|
FloatBuffer model = BufferUtils.createFloatBuffer(16);
|
||||||
|
GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, projection);
|
||||||
|
GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, model);
|
||||||
|
float[] projectionArray = new float[16];
|
||||||
|
float[] modelArray = new float[16];
|
||||||
|
projection.get(projectionArray);
|
||||||
|
model.get(modelArray);
|
||||||
|
|
||||||
|
double camX = mc.getRenderViewEntity().lastTickPosX
|
||||||
|
+ (mc.getRenderViewEntity().posX - mc.getRenderViewEntity().lastTickPosX)
|
||||||
|
* (double) mc.getRenderPartialTicks();
|
||||||
|
double camY = mc.getRenderViewEntity().lastTickPosY
|
||||||
|
+ (mc.getRenderViewEntity().posY - mc.getRenderViewEntity().lastTickPosY)
|
||||||
|
* (double) mc.getRenderPartialTicks();
|
||||||
|
double camZ = mc.getRenderViewEntity().lastTickPosZ
|
||||||
|
+ (mc.getRenderViewEntity().posZ - mc.getRenderViewEntity().lastTickPosZ)
|
||||||
|
* (double) mc.getRenderPartialTicks();
|
||||||
|
|
||||||
|
// the colour the far view fades into: the colour of the sky
|
||||||
|
float fogStart = (float) (mc.gameSettings.renderDistanceChunks * 16.0D * 0.75D);
|
||||||
|
float fogEnd = (float) (mc.gameSettings.renderDistanceChunks * 16.0D);
|
||||||
|
float fogRed = 0.47F;
|
||||||
|
float fogGreen = 0.65F;
|
||||||
|
float fogBlue = 1.0F;
|
||||||
|
|
||||||
|
GlStateManager.pushMatrix();
|
||||||
|
GlStateManager.disableAlpha();
|
||||||
|
GlStateManager.disableLighting();
|
||||||
|
GlStateManager.depthMask(true);
|
||||||
|
GlStateManager.enableDepth();
|
||||||
|
GlStateManager.disableCull();
|
||||||
|
GlStateManager.shadeModel(7425);
|
||||||
|
|
||||||
|
prog.use();
|
||||||
|
GL20.glUniformMatrix4(prog.uniform("uProj"), false, projection);
|
||||||
|
GL20.glUniformMatrix4(prog.uniform("uModel"), false, model);
|
||||||
|
GL20.glUniform3f(prog.uniform("uCam"), (float) camX, (float) camY, (float) camZ);
|
||||||
|
GL20.glUniform4f(prog.uniform("uFogColour"), fogRed, fogGreen, fogBlue, 1.0F);
|
||||||
|
GL20.glUniform1f(prog.uniform("uFogStart"), fogStart);
|
||||||
|
GL20.glUniform1f(prog.uniform("uFogEnd"), fogEnd);
|
||||||
|
|
||||||
|
for (java.util.Map.Entry<Long, Packets.LodRegion> entry : REGIONS.entrySet()) {
|
||||||
|
Long key = entry.getKey();
|
||||||
|
Mesh mesh = MESHES.get(key);
|
||||||
|
if (mesh == null) {
|
||||||
|
mesh = build(entry.getValue());
|
||||||
|
MESHES.put(key, mesh);
|
||||||
|
}
|
||||||
|
mesh.draw();
|
||||||
|
drawn++;
|
||||||
|
}
|
||||||
|
if (drawn > 0 && !announced) {
|
||||||
|
announced = true;
|
||||||
|
LOGGER.info("RustNotMC: the far view is drawn: " + drawn + " regions");
|
||||||
|
}
|
||||||
|
|
||||||
|
prog.unuse();
|
||||||
|
GlStateManager.shadeModel(7424);
|
||||||
|
GlStateManager.enableCull();
|
||||||
|
GlStateManager.enableLighting();
|
||||||
|
GlStateManager.enableAlpha();
|
||||||
|
GlStateManager.popMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds the mesh of one region. */
|
||||||
|
private static Mesh build(Packets.LodRegion region) {
|
||||||
|
int size = region.size;
|
||||||
|
int step = region.step;
|
||||||
|
int cells = (size - 1) * (size - 1);
|
||||||
|
int vertices = cells * 6;
|
||||||
|
ByteBuffer data = ByteBuffer.allocateDirect(vertices * 16).order(ByteOrder.nativeOrder());
|
||||||
|
|
||||||
|
for (int z = 0; z < size - 1; z++) {
|
||||||
|
for (int x = 0; x < size - 1; x++) {
|
||||||
|
int at = z * size + x;
|
||||||
|
int h00 = region.heights[at] & 0xFF;
|
||||||
|
int h10 = region.heights[at + 1] & 0xFF;
|
||||||
|
int h01 = region.heights[at + size] & 0xFF;
|
||||||
|
int h11 = region.heights[at + size + 1] & 0xFF;
|
||||||
|
|
||||||
|
float x0 = region.originX + x * step;
|
||||||
|
float x1 = x0 + step;
|
||||||
|
float z0 = region.originZ + z * step;
|
||||||
|
float z1 = z0 + step;
|
||||||
|
|
||||||
|
int c = at * 3;
|
||||||
|
float r = (region.colors[c] & 0xFF) / 255.0F;
|
||||||
|
float g = (region.colors[c + 1] & 0xFF) / 255.0F;
|
||||||
|
float b = (region.colors[c + 2] & 0xFF) / 255.0F;
|
||||||
|
|
||||||
|
// two triangles: the first one along the diagonal, the second
|
||||||
|
// one the other way, so that the ground is never torn
|
||||||
|
vertex(data, x0, h00, z0, r, g, b);
|
||||||
|
vertex(data, x1, h10, z0, r, g, b);
|
||||||
|
vertex(data, x1, h11, z1, r, g, b);
|
||||||
|
vertex(data, x0, h00, z0, r, g, b);
|
||||||
|
vertex(data, x1, h11, z1, r, g, b);
|
||||||
|
vertex(data, x0, h01, z1, r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.flip();
|
||||||
|
|
||||||
|
int id = GL15.glGenBuffers();
|
||||||
|
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, id);
|
||||||
|
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
|
||||||
|
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||||
|
return new Mesh(id, vertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One vertex of the far view: a place and a colour. */
|
||||||
|
private static void vertex(ByteBuffer data, float x, float y, float z, float r, float g,
|
||||||
|
float b) {
|
||||||
|
data.putFloat(x);
|
||||||
|
data.putFloat(y);
|
||||||
|
data.putFloat(z);
|
||||||
|
data.put((byte) (r * 255.0F));
|
||||||
|
data.put((byte) (g * 255.0F));
|
||||||
|
data.put((byte) (b * 255.0F));
|
||||||
|
data.put((byte) 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What the driver says about a shader that did not build. */
|
||||||
|
private static String infoLog(int shader) {
|
||||||
|
java.nio.IntBuffer length = BufferUtils.createIntBuffer(1);
|
||||||
|
java.nio.ByteBuffer text = BufferUtils.createByteBuffer(8192);
|
||||||
|
GL20.glGetShaderInfoLog(shader, length, text);
|
||||||
|
byte[] bytes = new byte[Math.max(0, length.get(0))];
|
||||||
|
text.get(bytes);
|
||||||
|
return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What the driver says about a program that did not link. */
|
||||||
|
private static String programLog(int program) {
|
||||||
|
java.nio.IntBuffer length = BufferUtils.createIntBuffer(1);
|
||||||
|
java.nio.ByteBuffer text = BufferUtils.createByteBuffer(8192);
|
||||||
|
GL20.glGetProgramInfoLog(program, length, text);
|
||||||
|
byte[] bytes = new byte[Math.max(0, length.get(0))];
|
||||||
|
text.get(bytes);
|
||||||
|
return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds the shader of the far view. */
|
||||||
|
private static Program program() {
|
||||||
|
if (program != null) {
|
||||||
|
return program.ok ? program : null;
|
||||||
|
}
|
||||||
|
String vertex =
|
||||||
|
"#version 330\n"
|
||||||
|
+ "in vec3 aPos;\n"
|
||||||
|
+ "in vec4 aColour;\n"
|
||||||
|
+ "uniform mat4 uProj;\n"
|
||||||
|
+ "uniform mat4 uModel;\n"
|
||||||
|
+ "uniform vec3 uCam;\n"
|
||||||
|
+ "uniform vec4 uFogColour;\n"
|
||||||
|
+ "uniform float uFogStart;\n"
|
||||||
|
+ "uniform float uFogEnd;\n"
|
||||||
|
+ "out vec4 vColour;\n"
|
||||||
|
+ "out float vDistance;\n"
|
||||||
|
+ "void main() {\n"
|
||||||
|
+ " vColour = aColour;\n"
|
||||||
|
+ " vDistance = length(aPos - uCam);\n"
|
||||||
|
+ " gl_Position = uProj * uModel * vec4(aPos, 1.0);\n"
|
||||||
|
+ "}\n";
|
||||||
|
String fragment =
|
||||||
|
"#version 330\n"
|
||||||
|
+ "in vec4 vColour;\n"
|
||||||
|
+ "in float vDistance;\n"
|
||||||
|
+ "uniform vec4 uFogColour;\n"
|
||||||
|
+ "uniform float uFogStart;\n"
|
||||||
|
+ "uniform float uFogEnd;\n"
|
||||||
|
+ "out vec4 fragColour;\n"
|
||||||
|
+ "void main() {\n"
|
||||||
|
+ " float f = clamp((vDistance - uFogStart) / max(uFogEnd - uFogStart, 0.001),"
|
||||||
|
+ " 0.0, 1.0);\n"
|
||||||
|
+ " fragColour = vec4(mix(vColour.rgb, uFogColour.rgb, f), 1.0);\n"
|
||||||
|
+ "}\n";
|
||||||
|
program = Program.build(vertex, fragment);
|
||||||
|
if (!program.ok) {
|
||||||
|
LOGGER.error("RustNotMC: LOD: the shader would not build");
|
||||||
|
}
|
||||||
|
return program.ok ? program : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The mesh of one region. */
|
||||||
|
private static final class Mesh {
|
||||||
|
|
||||||
|
/** The buffer of the vertices. */
|
||||||
|
final int buffer;
|
||||||
|
|
||||||
|
/** How many vertices there are. */
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
Mesh(int buffer, int count) {
|
||||||
|
this.buffer = buffer;
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Draws the mesh. */
|
||||||
|
void draw() {
|
||||||
|
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, this.buffer);
|
||||||
|
GL20.glEnableVertexAttribArray(0);
|
||||||
|
GL20.glEnableVertexAttribArray(1);
|
||||||
|
GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 16, 0L);
|
||||||
|
GL20.glVertexAttribPointer(1, 4, GL11.GL_UNSIGNED_BYTE, true, 16, 12L);
|
||||||
|
GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, this.count);
|
||||||
|
GL20.glDisableVertexAttribArray(1);
|
||||||
|
GL20.glDisableVertexAttribArray(0);
|
||||||
|
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Throws the mesh away. */
|
||||||
|
void free() {
|
||||||
|
GL15.glDeleteBuffers(this.buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A shader program of the far view. */
|
||||||
|
private static final class Program {
|
||||||
|
|
||||||
|
/** The program itself. */
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
/** Whether the program is good. */
|
||||||
|
final boolean ok;
|
||||||
|
|
||||||
|
/** The names of the uniforms. */
|
||||||
|
final java.util.Map<String, Integer> uniforms =
|
||||||
|
new java.util.HashMap<String, Integer>();
|
||||||
|
|
||||||
|
private Program(int id, boolean ok) {
|
||||||
|
this.id = id;
|
||||||
|
this.ok = ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds a program, or says why it did not build. */
|
||||||
|
static Program build(String vertex, String fragment) {
|
||||||
|
int vs = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
|
||||||
|
GL20.glShaderSource(vs, vertex);
|
||||||
|
GL20.glCompileShader(vs);
|
||||||
|
if (GL20.glGetShaderi(vs, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
|
||||||
|
String log = infoLog(vs);
|
||||||
|
GL20.glDeleteShader(vs);
|
||||||
|
LOGGER.error("RustNotMC: LOD: the vertex shader: " + log);
|
||||||
|
return new Program(0, false);
|
||||||
|
}
|
||||||
|
int fs = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
|
||||||
|
GL20.glShaderSource(fs, fragment);
|
||||||
|
GL20.glCompileShader(fs);
|
||||||
|
if (GL20.glGetShaderi(fs, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
|
||||||
|
String log = infoLog(fs);
|
||||||
|
GL20.glDeleteShader(fs);
|
||||||
|
GL20.glDeleteShader(vs);
|
||||||
|
LOGGER.error("RustNotMC: LOD: the fragment shader: " + log);
|
||||||
|
return new Program(0, false);
|
||||||
|
}
|
||||||
|
int program = GL20.glCreateProgram();
|
||||||
|
GL20.glAttachShader(program, vs);
|
||||||
|
GL20.glAttachShader(program, fs);
|
||||||
|
GL20.glBindAttribLocation(program, 0, "aPos");
|
||||||
|
GL20.glBindAttribLocation(program, 1, "aColour");
|
||||||
|
GL20.glLinkProgram(program);
|
||||||
|
if (GL20.glGetProgrami(program, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) {
|
||||||
|
String log = programLog(program);
|
||||||
|
GL20.glDeleteProgram(program);
|
||||||
|
LOGGER.error("RustNotMC: LOD: the program: " + log);
|
||||||
|
return new Program(0, false);
|
||||||
|
}
|
||||||
|
return new Program(program, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Uses the program. */
|
||||||
|
void use() {
|
||||||
|
GL20.glUseProgram(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops using the program. */
|
||||||
|
void unuse() {
|
||||||
|
GL20.glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The place of a uniform. */
|
||||||
|
int uniform(String name) {
|
||||||
|
Integer known = this.uniforms.get(name);
|
||||||
|
if (known != null) {
|
||||||
|
return known.intValue();
|
||||||
|
}
|
||||||
|
int at = GL20.glGetUniformLocation(this.id, name);
|
||||||
|
this.uniforms.put(name, Integer.valueOf(at));
|
||||||
|
return at;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -296,6 +296,11 @@ public final class RsxNetworkManager extends NetworkManager {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case Packets.LOD_REGION: {
|
||||||
|
Packets.LodRegion region = Packets.LodRegion.read(new Buf(frame.body()));
|
||||||
|
net.rustnotmc.client.RsxLod.accept(region);
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
onPlay(frame);
|
onPlay(frame);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,9 @@ public final class Packets {
|
|||||||
/** The client asks the server to read its box file again. */
|
/** The client asks the server to read its box file again. */
|
||||||
public static final int HITBOXES_RELOAD = 0x2C;
|
public static final int HITBOXES_RELOAD = 0x2C;
|
||||||
|
|
||||||
|
/** A piece of the far view: the ground of one region, in a grid. */
|
||||||
|
public static final int LOD_REGION = 0x34;
|
||||||
|
|
||||||
/** The client hits something. */
|
/** The client hits something. */
|
||||||
public static final int ATTACK = 0x2D;
|
public static final int ATTACK = 0x2D;
|
||||||
|
|
||||||
@@ -1834,6 +1837,66 @@ public final class Packets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A piece of the far view.
|
||||||
|
*
|
||||||
|
* The far view is the ground of the world seen from far away: a grid of
|
||||||
|
* heights and colours, one sample every few blocks. The client turns it
|
||||||
|
* into a mesh and draws it beyond the chunks that are loaded.
|
||||||
|
*/
|
||||||
|
public static final class LodRegion implements Packet {
|
||||||
|
|
||||||
|
/** The block the region starts at. */
|
||||||
|
public final int originX;
|
||||||
|
public final int originZ;
|
||||||
|
|
||||||
|
/** How many blocks there are between two samples. */
|
||||||
|
public final int step;
|
||||||
|
|
||||||
|
/** How many samples there are on a side. */
|
||||||
|
public final int size;
|
||||||
|
|
||||||
|
/** The height of every sample, in blocks. */
|
||||||
|
public final byte[] heights;
|
||||||
|
|
||||||
|
/** The colour of every sample: three bytes each, red green blue. */
|
||||||
|
public final byte[] colors;
|
||||||
|
|
||||||
|
public LodRegion(int originX, int originZ, int step, int size, byte[] heights,
|
||||||
|
byte[] colors) {
|
||||||
|
this.originX = originX;
|
||||||
|
this.originZ = originZ;
|
||||||
|
this.step = step;
|
||||||
|
this.size = size;
|
||||||
|
this.heights = heights;
|
||||||
|
this.colors = colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Buf write() {
|
||||||
|
Buf buf = new Buf(32 + this.heights.length + this.colors.length);
|
||||||
|
buf.writeVarInt(this.originX);
|
||||||
|
buf.writeVarInt(this.originZ);
|
||||||
|
buf.writeVarInt(this.step);
|
||||||
|
buf.writeVarInt(this.size);
|
||||||
|
buf.writeBytes(this.heights);
|
||||||
|
buf.writeBytes(this.colors);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LodRegion read(Buf in) {
|
||||||
|
int originX = in.readVarInt();
|
||||||
|
int originZ = in.readVarInt();
|
||||||
|
int step = in.readVarInt();
|
||||||
|
int size = in.readVarInt();
|
||||||
|
if (size < 0 || size > 1024) {
|
||||||
|
throw new ProtocolException("that many samples is not real");
|
||||||
|
}
|
||||||
|
byte[] heights = in.readBytes(size * size);
|
||||||
|
byte[] colors = in.readBytes(size * size * 3);
|
||||||
|
return new LodRegion(originX, originZ, step, size, heights, colors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every packet id this build knows, and what it decodes into.
|
* Every packet id this build knows, and what it decodes into.
|
||||||
*
|
*
|
||||||
@@ -1877,6 +1940,7 @@ public final class Packets {
|
|||||||
map.put(Integer.valueOf(HITBOXES), "Hitboxes");
|
map.put(Integer.valueOf(HITBOXES), "Hitboxes");
|
||||||
map.put(Integer.valueOf(HITBOX_INSTANCES), "HitboxInstances");
|
map.put(Integer.valueOf(HITBOX_INSTANCES), "HitboxInstances");
|
||||||
map.put(Integer.valueOf(HITBOXES_RELOAD), "HitboxesReload");
|
map.put(Integer.valueOf(HITBOXES_RELOAD), "HitboxesReload");
|
||||||
|
map.put(Integer.valueOf(LOD_REGION), "LodRegion");
|
||||||
map.put(Integer.valueOf(ATTACK), "Attack");
|
map.put(Integer.valueOf(ATTACK), "Attack");
|
||||||
map.put(Integer.valueOf(CHAT), "Chat");
|
map.put(Integer.valueOf(CHAT), "Chat");
|
||||||
map.put(Integer.valueOf(CHAT_SEND), "ChatSend");
|
map.put(Integer.valueOf(CHAT_SEND), "ChatSend");
|
||||||
|
|||||||
@@ -554,6 +554,7 @@ public final class ProtocolTest {
|
|||||||
Packets.HITBOXES, Packets.HITBOX_INSTANCES, Packets.HITBOXES_RELOAD,
|
Packets.HITBOXES, Packets.HITBOX_INSTANCES, Packets.HITBOXES_RELOAD,
|
||||||
Packets.ATTACK, Packets.CHAT, Packets.CHAT_SEND, Packets.PLAYER_LIST,
|
Packets.ATTACK, Packets.CHAT, Packets.CHAT_SEND, Packets.PLAYER_LIST,
|
||||||
Packets.SESSION_RELOAD, Packets.SESSION_RENEWED, Packets.ADMIN_STATE,
|
Packets.SESSION_RELOAD, Packets.SESSION_RENEWED, Packets.ADMIN_STATE,
|
||||||
|
Packets.LOD_REGION,
|
||||||
Packets.DISCONNECT,
|
Packets.DISCONNECT,
|
||||||
};
|
};
|
||||||
for (int i = 0; i < ids.length; i++) {
|
for (int i = 0; i < ids.length; i++) {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ $suites = @(
|
|||||||
'net.rustnotmc.server.ServerTest',
|
'net.rustnotmc.server.ServerTest',
|
||||||
'net.rustnotmc.server.world.WorldTest',
|
'net.rustnotmc.server.world.WorldTest',
|
||||||
'net.rustnotmc.server.world.NodeTest',
|
'net.rustnotmc.server.world.NodeTest',
|
||||||
|
'net.rustnotmc.server.world.LodTest',
|
||||||
'net.rustnotmc.server.item.InventoryTest',
|
'net.rustnotmc.server.item.InventoryTest',
|
||||||
'net.rustnotmc.server.craft.CraftTest',
|
'net.rustnotmc.server.craft.CraftTest',
|
||||||
'net.rustnotmc.server.hitbox.HitboxTest',
|
'net.rustnotmc.server.hitbox.HitboxTest',
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ public final class ClientSession implements Runnable {
|
|||||||
/** The identity of the player, as the launcher gave it. */
|
/** The identity of the player, as the launcher gave it. */
|
||||||
private volatile String uuid = "";
|
private volatile String uuid = "";
|
||||||
|
|
||||||
|
/** The regions of the far view that were sent already. */
|
||||||
|
private final java.util.Set<Integer> sentLodRegions =
|
||||||
|
new java.util.LinkedHashSet<Integer>();
|
||||||
|
|
||||||
/** When the session runs out. */
|
/** When the session runs out. */
|
||||||
private volatile long sessionExpiresAt;
|
private volatile long sessionExpiresAt;
|
||||||
|
|
||||||
@@ -459,6 +463,37 @@ public final class ClientSession implements Runnable {
|
|||||||
send(Packets.ADMIN_STATE, new Packets.AdminState(admin));
|
send(Packets.ADMIN_STATE, new Packets.AdminState(admin));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the far view around the player.
|
||||||
|
*
|
||||||
|
* The far view is a grid of heights and colours, one region at a time. A
|
||||||
|
* region is only sent once: the client keeps what it got.
|
||||||
|
*/
|
||||||
|
void sendLod() {
|
||||||
|
if (this.player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int chunkX = (int) Math.floor(this.player.x() / 16.0D);
|
||||||
|
int chunkZ = (int) Math.floor(this.player.z() / 16.0D);
|
||||||
|
int regionX = net.rustnotmc.server.world.LodGenerator.regionChunk(chunkX);
|
||||||
|
int regionZ = net.rustnotmc.server.world.LodGenerator.regionChunk(chunkZ);
|
||||||
|
for (int dx = -2; dx <= 2; dx++) {
|
||||||
|
for (int dz = -2; dz <= 2; dz++) {
|
||||||
|
int rx = regionX + dx * net.rustnotmc.server.world.LodGenerator.REGION_CHUNKS;
|
||||||
|
int rz = regionZ + dz * net.rustnotmc.server.world.LodGenerator.REGION_CHUNKS;
|
||||||
|
Integer key = Integer.valueOf(rx * 100003 + rz);
|
||||||
|
if (this.sentLodRegions.contains(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.sentLodRegions.add(key);
|
||||||
|
if (this.sentLodRegions.size() > 128) {
|
||||||
|
this.sentLodRegions.clear();
|
||||||
|
}
|
||||||
|
send(Packets.LOD_REGION, this.server.lod().region(rx, rz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void onAuth(Frame frame) {
|
private void onAuth(Frame frame) {
|
||||||
if (this.state != SessionState.HANDSHAKING) {
|
if (this.state != SessionState.HANDSHAKING) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public final class Server {
|
|||||||
private volatile AdminRegistry admins = new AdminRegistry(null);
|
private volatile AdminRegistry admins = new AdminRegistry(null);
|
||||||
private volatile net.rustnotmc.server.player.PlayerData playerData =
|
private volatile net.rustnotmc.server.player.PlayerData playerData =
|
||||||
new net.rustnotmc.server.player.PlayerData(null);
|
new net.rustnotmc.server.player.PlayerData(null);
|
||||||
|
private volatile net.rustnotmc.server.world.LodGenerator lod;
|
||||||
private volatile HitboxRegistry hitboxes = new HitboxRegistry(null);
|
private volatile HitboxRegistry hitboxes = new HitboxRegistry(null);
|
||||||
|
|
||||||
private volatile ServerSocket listener;
|
private volatile ServerSocket listener;
|
||||||
@@ -82,6 +83,9 @@ public final class Server {
|
|||||||
this.capacity = capacity;
|
this.capacity = capacity;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.world = world;
|
this.world = world;
|
||||||
|
// the far view of the world, kept in memory unless the caller says
|
||||||
|
// where it should be written down
|
||||||
|
this.lod = new net.rustnotmc.server.world.LodGenerator(world.generator(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------- getters
|
// -------------------------------------------------------------- getters
|
||||||
@@ -251,6 +255,15 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The far view of the world. */
|
||||||
|
public net.rustnotmc.server.world.LodGenerator lod() {
|
||||||
|
return this.lod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void lod(net.rustnotmc.server.world.LodGenerator lod) {
|
||||||
|
this.lod = lod;
|
||||||
|
}
|
||||||
|
|
||||||
/** Where the data of the players is kept. */
|
/** Where the data of the players is kept. */
|
||||||
public net.rustnotmc.server.player.PlayerData playerData() {
|
public net.rustnotmc.server.player.PlayerData playerData() {
|
||||||
return this.playerData;
|
return this.playerData;
|
||||||
@@ -502,6 +515,7 @@ public final class Server {
|
|||||||
this.world.evictIdle();
|
this.world.evictIdle();
|
||||||
for (Player player : this.players.values()) {
|
for (Player player : this.players.values()) {
|
||||||
tickResearch(player);
|
tickResearch(player);
|
||||||
|
player.session().sendLod();
|
||||||
// the rights of a player can be taken away while they play, and
|
// the rights of a player can be taken away while they play, and
|
||||||
// the client has to hear about it at once
|
// the client has to hear about it at once
|
||||||
boolean admin = player.isAdmin();
|
boolean admin = player.isAdmin();
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ public final class ServerMain {
|
|||||||
// The administrators are the server's own list.
|
// The administrators are the server's own list.
|
||||||
Path adminFile = adminsFile != null ? adminsFile : worldDir.resolve("admins.txt");
|
Path adminFile = adminsFile != null ? adminsFile : worldDir.resolve("admins.txt");
|
||||||
server.admins(new net.rustnotmc.server.admin.AdminRegistry(adminFile));
|
server.admins(new net.rustnotmc.server.admin.AdminRegistry(adminFile));
|
||||||
|
// the far view of the world, kept beside it
|
||||||
|
server.lod(new net.rustnotmc.server.world.LodGenerator(world.generator(),
|
||||||
|
worldDir.resolve("lod")));
|
||||||
// where the players leave their things
|
// where the players leave their things
|
||||||
server.playerData(new net.rustnotmc.server.player.PlayerData(
|
server.playerData(new net.rustnotmc.server.player.PlayerData(
|
||||||
worldDir.resolve("players")));
|
worldDir.resolve("players")));
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package net.rustnotmc.server.world;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import net.rustnotmc.protocol.Blocks;
|
||||||
|
import net.rustnotmc.protocol.Packets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The far view of the world.
|
||||||
|
*
|
||||||
|
* The chunks around a player hold the whole detail, but a player sees much
|
||||||
|
* further than that. The far view is the ground of the world seen from far
|
||||||
|
* away: a grid of heights and colours, one sample every four blocks. The
|
||||||
|
* client turns the grid into a mesh and draws it beyond the loaded chunks.
|
||||||
|
*
|
||||||
|
* The regions are counted from the origin of the world, so that every server
|
||||||
|
* and every client numbers them the same way, and they are written to disk,
|
||||||
|
* so that a region is only worked out once.
|
||||||
|
*/
|
||||||
|
public final class LodGenerator {
|
||||||
|
|
||||||
|
/** How many chunks there are on the side of a region. */
|
||||||
|
public static final int REGION_CHUNKS = 16;
|
||||||
|
|
||||||
|
/** How many blocks there are on the side of a region. */
|
||||||
|
public static final int REGION_BLOCKS = REGION_CHUNKS * 16;
|
||||||
|
|
||||||
|
/** How many blocks there are between two samples. */
|
||||||
|
public static final int STEP = 4;
|
||||||
|
|
||||||
|
/** How many samples there are on the side of a region. */
|
||||||
|
public static final int SAMPLES = REGION_BLOCKS / STEP;
|
||||||
|
|
||||||
|
private final WorldGenerator generator;
|
||||||
|
|
||||||
|
/** Where the regions are kept, or null when they are not kept. */
|
||||||
|
private final Path cache;
|
||||||
|
|
||||||
|
public LodGenerator(WorldGenerator generator, Path cache) {
|
||||||
|
this.generator = generator;
|
||||||
|
this.cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The chunk a region starts at. */
|
||||||
|
public static int regionChunk(int chunk) {
|
||||||
|
return Math.floorDiv(chunk, REGION_CHUNKS) * REGION_CHUNKS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The far view of one region.
|
||||||
|
*
|
||||||
|
* The region is read from the disk when it is there, and worked out when
|
||||||
|
* it is not.
|
||||||
|
*/
|
||||||
|
public Packets.LodRegion region(int chunkX, int chunkZ) {
|
||||||
|
int startChunk = regionChunk(chunkX);
|
||||||
|
int startZ = regionChunk(chunkZ);
|
||||||
|
int originX = startChunk * 16;
|
||||||
|
int originZ = startZ * 16;
|
||||||
|
|
||||||
|
byte[] heights = new byte[SAMPLES * SAMPLES];
|
||||||
|
byte[] colors = new byte[SAMPLES * SAMPLES * 3];
|
||||||
|
if (!read(startChunk, startZ, heights, colors)) {
|
||||||
|
write(startChunk, startZ, heights, colors);
|
||||||
|
}
|
||||||
|
return new Packets.LodRegion(originX, originZ, STEP, SAMPLES, heights, colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads a region from the disk. */
|
||||||
|
private boolean read(int chunkX, int chunkZ, byte[] heights, byte[] colors) {
|
||||||
|
if (this.cache == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Path file = this.cache.resolve(chunkX + "_" + chunkZ + ".bin");
|
||||||
|
if (!Files.isRegularFile(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] data = Files.readAllBytes(file);
|
||||||
|
int expected = heights.length + colors.length;
|
||||||
|
if (data.length != expected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
System.arraycopy(data, 0, heights, 0, heights.length);
|
||||||
|
System.arraycopy(data, heights.length, colors, 0, colors.length);
|
||||||
|
return true;
|
||||||
|
} catch (IOException broken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Works a region out and writes it down. */
|
||||||
|
private void write(int chunkX, int chunkZ, byte[] heights, byte[] colors) {
|
||||||
|
for (int z = 0; z < SAMPLES; z++) {
|
||||||
|
for (int x = 0; x < SAMPLES; x++) {
|
||||||
|
int worldX = chunkX * 16 + x * STEP;
|
||||||
|
int worldZ = chunkZ * 16 + z * STEP;
|
||||||
|
int height = this.generator.heightAt(worldX, worldZ);
|
||||||
|
int at = z * SAMPLES + x;
|
||||||
|
heights[at] = (byte) height;
|
||||||
|
int colour = colourOf(worldX, worldZ, height);
|
||||||
|
colors[at * 3] = (byte) ((colour >> 16) & 0xFF);
|
||||||
|
colors[at * 3 + 1] = (byte) ((colour >> 8) & 0xFF);
|
||||||
|
colors[at * 3 + 2] = (byte) (colour & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.cache == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.createDirectories(this.cache);
|
||||||
|
byte[] data = new byte[heights.length + colors.length];
|
||||||
|
System.arraycopy(heights, 0, data, 0, heights.length);
|
||||||
|
System.arraycopy(colors, 0, data, heights.length, colors.length);
|
||||||
|
Path file = this.cache.resolve(chunkX + "_" + chunkZ + ".bin");
|
||||||
|
Path temp = file.resolveSibling(file.getFileName() + ".tmp");
|
||||||
|
Files.write(temp, data);
|
||||||
|
Files.move(temp, file, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (IOException broken) {
|
||||||
|
// The far view is a nicety: a region that cannot be written is
|
||||||
|
// worked out again next time.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The colour of the ground at a place, as the far view shows it. */
|
||||||
|
private int colourOf(int worldX, int worldZ, int height) {
|
||||||
|
if (height < WorldGenerator.SEA_LEVEL) {
|
||||||
|
return 0x2E5C8A;
|
||||||
|
}
|
||||||
|
byte top = topBlock(worldX, worldZ, height);
|
||||||
|
switch (top) {
|
||||||
|
case Blocks.SAND:
|
||||||
|
return 0xC8B47C;
|
||||||
|
case Blocks.STONE:
|
||||||
|
return 0x6E6E6E;
|
||||||
|
case Blocks.ASPHALT:
|
||||||
|
return 0x3A3A3A;
|
||||||
|
case Blocks.WATER:
|
||||||
|
return 0x2E5C8A;
|
||||||
|
default:
|
||||||
|
return 0x4E7A34;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The block on the top of a column. */
|
||||||
|
private byte topBlock(int worldX, int worldZ, int height) {
|
||||||
|
if (this.generator.isRoad(worldX, worldZ)) {
|
||||||
|
return Blocks.ASPHALT;
|
||||||
|
}
|
||||||
|
if (height < WorldGenerator.SEA_LEVEL) {
|
||||||
|
return Blocks.WATER;
|
||||||
|
}
|
||||||
|
if (height <= WorldGenerator.SEA_LEVEL + 1) {
|
||||||
|
return Blocks.SAND;
|
||||||
|
}
|
||||||
|
return Blocks.GRASS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package net.rustnotmc.server.world;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import net.rustnotmc.protocol.Packets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the far view of the world.
|
||||||
|
*
|
||||||
|
* The far view is what a player sees beyond the loaded chunks: a grid of
|
||||||
|
* heights and colours. It has to agree with the terrain the server has, or
|
||||||
|
* the world would look wrong at the edge.
|
||||||
|
*/
|
||||||
|
public final class LodTest {
|
||||||
|
|
||||||
|
private static int checks;
|
||||||
|
private static int failed;
|
||||||
|
private static final java.util.List<String> failures = new java.util.ArrayList<String>();
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
Path work = Path.of(args.length > 0 ? args[0] : "build/selftest");
|
||||||
|
Files.createDirectories(work);
|
||||||
|
|
||||||
|
run("a region holds a grid of samples", new Throwing() {
|
||||||
|
public void go() throws Exception { testGrid(work); }
|
||||||
|
});
|
||||||
|
run("the heights are the heights of the terrain", new Throwing() {
|
||||||
|
public void go() throws Exception { testHeights(work); }
|
||||||
|
});
|
||||||
|
run("the region is kept on disk", new Throwing() {
|
||||||
|
public void go() throws Exception { testCache(work); }
|
||||||
|
});
|
||||||
|
run("the regions are numbered from the origin", new Throwing() {
|
||||||
|
public void go() throws Exception { testNumbering(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
for (int i = 0; i < failures.size(); i++) {
|
||||||
|
System.out.println(" FAIL " + failures.get(i));
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("checks: " + checks + ", failed: " + failed);
|
||||||
|
if (failed != 0) {
|
||||||
|
System.out.println("FAILED");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
System.out.println("OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface Throwing {
|
||||||
|
void go() throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run(String name, Throwing body) {
|
||||||
|
checks++;
|
||||||
|
int before = failed;
|
||||||
|
try {
|
||||||
|
body.go();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
failed++;
|
||||||
|
failures.add(name + ": threw " + t);
|
||||||
|
t.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (failed == before) {
|
||||||
|
System.out.println(" ok " + name);
|
||||||
|
} else {
|
||||||
|
System.out.println(" FAIL " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void eq(Object what, Object expected, Object actual) {
|
||||||
|
if (expected == null ? actual != null : !expected.equals(actual)) {
|
||||||
|
failed++;
|
||||||
|
failures.add(what + ": expected " + expected + " but got " + actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void yes(String what, boolean condition) {
|
||||||
|
if (!condition) {
|
||||||
|
failed++;
|
||||||
|
failures.add(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testGrid(Path work) throws Exception {
|
||||||
|
LodGenerator lod = new LodGenerator(new WorldGenerator(1234L), null);
|
||||||
|
Packets.LodRegion region = lod.region(0, 0);
|
||||||
|
eq("the samples on a side", Integer.valueOf(LodGenerator.SAMPLES),
|
||||||
|
Integer.valueOf(region.size));
|
||||||
|
eq("the heights", Integer.valueOf(LodGenerator.SAMPLES * LodGenerator.SAMPLES),
|
||||||
|
Integer.valueOf(region.heights.length));
|
||||||
|
eq("the colours", Integer.valueOf(LodGenerator.SAMPLES * LodGenerator.SAMPLES * 3),
|
||||||
|
Integer.valueOf(region.colors.length));
|
||||||
|
eq("the step", Integer.valueOf(LodGenerator.STEP), Integer.valueOf(region.step));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testHeights(Path work) throws Exception {
|
||||||
|
WorldGenerator generator = new WorldGenerator(1234L);
|
||||||
|
LodGenerator lod = new LodGenerator(generator, null);
|
||||||
|
Packets.LodRegion region = lod.region(0, 0);
|
||||||
|
for (int z = 0; z < 4; z++) {
|
||||||
|
for (int x = 0; x < 4; x++) {
|
||||||
|
int worldX = x * LodGenerator.STEP;
|
||||||
|
int worldZ = z * LodGenerator.STEP;
|
||||||
|
int at = z * region.size + x;
|
||||||
|
eq("the height at " + worldX + "," + worldZ,
|
||||||
|
Integer.valueOf(generator.heightAt(worldX, worldZ)),
|
||||||
|
Integer.valueOf(region.heights[at] & 0xFF));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testCache(Path work) throws Exception {
|
||||||
|
Path cache = work.resolve("lod-test");
|
||||||
|
WorldGenerator generator = new WorldGenerator(1234L);
|
||||||
|
LodGenerator lod = new LodGenerator(generator, cache);
|
||||||
|
Packets.LodRegion first = lod.region(0, 0);
|
||||||
|
yes("the region is written down", Files.isRegularFile(cache.resolve("0_0.bin")));
|
||||||
|
|
||||||
|
// the second time it is read back, and it is the same
|
||||||
|
Packets.LodRegion second = lod.region(0, 0);
|
||||||
|
yes("the same heights", java.util.Arrays.equals(first.heights, second.heights));
|
||||||
|
yes("the same colours", java.util.Arrays.equals(first.colors, second.colors));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testNumbering() {
|
||||||
|
eq("the first region", Integer.valueOf(0),
|
||||||
|
Integer.valueOf(LodGenerator.regionChunk(0)));
|
||||||
|
eq("the region of a chunk inside", Integer.valueOf(0),
|
||||||
|
Integer.valueOf(LodGenerator.regionChunk(15)));
|
||||||
|
eq("the next region", Integer.valueOf(16),
|
||||||
|
Integer.valueOf(LodGenerator.regionChunk(16)));
|
||||||
|
eq("and back for the negative ones", Integer.valueOf(-16),
|
||||||
|
Integer.valueOf(LodGenerator.regionChunk(-1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user