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/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java index ab18bbf87dfc1455ed185a5152dad6d236565ecc..227c89f7d58cdcfad762f1f11e1f87203a444c6f 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -412,7 +412,11 @@ public final class RegionizedPlayerChunkLoader { if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); - PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ)); + // Leaves start - servux + LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); + org.leavesmc.leaves.protocol.servux.ServuxStructuresProtocol.onStartedWatchingChunk(player, chunk); + PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); + // Leaves end - servux return; } throw new IllegalStateException(); 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/ServuxEntityDataProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..8fb61870622738290167b5fa81f157f4eb6189a9 --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java @@ -0,0 +1,295 @@ +package org.leavesmc.leaves.protocol.servux; + +import io.netty.buffer.Unpooled; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +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.List; +import java.util.Map; +import java.util.UUID; + +// Powered by Servux(https://github.com/sakura-ryoko/servux) + +@LeavesProtocol(namespace = "servux") +public class ServuxEntityDataProtocol { + + public static final ResourceLocation CHANNEL = ServuxProtocol.id("entity_data"); + public static final int PROTOCOL_VERSION = 1; + + private static final Map readingSessionKeys = new HashMap<>(); + + @ProtocolHandler.PlayerJoin + public static void onPlayerLoggedIn(ServerPlayer player) { + if (!LeavesConfig.servuxProtocol) { + return; + } + + sendMetadata(player); + } + + @ProtocolHandler.PayloadReceiver(payload = EntityDataPayload.class, payloadId = "entity_data") + public static void onPacketReceive(ServerPlayer player, EntityDataPayload payload) { + if (!LeavesConfig.servuxProtocol) { + return; + } + + switch (payload.packetType) { + case PACKET_C2S_METADATA_REQUEST -> sendMetadata(player); + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> onBlockEntityRequest(player, payload.pos); + case PACKET_C2S_ENTITY_REQUEST -> onEntityRequest(player, payload.entityId); + case PACKET_C2S_NBT_RESPONSE_DATA -> { + UUID uuid = player.getUUID(); + long readingSessionKey; + + if (!readingSessionKeys.containsKey(uuid)) { + readingSessionKey = RandomSource.create(Util.getMillis()).nextLong(); + readingSessionKeys.put(uuid, readingSessionKey); + } else { + readingSessionKey = readingSessionKeys.get(uuid); + } + + FriendlyByteBuf fullPacket = PacketSplitter.receive(readingSessionKey, payload.buffer); + + if (fullPacket != null) { + readingSessionKeys.remove(uuid); + LeavesLogger.LOGGER.warning("ServuxEntityDataProtocol,PACKET_C2S_NBT_RESPONSE_DATA NOT Implemented!"); + } + } + } + } + + public static void sendMetadata(ServerPlayer player) { + CompoundTag metadata = new CompoundTag(); + metadata.putString("name", "entity_data"); + metadata.putString("id", CHANNEL.toString()); + metadata.putInt("version", PROTOCOL_VERSION); + metadata.putString("servux", ServuxProtocol.SERVUX_STRING); + + EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_METADATA); + payload.nbt.merge(metadata); + sendPacket(player, payload); + } + + public static void onBlockEntityRequest(ServerPlayer player, BlockPos pos) { + MinecraftServer.getServer().execute(() -> { + BlockEntity be = player.serverLevel().getBlockEntity(pos); + CompoundTag nbt = be != null ? be.saveWithoutMetadata(player.registryAccess()) : new CompoundTag(); + + EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE); + payload.pos = pos.immutable(); + payload.nbt.merge(nbt); + sendPacket(player, payload); + }); + } + + public static void onEntityRequest(ServerPlayer player, int entityId) { + MinecraftServer.getServer().execute(() -> { + Entity entity = player.serverLevel().getEntity(entityId); + CompoundTag nbt = entity != null ? entity.saveWithoutId(new CompoundTag()) : new CompoundTag(); + + EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE); + payload.entityId = entityId; + payload.nbt.merge(nbt); + sendPacket(player, payload); + }); + } + + public static void sendPacket(ServerPlayer player, EntityDataPayload payload) { + if (!LeavesConfig.servuxProtocol) { + return; + } + + if (payload.packetType == EntityDataPayloadType.PACKET_S2C_NBT_RESPONSE_START) { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + buffer.writeNbt(payload.nbt); + PacketSplitter.send(ServuxEntityDataProtocol::sendWithSplitter, buffer, player); + } else { + ProtocolUtils.sendPayloadPacket(player, payload); + } + } + + private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { + EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_NBT_RESPONSE_DATA); + payload.buffer = buf; + payload.nbt = new CompoundTag(); + sendPacket(player, payload); + } + + public enum EntityDataPayloadType { + PACKET_S2C_METADATA(1), + PACKET_C2S_METADATA_REQUEST(2), + PACKET_C2S_BLOCK_ENTITY_REQUEST(3), + PACKET_C2S_ENTITY_REQUEST(4), + PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE(5), + PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE(6), + // For Packet Splitter (Oversize Packets, S2C) + PACKET_S2C_NBT_RESPONSE_START(10), + PACKET_S2C_NBT_RESPONSE_DATA(11), + // For Packet Splitter (Oversize Packets, C2S) + PACKET_C2S_NBT_RESPONSE_START(12), + PACKET_C2S_NBT_RESPONSE_DATA(13), + PACKET_C2S_LITEMATICA_PASTE(14), + PACKET_C2S_REQUEST_ALL_ENTITIES_IN_CHUNK(15); + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + + public final int type; + + EntityDataPayloadType(int type) { + this.type = type; + EntityDataPayloadType.Helper.ID_TO_TYPE.put(type, this); + } + + public static EntityDataPayloadType fromId(int id) { + return EntityDataPayloadType.Helper.ID_TO_TYPE.get(id); + } + } + + public static class EntityDataPayload implements LeavesCustomPayload { + + private final EntityDataPayloadType packetType; + private int transactionId; + private int entityId; + private BlockPos pos; + private CompoundTag nbt; + private FriendlyByteBuf buffer; + private List requestingChunks; + + private EntityDataPayload(EntityDataPayloadType type) { + this.packetType = type; + this.transactionId = -1; + this.entityId = -1; + this.pos = BlockPos.ZERO; + this.nbt = new CompoundTag(); + this.clearPacket(); + } + + private void clearPacket() { + if (this.buffer != null) { + this.buffer.clear(); + this.buffer = new FriendlyByteBuf(Unpooled.buffer()); + } + } + + @New + public static EntityDataPayload decode(ResourceLocation location, FriendlyByteBuf buffer) { + EntityDataPayloadType type = EntityDataPayloadType.fromId(buffer.readVarInt()); + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } + + EntityDataPayload payload = new EntityDataPayload(type); + switch (type) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + buffer.readVarInt(); + payload.pos = buffer.readBlockPos().immutable(); + } + + case PACKET_C2S_ENTITY_REQUEST -> { + buffer.readVarInt(); + payload.entityId = buffer.readVarInt(); + } + + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + payload.pos = buffer.readBlockPos().immutable(); + CompoundTag nbt = buffer.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + payload.entityId = buffer.readVarInt(); + CompoundTag nbt = buffer.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + + case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> { + payload.buffer = new FriendlyByteBuf(buffer.readBytes(buffer.readableBytes())); + payload.nbt = new CompoundTag(); + } + + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> { + CompoundTag nbt = buffer.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + + case PACKET_C2S_LITEMATICA_PASTE -> { + payload.nbt = buffer.readNbt(); + } + + case PACKET_C2S_REQUEST_ALL_ENTITIES_IN_CHUNK -> { + payload.requestingChunks = buffer.readList(FriendlyByteBuf::readChunkPos); + } + } + + return payload; + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.packetType.type); + + switch (this.packetType) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + buf.writeVarInt(this.transactionId); + buf.writeBlockPos(this.pos); + } + + case PACKET_C2S_ENTITY_REQUEST -> { + buf.writeVarInt(this.transactionId); + buf.writeVarInt(this.entityId); + } + + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + buf.writeBlockPos(this.pos); + buf.writeNbt(this.nbt); + } + + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + buf.writeVarInt(this.entityId); + buf.writeNbt(this.nbt); + } + + case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> { + buf.writeBytes(this.buffer.readBytes(this.buffer.readableBytes())); + } + + case PACKET_C2S_REQUEST_ALL_ENTITIES_IN_CHUNK -> { + buf.writeCollection(this.requestingChunks, FriendlyByteBuf::writeChunkPos); + } + + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA, PACKET_C2S_LITEMATICA_PASTE -> { + buf.writeNbt(this.nbt); + } + } + } + + @Override + public ResourceLocation id() { + return CHANNEL; + } + } +} 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..028e9f00bb6422f0df40c6d6d286fd0c7e1c290c --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java @@ -0,0 +1,465 @@ +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.LevelChunk; +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.PlayerJoin + public static void onPlayerLoggedIn(ServerPlayer player) { + if (!LeavesConfig.servuxProtocol) { + return; + } + + onPlayerSubscribed(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 onStartedWatchingChunk(ServerPlayer player, LevelChunk chunk) { + if (!LeavesConfig.servuxProtocol) { + return; + } + + MinecraftServer server = player.getServer(); + + if (players.containsKey(player.getId()) && server != null) { + addChunkTimeoutIfHasReferences(player.getUUID(), chunk, server.getTickCount()); + } + } + + private static void addChunkTimeoutIfHasReferences(final UUID uuid, LevelChunk chunk, final int tickCounter) { + final ChunkPos pos = chunk.getPos(); + + if (chunkHasStructureReferences(pos.x, pos.z, chunk.getLevel())) { + final Map map = timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>()); + map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter - timeout)); + } + } + + private static boolean chunkHasStructureReferences(int chunkX, int chunkZ, Level world) { + if (!world.hasChunk(chunkX, chunkZ)) { + return false; + } + + ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); + + if (chunk == null) { + return false; + } + + for (Map.Entry entry : chunk.getAllReferences().entrySet()) { + if (!entry.getValue().isEmpty()) { + return true; + } + } + + return 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)); + } +}