diff --git a/patches/server/0007-Leaves-Protocol-Core.patch b/patches/server/0007-Leaves-Protocol-Core.patch index 61432287..0addcb21 100644 --- a/patches/server/0007-Leaves-Protocol-Core.patch +++ b/patches/server/0007-Leaves-Protocol-Core.patch @@ -179,12 +179,13 @@ index 0000000000000000000000000000000000000000..986d2a6641ff8017dddf3e5f2655adfc +} diff --git a/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java b/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java new file mode 100644 -index 0000000000000000000000000000000000000000..4fd8e06c8d2d3016ccc0baf1c43d77d87ea1dca5 +index 0000000000000000000000000000000000000000..e5eb67c0bbdf4953ed0ccc3281f06eda26a7956e --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java -@@ -0,0 +1,386 @@ +@@ -0,0 +1,435 @@ +package org.leavesmc.leaves.protocol.core; + ++import com.google.common.collect.ImmutableSet; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; @@ -211,6 +212,7 @@ index 0000000000000000000000000000000000000000..4fd8e06c8d2d3016ccc0baf1c43d77d8 +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; ++import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; @@ -226,6 +228,7 @@ index 0000000000000000000000000000000000000000..4fd8e06c8d2d3016ccc0baf1c43d77d8 + + private static final Map> KNOWN_TYPES = new HashMap<>(); + private static final Map> KNOW_RECEIVERS = new HashMap<>(); ++ private static Set ALL_KNOWN_ID = new HashSet<>(); + + private static final List TICKERS = new ArrayList<>(); + private static final List PLAYER_JOIN = new ArrayList<>(); @@ -340,6 +343,20 @@ index 0000000000000000000000000000000000000000..4fd8e06c8d2d3016ccc0baf1c43d77d8 + KNOWN_TYPES.put(protocol, map); + } + } ++ ++ for (LeavesProtocol protocol : KNOWN_TYPES.keySet()) { ++ Map map = KNOWN_TYPES.get(protocol); ++ for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) { ++ if (receiver.sendFabricRegister() && !receiver.ignoreId()) { ++ for (String payloadId : receiver.payloadId()) { ++ for (String namespace : protocol.namespace()) { ++ ALL_KNOWN_ID.add(new ResourceLocation(namespace, payloadId)); ++ } ++ } ++ } ++ } ++ } ++ ALL_KNOWN_ID = ImmutableSet.copyOf(ALL_KNOWN_ID); + } + + public static LeavesCustomPayload decode(ResourceLocation id, FriendlyByteBuf buf) { @@ -416,6 +433,8 @@ index 0000000000000000000000000000000000000000..4fd8e06c8d2d3016ccc0baf1c43d77d8 + LOGGER.warning("Failed to handle player join, " + exception.getCause() + ": " + exception.getMessage()); + } + } ++ ++ ProtocolUtils.sendPayloadPacket(player, new FabricRegisterPayload(ALL_KNOWN_ID)); + } + + public static void handlePlayerLeave(ServerPlayer player) { @@ -452,7 +471,7 @@ index 0000000000000000000000000000000000000000..4fd8e06c8d2d3016ccc0baf1c43d77d8 + Map map = MINECRAFT_REGISTER.get(protocol); + for (ProtocolHandler.MinecraftRegister register : map.keySet()) { + if (register.ignoreId() || register.channelId().equals(channel[1]) || -+ ArrayUtils.contains(register.channelIds(), channel[1])) { ++ ArrayUtils.contains(register.channelIds(), channel[1])) { + try { + map.get(register).invoke(null, player); + } catch (InvocationTargetException | IllegalAccessException exception) { @@ -568,13 +587,43 @@ index 0000000000000000000000000000000000000000..4fd8e06c8d2d3016ccc0baf1c43d77d8 + buf.writeBytes(data); + } + } ++ ++ public record FabricRegisterPayload(Set channels) implements LeavesCustomPayload { ++ ++ public static final ResourceLocation CHANNEL = ResourceLocation.withDefaultNamespace("register"); ++ ++ @New ++ public FabricRegisterPayload(ResourceLocation location, FriendlyByteBuf buf) { ++ this(buf.readCollection(HashSet::new, FriendlyByteBuf::readResourceLocation)); ++ } ++ ++ @Override ++ public void write(FriendlyByteBuf buf) { ++ boolean first = true; ++ ++ ResourceLocation channel; ++ for (Iterator var3 = this.channels.iterator(); var3.hasNext(); buf.writeBytes(channel.toString().getBytes(StandardCharsets.US_ASCII))) { ++ channel = var3.next(); ++ if (first) { ++ first = false; ++ } else { ++ buf.writeByte(0); ++ } ++ } ++ } ++ ++ @Override ++ public ResourceLocation id() { ++ return CHANNEL; ++ } ++ } +} diff --git a/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java b/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java new file mode 100644 -index 0000000000000000000000000000000000000000..f941f095184cf4b7af284d1357ea1a85815e8a66 +index 0000000000000000000000000000000000000000..9d71f8e6af24301bedf60f5c87e0bb3c1697d5e3 --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java -@@ -0,0 +1,61 @@ +@@ -0,0 +1,63 @@ +package org.leavesmc.leaves.protocol.core; + +import java.lang.annotation.ElementType; @@ -599,6 +648,8 @@ index 0000000000000000000000000000000000000000..f941f095184cf4b7af284d1357ea1a85 + String[] payloadId() default ""; + + boolean ignoreId() default false; ++ ++ boolean sendFabricRegister() default true; + } + + @Target(ElementType.METHOD) diff --git a/patches/server/0115-Servux-Protocol.patch b/patches/server/0115-Servux-Protocol.patch index c9c53409..3e0e7012 100644 --- a/patches/server/0115-Servux-Protocol.patch +++ b/patches/server/0115-Servux-Protocol.patch @@ -4,6 +4,23 @@ 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 @@ -139,6 +156,307 @@ index 0000000000000000000000000000000000000000..3a0e790f0d8e6866950601f9936984a8 + 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 @@ -164,10 +482,10 @@ index 0000000000000000000000000000000000000000..f80678c0abc38c72b63a32450bd72626 +} 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 +index 0000000000000000000000000000000000000000..028e9f00bb6422f0df40c6d6d286fd0c7e1c290c --- /dev/null +++ b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java -@@ -0,0 +1,413 @@ +@@ -0,0 +1,465 @@ +package org.leavesmc.leaves.protocol.servux; + +import io.netty.buffer.Unpooled; @@ -186,6 +504,7 @@ index 0000000000000000000000000000000000000000..b7839e1e8cdd68f5db3e6fe237a767a6 +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; @@ -239,6 +558,15 @@ index 0000000000000000000000000000000000000000..b7839e1e8cdd68f5db3e6fe237a767a6 + } + } + ++ @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) { @@ -250,7 +578,7 @@ index 0000000000000000000000000000000000000000..b7839e1e8cdd68f5db3e6fe237a767a6 + + @ProtocolHandler.Ticker + public static void tick() { -+ if (LeavesConfig.servuxProtocol) { ++ if (!LeavesConfig.servuxProtocol) { + return; + } + @@ -273,6 +601,48 @@ index 0000000000000000000000000000000000000000..b7839e1e8cdd68f5db3e6fe237a767a6 + } + } + ++ 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);