Дальний вид мира: сетка высот и цвета
- Новый пакет 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");
|
||||
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;
|
||||
}
|
||||
case Packets.LOD_REGION: {
|
||||
Packets.LodRegion region = Packets.LodRegion.read(new Buf(frame.body()));
|
||||
net.rustnotmc.client.RsxLod.accept(region);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
onPlay(frame);
|
||||
}
|
||||
|
||||
@@ -158,6 +158,9 @@ public final class Packets {
|
||||
/** The client asks the server to read its box file again. */
|
||||
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. */
|
||||
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.
|
||||
*
|
||||
@@ -1877,6 +1940,7 @@ public final class Packets {
|
||||
map.put(Integer.valueOf(HITBOXES), "Hitboxes");
|
||||
map.put(Integer.valueOf(HITBOX_INSTANCES), "HitboxInstances");
|
||||
map.put(Integer.valueOf(HITBOXES_RELOAD), "HitboxesReload");
|
||||
map.put(Integer.valueOf(LOD_REGION), "LodRegion");
|
||||
map.put(Integer.valueOf(ATTACK), "Attack");
|
||||
map.put(Integer.valueOf(CHAT), "Chat");
|
||||
map.put(Integer.valueOf(CHAT_SEND), "ChatSend");
|
||||
|
||||
@@ -554,6 +554,7 @@ public final class ProtocolTest {
|
||||
Packets.HITBOXES, Packets.HITBOX_INSTANCES, Packets.HITBOXES_RELOAD,
|
||||
Packets.ATTACK, Packets.CHAT, Packets.CHAT_SEND, Packets.PLAYER_LIST,
|
||||
Packets.SESSION_RELOAD, Packets.SESSION_RENEWED, Packets.ADMIN_STATE,
|
||||
Packets.LOD_REGION,
|
||||
Packets.DISCONNECT,
|
||||
};
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
|
||||
@@ -49,6 +49,7 @@ $suites = @(
|
||||
'net.rustnotmc.server.ServerTest',
|
||||
'net.rustnotmc.server.world.WorldTest',
|
||||
'net.rustnotmc.server.world.NodeTest',
|
||||
'net.rustnotmc.server.world.LodTest',
|
||||
'net.rustnotmc.server.item.InventoryTest',
|
||||
'net.rustnotmc.server.craft.CraftTest',
|
||||
'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. */
|
||||
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. */
|
||||
private volatile long sessionExpiresAt;
|
||||
|
||||
@@ -459,6 +463,37 @@ public final class ClientSession implements Runnable {
|
||||
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) {
|
||||
if (this.state != SessionState.HANDSHAKING) {
|
||||
return;
|
||||
|
||||
@@ -61,6 +61,7 @@ public final class Server {
|
||||
private volatile AdminRegistry admins = new AdminRegistry(null);
|
||||
private volatile net.rustnotmc.server.player.PlayerData playerData =
|
||||
new net.rustnotmc.server.player.PlayerData(null);
|
||||
private volatile net.rustnotmc.server.world.LodGenerator lod;
|
||||
private volatile HitboxRegistry hitboxes = new HitboxRegistry(null);
|
||||
|
||||
private volatile ServerSocket listener;
|
||||
@@ -82,6 +83,9 @@ public final class Server {
|
||||
this.capacity = capacity;
|
||||
this.port = port;
|
||||
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
|
||||
@@ -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. */
|
||||
public net.rustnotmc.server.player.PlayerData playerData() {
|
||||
return this.playerData;
|
||||
@@ -502,6 +515,7 @@ public final class Server {
|
||||
this.world.evictIdle();
|
||||
for (Player player : this.players.values()) {
|
||||
tickResearch(player);
|
||||
player.session().sendLod();
|
||||
// the rights of a player can be taken away while they play, and
|
||||
// the client has to hear about it at once
|
||||
boolean admin = player.isAdmin();
|
||||
|
||||
@@ -129,6 +129,9 @@ public final class ServerMain {
|
||||
// The administrators are the server's own list.
|
||||
Path adminFile = adminsFile != null ? adminsFile : worldDir.resolve("admins.txt");
|
||||
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
|
||||
server.playerData(new net.rustnotmc.server.player.PlayerData(
|
||||
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