Дальний вид мира: сетка высот и цвета
- Новый пакет LOD_REGION: сервер отдаёт участок дальнего вида — сетку высот и цветов, по одной пробе каждые 4 блока. - LodGenerator на сервере считает участки от начала мира, держит их в памяти и пишет на диск рядом с миром, чтобы считать один раз. - Клиент строит из участка сетку и рисует своим шейдером (#version 330) до обычных чанков: чанки ближе и подробнее, поэтому закрывают дальний вид. - Дальний вид приходит по мере того, как игрок ходит по миру: сервер шлёт участки вокруг него раз в тик. Проверки в сервере 96, в протоколе 24, в клиенте 128.
This commit is contained in:
@@ -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