From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: violetc <58360096+s-yh-china@users.noreply.github.com> Date: Wed, 13 Sep 2023 19:31:20 +0800 Subject: [PATCH] Servux Protocol diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index fc9c4711495136c564ad0da3de314811256df4a1..0138c39c6c0b2c1f3526f9b4ff30d132d95a4e6f 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -2050,6 +2050,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } this.lastSpawnChunkRadius = i; + org.leavesmc.leaves.protocol.servux.ServuxStructuresProtocol.refreshSpawnMetadata = true; // Leaves - servux } public LongSet getForcedChunks() { diff --git a/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java b/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java new file mode 100644 index 0000000000000000000000000000000000000000..3a0e790f0d8e6866950601f9936984a83a4cdf2c --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java @@ -0,0 +1,117 @@ +package org.leavesmc.leaves.protocol.servux; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +// Powered by Servux(https://github.com/sakura-ryoko/servux) + +/** + * Network packet splitter code from QuickCarpet by skyrising + * + * @author skyrising + *

+ * Updated by Sakura to work with newer versions by changing the Reading Session keys, + * and using the HANDLER interface to send packets via the Payload system + *

+ * Move to Leaves by violetc + */ +public class PacketSplitter { + public static final int MAX_TOTAL_PER_PACKET_S2C = 1048576; + public static final int MAX_PAYLOAD_PER_PACKET_S2C = MAX_TOTAL_PER_PACKET_S2C - 5; + public static final int MAX_TOTAL_PER_PACKET_C2S = 32767; + public static final int MAX_PAYLOAD_PER_PACKET_C2S = MAX_TOTAL_PER_PACKET_C2S - 5; + public static final int DEFAULT_MAX_RECEIVE_SIZE_C2S = 1048576; + public static final int DEFAULT_MAX_RECEIVE_SIZE_S2C = 67108864; + + private static final Map READING_SESSIONS = new HashMap<>(); + + public static boolean send(IPacketSplitterHandler handler, FriendlyByteBuf packet, ServerPlayer player) { + return send(handler, packet, player, MAX_PAYLOAD_PER_PACKET_S2C); + } + + private static boolean send(IPacketSplitterHandler handler, FriendlyByteBuf packet, ServerPlayer player, int payloadLimit) { + int len = packet.writerIndex(); + + packet.resetReaderIndex(); + + for (int offset = 0; offset < len; offset += payloadLimit) { + int thisLen = Math.min(len - offset, payloadLimit); + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(thisLen)); + + buf.resetWriterIndex(); + + if (offset == 0) { + buf.writeVarInt(len); + } + + buf.writeBytes(packet, thisLen); + handler.encode(player, buf); + } + + packet.release(); + + return true; + } + + public static FriendlyByteBuf receive(long key, FriendlyByteBuf buf) { + return receive(key, buf, DEFAULT_MAX_RECEIVE_SIZE_S2C); + } + + @Nullable + private static FriendlyByteBuf receive(long key, FriendlyByteBuf buf, int maxLength) { + return READING_SESSIONS.computeIfAbsent(key, ReadingSession::new).receive(buf, maxLength); + } + + /** + * I had to fix the `Pair.of` key mappings, because they were removed from MC; + * So I made it into a pre-shared random session 'key' between client and server. + * Generated using 'long key = Random.create(Util.getMeasuringTimeMs()).nextLong();' + * - + * It can be shared to the receiving end via a separate packet; or it can just be + * generated randomly on the receiving end per an expected Reading Session. + * It needs to be stored and changed for every unique session. + */ + private static class ReadingSession { + private final long key; + private int expectedSize = -1; + private FriendlyByteBuf received; + + private ReadingSession(long key) { + this.key = key; + } + + @Nullable + private FriendlyByteBuf receive(FriendlyByteBuf data, int maxLength) { + data.readerIndex(0); + // data = PacketUtils.slice(data); + + if (this.expectedSize < 0) { + this.expectedSize = data.readVarInt(); + + if (this.expectedSize > maxLength) { + throw new IllegalArgumentException("Payload too large"); + } + + this.received = new FriendlyByteBuf(Unpooled.buffer(this.expectedSize)); + } + + this.received.writeBytes(data.readBytes(data.readableBytes())); + + if (this.received.writerIndex() >= this.expectedSize) { + READING_SESSIONS.remove(this.key); + return this.received; + } + + return null; + } + } + + public interface IPacketSplitterHandler { + void encode(ServerPlayer player, FriendlyByteBuf buf); + } +} diff --git a/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..f80678c0abc38c72b63a32450bd726268d208d6a --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java @@ -0,0 +1,17 @@ +package org.leavesmc.leaves.protocol.servux; + +import io.papermc.paper.ServerBuildInfo; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public class ServuxProtocol { + + public static final String PROTOCOL_ID = "servux"; + public static final String SERVUX_STRING = "servux-leaves-" + ServerBuildInfo.buildInfo().asString(ServerBuildInfo.StringRepresentation.VERSION_SIMPLE); + + @Contract("_ -> new") + public static @NotNull ResourceLocation id(String path) { + return new ResourceLocation(PROTOCOL_ID, path); + } +} diff --git a/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..b7839e1e8cdd68f5db3e6fe237a767a664659daa --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java @@ -0,0 +1,413 @@ +package org.leavesmc.leaves.protocol.servux; + +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +// Powered by Servux(https://github.com/sakura-ryoko/servux) + +@LeavesProtocol(namespace = "servux") +public class ServuxStructuresProtocol { + + public static final int PROTOCOL_VERSION = 2; + + private static final int updateInterval = 40; + private static final int timeout = 30 * 20; + + public static boolean refreshSpawnMetadata = false; + private static int retainDistance; + + public static final ResourceLocation CHANNEL = ServuxProtocol.id("structures"); + + private static final Map players = new ConcurrentHashMap<>(); + private static final Map> timeouts = new HashMap<>(); + + @ProtocolHandler.PayloadReceiver(payload = StructuresPayload.class, payloadId = "structures") + public static void onPacketReceive(ServerPlayer player, StructuresPayload payload) { + if (!LeavesConfig.servuxProtocol) { + return; + } + + switch (payload.packetType()) { + case PACKET_C2S_STRUCTURES_REGISTER -> onPlayerSubscribed(player); + case PACKET_C2S_REQUEST_SPAWN_METADATA -> refreshSpawnMetadata(player); + case PACKET_C2S_STRUCTURES_UNREGISTER -> { + onPlayerLoggedOut(player); + refreshSpawnMetadata(player); + } + } + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + if (!LeavesConfig.servuxProtocol) { + return; + } + + players.remove(player.getId()); + } + + @ProtocolHandler.Ticker + public static void tick() { + if (LeavesConfig.servuxProtocol) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + int tickCounter = server.getTickCount(); + if ((tickCounter % updateInterval) == 0) { + retainDistance = server.getPlayerList().getViewDistance() + 2; + for (ServerPlayer player : players.values()) { + if (refreshSpawnMetadata) { + refreshSpawnMetadata(player); + } + + // TODO DimensionChange + refreshTrackedChunks(player, tickCounter); + } + + if (refreshSpawnMetadata) { + refreshSpawnMetadata = false; + } + } + } + + public static void onPlayerSubscribed(@NotNull ServerPlayer player) { + if (!players.containsKey(player.getId())) { + players.put(player.getId(), player); + } else { + LeavesLogger.LOGGER.warning(player.getScoreboardName() + " re-register servux:structures"); + } + + CompoundTag tag = new CompoundTag(); + tag.putString("name", "structure_bounding_boxes"); + tag.putString("id", CHANNEL.toString()); + tag.putInt("version", PROTOCOL_VERSION); + tag.putString("servux", ServuxProtocol.SERVUX_STRING); + tag.putInt("timeout", timeout); + + MinecraftServer server = MinecraftServer.getServer(); + BlockPos spawnPos = server.overworld().levelData.getSpawnPos(); + tag.putInt("spawnPosX", spawnPos.getX()); + tag.putInt("spawnPosY", spawnPos.getY()); + tag.putInt("spawnPosZ", spawnPos.getZ()); + tag.putInt("spawnChunkRadius", server.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); + + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_METADATA, tag)); + initialSyncStructures(player, player.moonrise$getViewDistanceHolder().getViewDistances().sendViewDistance() + 2, server.getTickCount()); + } + + public static void refreshSpawnMetadata(ServerPlayer player) { + CompoundTag tag = new CompoundTag(); + tag.putString("id", CHANNEL.toString()); + tag.putString("servux", ServuxProtocol.SERVUX_STRING); + + MinecraftServer server = MinecraftServer.getServer(); + BlockPos spawnPos = server.overworld().levelData.getSpawnPos(); + tag.putInt("spawnPosX", spawnPos.getX()); + tag.putInt("spawnPosY", spawnPos.getY()); + tag.putInt("spawnPosZ", spawnPos.getZ()); + tag.putInt("spawnChunkRadius", server.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); + + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_SPAWN_METADATA, tag)); + } + + public static void initialSyncStructures(ServerPlayer player, int chunkRadius, int tickCounter) { + UUID uuid = player.getUUID(); + ChunkPos center = player.getLastSectionPos().chunk(); + Map references = getStructureReferences(player.serverLevel(), center, chunkRadius); + + timeouts.remove(uuid); + + sendStructures(player, references, tickCounter); + } + + public static Map getStructureReferences(ServerLevel world, ChunkPos center, int chunkRadius) { + Map references = new HashMap<>(); + + for (int cx = center.x - chunkRadius; cx <= center.x + chunkRadius; ++cx) { + for (int cz = center.z - chunkRadius; cz <= center.z + chunkRadius; ++cz) { + getReferencesFromChunk(cx, cz, world, references); + } + } + + return references; + } + + public static void getReferencesFromChunk(int chunkX, int chunkZ, Level world, Map references) { + if (!world.hasChunk(chunkX, chunkZ)) { + return; + } + + ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); + + if (chunk == null) { + return; + } + + for (Map.Entry entry : chunk.getAllReferences().entrySet()) { + Structure feature = entry.getKey(); + LongSet startChunks = entry.getValue(); + + // TODO add an option && feature != StructureFeature.MINESHAFT (?) + if (!startChunks.isEmpty()) { + references.merge(feature, startChunks, (oldSet, entrySet) -> { + LongOpenHashSet newSet = new LongOpenHashSet(oldSet); + newSet.addAll(entrySet); + return newSet; + }); + } + } + } + + public static void sendStructures(ServerPlayer player, Map references, int tickCounter) { + ServerLevel world = player.serverLevel(); + Map starts = getStructureStarts(world, references); + + if (!starts.isEmpty()) { + addOrRefreshTimeouts(player.getUUID(), references, tickCounter); + + ListTag structureList = getStructureList(starts, world); + + if (players.containsKey(player.getId())) { + CompoundTag test = new CompoundTag(); + test.put("Structures", structureList.copy()); + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA_START, test)); + } + } + } + + public static ListTag getStructureList(Map structures, ServerLevel world) { + ListTag list = new ListTag(); + StructurePieceSerializationContext ctx = StructurePieceSerializationContext.fromLevel(world); + + for (Map.Entry entry : structures.entrySet()) { + ChunkPos pos = entry.getKey(); + list.add(entry.getValue().createTag(ctx, pos)); + } + + return list; + } + + public static Map getStructureStarts(ServerLevel world, Map references) { + Map starts = new HashMap<>(); + + for (Map.Entry entry : references.entrySet()) { + Structure structure = entry.getKey(); + LongSet startChunks = entry.getValue(); + LongIterator iter = startChunks.iterator(); + + while (iter.hasNext()) { + ChunkPos pos = new ChunkPos(iter.nextLong()); + + if (!world.hasChunk(pos.x, pos.z)) { + continue; + } + + ChunkAccess chunk = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS, false); + + if (chunk == null) { + continue; + } + + StructureStart start = chunk.getStartForStructure(structure); + + if (start != null) { + starts.put(pos, start); + } + } + } + + return starts; + } + + public static void refreshTrackedChunks(ServerPlayer player, int tickCounter) { + UUID uuid = player.getUUID(); + Map map = timeouts.get(uuid); + + if (map != null) { + sendAndRefreshExpiredStructures(player, map, tickCounter); + } + } + + public static void sendAndRefreshExpiredStructures(ServerPlayer player, Map map, int tickCounter) { + Set positionsToUpdate = new HashSet<>(); + + for (Map.Entry entry : map.entrySet()) { + Timeout out = entry.getValue(); + + if (out.needsUpdate(tickCounter, timeout)) { + positionsToUpdate.add(entry.getKey()); + } + } + + if (!positionsToUpdate.isEmpty()) { + ServerLevel world = player.serverLevel(); + ChunkPos center = player.getLastSectionPos().chunk(); + Map references = new HashMap<>(); + + for (ChunkPos pos : positionsToUpdate) { + if (isOutOfRange(pos, center)) { + map.remove(pos); + } else { + getReferencesFromChunk(pos.x, pos.z, world, references); + + Timeout timeout = map.get(pos); + + if (timeout != null) { + timeout.setLastSync(tickCounter); + } + } + } + + if (!references.isEmpty()) { + sendStructures(player, references, tickCounter); + } + } + } + + protected static boolean isOutOfRange(ChunkPos pos, ChunkPos center) { + return Math.abs(pos.x - center.x) > retainDistance || Math.abs(pos.z - center.z) > retainDistance; + } + + public static void addOrRefreshTimeouts(final UUID uuid, final Map references, final int tickCounter) { + Map map = timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>()); + + for (LongSet chunks : references.values()) { + for (Long chunkPosLong : chunks) { + final ChunkPos pos = new ChunkPos(chunkPosLong); + map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter)).setLastSync(tickCounter); + } + } + } + + public enum StructuresPayloadType { + PACKET_S2C_METADATA(1), + PACKET_S2C_STRUCTURE_DATA(2), + PACKET_C2S_STRUCTURES_REGISTER(3), + PACKET_C2S_STRUCTURES_UNREGISTER(4), + PACKET_S2C_STRUCTURE_DATA_START(5), + PACKET_S2C_SPAWN_METADATA(10), + PACKET_C2S_REQUEST_SPAWN_METADATA(11); + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + + public final int type; + + StructuresPayloadType(int type) { + this.type = type; + Helper.ID_TO_TYPE.put(type, this); + } + + public static StructuresPayloadType fromId(int id) { + return Helper.ID_TO_TYPE.get(id); + } + } + + public record StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt, FriendlyByteBuf buffer) implements LeavesCustomPayload { + + public StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt) { + this(packetType, nbt, null); + } + + public StructuresPayload(StructuresPayloadType packetType, FriendlyByteBuf buffer) { + this(packetType, new CompoundTag(), buffer); + } + + @New + private static StructuresPayload decode(ResourceLocation id, FriendlyByteBuf buf) { + int i = buf.readVarInt(); + StructuresPayloadType type = StructuresPayloadType.fromId(i); + + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } else if (type.equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) { + return new StructuresPayload(type, new FriendlyByteBuf(buf.readBytes(buf.readableBytes()))); + } else { + return new StructuresPayload(type, buf.readNbt()); + } + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.packetType.type); + if (this.packetType.equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) { + buf.writeBytes(this.buffer.readBytes(this.buffer.readableBytes())); + } else { + buf.writeNbt(this.nbt); + } + } + + @Override + public ResourceLocation id() { + return CHANNEL; + } + } + + public static class Timeout { + private int lastSync; + + public Timeout(int currentTick) { + this.lastSync = currentTick; + } + + public boolean needsUpdate(int currentTick, int timeout) { + return currentTick - this.lastSync >= timeout; + } + + public void setLastSync(int tickCounter) { + this.lastSync = tickCounter; + } + } + + public static void sendPacket(ServerPlayer player, StructuresPayload payload) { + if (!LeavesConfig.servuxProtocol) { + return; + } + + if (payload.packetType() == StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA_START) { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + buffer.writeNbt(payload.nbt()); + PacketSplitter.send(ServuxStructuresProtocol::sendWithSplitter, buffer, player); + } else { + ProtocolUtils.sendPayloadPacket(player, payload); + } + } + + private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA, buf)); + } +}