5b451c38da
Статтеры были не на сервере, а в клиенте, и стоили дорого. Измерено: перевод чанка в игровой формат шел через setBlockState, а он каждый раз обновляет карту высот и карту света. Чанк стоил 10-156 мс на главном потоке, их было 208, и переход через границу чанка упирался в 325 мс за один тик. Сделано: - Секции чанка пишутся прямо в формате игры. Палитра и упакованные индексы строятся игровым же классом, так что формат совпадает по определению, а заливка по блокам и карты высот и света не нужны. Чанк подешевел с 25 мс до 1-2 мс, это в 15-25 раз. - Обработка входящих пакетов ограничена тремя миллисекундами на тик. Колонка чанков, пришедшая за один раз, теперь размазывается по тикам: не влезшее ждет следующего, это 0.2 секунды, и глазом не видно. - Сгенерированный чанк больше не считается грязным. Террейн есть чистая функция от зерна и координат, он и так восстановится за пару миллисекунд, а писать его стоило 64 килобайта на чанк. Ходьба по миру больше не гоняет двадцать мегабайт по диску. На диск пишется только то, во что игрок построился. Проверено: 169 чанков при заходе, медленных (больше 4 мс) три, остальные быстрее; на диске ноль файлов, потому что игрок ничего не строил.
275 lines
11 KiB
Java
275 lines
11 KiB
Java
package net.rustnotmc.client.net;
|
|
|
|
import io.netty.buffer.Unpooled;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.EnumSet;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
import net.minecraft.block.Block;
|
|
import net.minecraft.network.Packet;
|
|
import net.minecraft.network.PacketBuffer;
|
|
import net.minecraft.network.play.INetHandlerPlayClient;
|
|
import net.minecraft.network.play.server.SPacketBlockChange;
|
|
import net.minecraft.network.play.server.SPacketChunkData;
|
|
import net.minecraft.network.play.server.SPacketDestroyEntities;
|
|
import net.minecraft.network.play.server.SPacketEntityTeleport;
|
|
import net.minecraft.network.play.server.SPacketJoinGame;
|
|
import net.minecraft.network.play.server.SPacketPlayerListItem;
|
|
import net.minecraft.network.play.server.SPacketPlayerPosLook;
|
|
import net.minecraft.network.play.server.SPacketSpawnPlayer;
|
|
import net.minecraft.network.play.server.SPacketSpawnPosition;
|
|
import net.minecraft.util.math.BlockPos;
|
|
import net.minecraft.world.EnumDifficulty;
|
|
import net.minecraft.world.GameType;
|
|
import net.minecraft.world.World;
|
|
import net.minecraft.world.WorldType;
|
|
import net.minecraft.world.chunk.BlockStateContainer;
|
|
import net.rustnotmc.protocol.ChunkFormat;
|
|
import net.rustnotmc.protocol.Frame;
|
|
import net.rustnotmc.protocol.Packets;
|
|
|
|
/**
|
|
* Turns the server's packets into the game's own.
|
|
*
|
|
* The client already knows how to be a Minecraft client: it has a world, a
|
|
* player, entities and a renderer, and all of that works. The only thing it
|
|
* does not have is the server's packets. So instead of writing a second
|
|
* renderer and a second world, the server's packets are translated into the
|
|
* game's own and handed to the game, which does what it already knows how to
|
|
* do.
|
|
*
|
|
* That is the whole reason the wire format is ours: it lets the client keep
|
|
* the parts that are worth keeping.
|
|
*/
|
|
public final class RsxWorldBridge {
|
|
|
|
private RsxWorldBridge() {
|
|
}
|
|
|
|
/**
|
|
* The packets that put a client into a world.
|
|
*
|
|
* Three of them, in this order: the world is created, the spawn point is
|
|
* set, and the player is put on it. The player cannot be moved before the
|
|
* world exists, because the player belongs to the world.
|
|
*/
|
|
public static List<Packet<INetHandlerPlayClient>> join(int entityId, int maxPlayers,
|
|
double spawnX, double spawnY, double spawnZ) {
|
|
List<Packet<INetHandlerPlayClient>> packets =
|
|
new ArrayList<Packet<INetHandlerPlayClient>>();
|
|
packets.add(new SPacketJoinGame(entityId, GameType.SURVIVAL, false, 0,
|
|
EnumDifficulty.NORMAL, maxPlayers, WorldType.DEFAULT, false));
|
|
packets.add(new SPacketSpawnPosition(
|
|
new BlockPos((int) Math.floor(spawnX), (int) Math.floor(spawnY),
|
|
(int) Math.floor(spawnZ))));
|
|
packets.add(new SPacketPlayerPosLook(spawnX, spawnY, spawnZ, 0.0F, 0.0F,
|
|
EnumSet.noneOf(SPacketPlayerPosLook.EnumFlags.class), 0));
|
|
return packets;
|
|
}
|
|
|
|
/**
|
|
* One chunk, as the game wants it.
|
|
*
|
|
* The game reads a chunk as sixteen sections of sixteen by sixteen by
|
|
* sixteen, each with a palette of block states, a light array and a sky
|
|
* light array, and a biome for every column.
|
|
*
|
|
* The section data is written straight into the game's own format instead
|
|
* of being built by filling a game chunk block by block. Filling one takes
|
|
* sixteen thousand calls, and every one of them updates a height map and a
|
|
* light map: it cost the client a quarter of a second for every chunk it
|
|
* walked into, which is the difference between a world that streams and a
|
|
* world that stutters.
|
|
*
|
|
* The light is set to full. The server does not compute light yet, and a
|
|
* world with no light in it is a world nobody can see.
|
|
*/
|
|
public static SPacketChunkData chunk(World world, int chunkX, int chunkZ, byte[] blocks)
|
|
throws java.io.IOException {
|
|
boolean skyLight = world.provider.hasSkyLight();
|
|
|
|
PacketBuffer sections = new PacketBuffer(Unpooled.buffer(128 * 1024));
|
|
int mask = 0;
|
|
|
|
for (int section = 0; section < 16; section++) {
|
|
BlockStateContainer container = new BlockStateContainer();
|
|
boolean any = false;
|
|
int base = section * 16;
|
|
|
|
for (int y = 0; y < 16; y++) {
|
|
for (int z = 0; z < 16; z++) {
|
|
for (int x = 0; x < 16; x++) {
|
|
byte id = blocks[ChunkFormat.index(x, base + y, z)];
|
|
if (id == net.rustnotmc.protocol.Blocks.AIR) {
|
|
// the palette starts with air and the storage
|
|
// starts at zero, so air is already there
|
|
continue;
|
|
}
|
|
container.set(x, y, z, RsxBlockMap.stateFor(id));
|
|
any = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!any) {
|
|
continue;
|
|
}
|
|
mask |= 1 << section;
|
|
container.write(sections);
|
|
sections.writeBytes(LIGHT);
|
|
if (skyLight) {
|
|
sections.writeBytes(LIGHT);
|
|
}
|
|
}
|
|
|
|
// one biome per column, and the same one for now
|
|
sections.writeBytes(BIOMES);
|
|
|
|
byte[] data = new byte[sections.writerIndex()];
|
|
sections.readBytes(data);
|
|
|
|
PacketBuffer body = new PacketBuffer(Unpooled.buffer(data.length + 64));
|
|
body.writeInt(chunkX);
|
|
body.writeInt(chunkZ);
|
|
body.writeBoolean(true);
|
|
body.writeVarInt(mask);
|
|
body.writeVarInt(data.length);
|
|
body.writeBytes(data);
|
|
body.writeVarInt(0); // no tile entities
|
|
|
|
SPacketChunkData packet = new SPacketChunkData();
|
|
packet.readPacketData(body);
|
|
return packet;
|
|
}
|
|
|
|
/** A section's light, all of it on. */
|
|
private static final byte[] LIGHT = new byte[2048];
|
|
|
|
/** The biomes of a chunk: one byte per column. */
|
|
private static final byte[] BIOMES = new byte[256];
|
|
|
|
static {
|
|
java.util.Arrays.fill(LIGHT, (byte) 0xFF);
|
|
java.util.Arrays.fill(BIOMES, (byte) 1); // plains
|
|
}
|
|
|
|
/** One block, changed. */
|
|
public static SPacketBlockChange blockChange(int x, int y, int z, byte id) throws java.io.IOException {
|
|
// the game's packet has no constructor that takes the values, so the
|
|
// bytes are written the way the game writes them and read back by the
|
|
// game's own reader. One format, one implementation of it.
|
|
PacketBuffer buffer = new PacketBuffer(Unpooled.buffer());
|
|
buffer.writeBlockPos(new BlockPos(x, y, z));
|
|
buffer.writeVarInt(Block.BLOCK_STATE_IDS.get(RsxBlockMap.stateFor(id)));
|
|
SPacketBlockChange packet = new SPacketBlockChange();
|
|
packet.readPacketData(buffer);
|
|
return packet;
|
|
}
|
|
|
|
/** Somebody else is here. */
|
|
public static SPacketSpawnPlayer spawnPlayer(int entityId, String name,
|
|
double x, double y, double z,
|
|
float yaw, float pitch) throws java.io.IOException {
|
|
// the game's packet has no constructor that takes the values, so the
|
|
// bytes are written the way the game writes them and read back by the
|
|
// game's own reader. One format, one implementation of it.
|
|
PacketBuffer buffer = new PacketBuffer(Unpooled.buffer());
|
|
buffer.writeVarInt(entityId);
|
|
buffer.writeUniqueId(uuidFor(name));
|
|
buffer.writeDouble(x);
|
|
buffer.writeDouble(y);
|
|
buffer.writeDouble(z);
|
|
buffer.writeByte(angle(yaw));
|
|
buffer.writeByte(angle(pitch));
|
|
buffer.writeVarInt(0); // no entity data entries
|
|
SPacketSpawnPlayer packet = new SPacketSpawnPlayer();
|
|
packet.readPacketData(buffer);
|
|
return packet;
|
|
}
|
|
|
|
/** Somebody else moved. */
|
|
public static SPacketEntityTeleport entityMove(int entityId, double x, double y,
|
|
double z, float yaw, float pitch)
|
|
throws java.io.IOException {
|
|
PacketBuffer buffer = new PacketBuffer(Unpooled.buffer());
|
|
buffer.writeVarInt(entityId);
|
|
buffer.writeDouble(x);
|
|
buffer.writeDouble(y);
|
|
buffer.writeDouble(z);
|
|
buffer.writeByte(angle(yaw));
|
|
buffer.writeByte(angle(pitch));
|
|
buffer.writeBoolean(true);
|
|
SPacketEntityTeleport packet = new SPacketEntityTeleport();
|
|
packet.readPacketData(buffer);
|
|
return packet;
|
|
}
|
|
|
|
/** Somebody else has left. */
|
|
public static SPacketDestroyEntities entityRemove(int entityId) {
|
|
return new SPacketDestroyEntities(new int[]{entityId});
|
|
}
|
|
|
|
/**
|
|
* The entry that tells the game who a player is.
|
|
*
|
|
* The game looks a player up by their identity when one appears, and
|
|
* refuses to draw a player it has never heard of. The server sends a name
|
|
* and no identity, so the identity is derived the same way the game
|
|
* derives it for an offline account.
|
|
*/
|
|
public static SPacketPlayerListItem playerList(String name) throws java.io.IOException {
|
|
PacketBuffer buffer = new PacketBuffer(Unpooled.buffer());
|
|
buffer.writeEnumValue(SPacketPlayerListItem.Action.ADD_PLAYER);
|
|
buffer.writeVarInt(1);
|
|
buffer.writeUniqueId(uuidFor(name));
|
|
buffer.writeString(name);
|
|
buffer.writeVarInt(0); // no profile properties
|
|
buffer.writeVarInt(GameType.SURVIVAL.getID());
|
|
buffer.writeVarInt(0); // latency
|
|
buffer.writeBoolean(false); // no display name
|
|
SPacketPlayerListItem packet = new SPacketPlayerListItem();
|
|
packet.readPacketData(buffer);
|
|
return packet;
|
|
}
|
|
|
|
/** The identity an offline account gets. */
|
|
public static UUID uuidFor(String name) {
|
|
return UUID.nameUUIDFromBytes(
|
|
("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8));
|
|
}
|
|
|
|
/** An angle in the eighths of a turn the game uses. */
|
|
private static byte angle(float degrees) {
|
|
return (byte) (int) (degrees * 256.0F / 360.0F);
|
|
}
|
|
|
|
/**
|
|
* The game's packet for one of ours, or null when we have no counterpart.
|
|
*
|
|
* A packet we cannot translate is dropped rather than guessed at.
|
|
*/
|
|
public static Packet<INetHandlerPlayClient> translate(Frame frame, World world)
|
|
throws java.io.IOException {
|
|
switch (frame.packetId()) {
|
|
case Packets.BLOCK_CHANGE: {
|
|
Packets.BlockChange change =
|
|
Packets.BlockChange.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
|
return blockChange(change.x, change.y, change.z, change.blockId);
|
|
}
|
|
case Packets.ENTITY_MOVE: {
|
|
Packets.EntityMove move =
|
|
Packets.EntityMove.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
|
return entityMove(move.entityId, move.x, move.y, move.z, move.yaw, move.pitch);
|
|
}
|
|
case Packets.ENTITY_REMOVE: {
|
|
Packets.EntityRemove remove =
|
|
Packets.EntityRemove.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
|
return entityRemove(remove.entityId);
|
|
}
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|