From f7d67edbdeb913889cc73d59aea8ee39d9a30986 Mon Sep 17 00:00:00 2001 From: perry_lin <2628192835@qq.com> Date: Thu, 3 Sep 2026 23:18:53 +0800 Subject: [PATCH 1/2] wtf is this accumulation but no reset??? --- .../elytrium/limboapi/server/LimboSessionHandlerImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 61e76037..8992fe05 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -141,6 +141,12 @@ public void onConfig(LimboPlayer player) { return; } + // The generic/unknown packet counter is a flood guard, so it should measure traffic inside a + // single keepalive interval instead of accumulating for the whole session: otherwise a normal + // idle client (periodic keepalives, acknowledgements, ...) is eventually kicked for a "too big + // packet" after a few minutes. + this.genericBytes = 0; + if (this.keepAlivePending) { if (++this.keepAlivesSkipped == 2) { connection.closeWith(this.plugin.getPackets().getTimeOut(this.player.getConnection().getState())); From df20ff2354c9303fe7f5bae3295fab1ffda06cd7 Mon Sep 17 00:00:00 2001 From: perry_lin <2628192835@qq.com> Date: Fri, 4 Sep 2026 03:45:11 +0800 Subject: [PATCH 2/2] fix player skin and main hand only display default (no double layer and only right main hand) --- build.gradle.kts | 2 +- .../limboapi/protocol/LimboProtocol.java | 7 +++ .../packets/s2c/SetEntityDataPacket.java | 63 +++++++++++++++++++ .../elytrium/limboapi/server/LimboImpl.java | 30 +++++++++ .../server/LimboSessionHandlerImpl.java | 13 ++++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/SetEntityDataPacket.java diff --git a/build.gradle.kts b/build.gradle.kts index 5d45062c..07a21683 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,7 +33,7 @@ allprojects { excludeFilter.set(file("${rootDir}/config/spotbugs/suppressions.xml")) } - tasks.withType() { + tasks.withType { reports.create("html") { required.set(true) outputLocation.set(layout.buildDirectory.file("reports/spotbugs/main/spotbugs.html")) diff --git a/plugin/src/main/java/net/elytrium/limboapi/protocol/LimboProtocol.java b/plugin/src/main/java/net/elytrium/limboapi/protocol/LimboProtocol.java index d5636219..33e6475e 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/protocol/LimboProtocol.java +++ b/plugin/src/main/java/net/elytrium/limboapi/protocol/LimboProtocol.java @@ -52,6 +52,7 @@ import net.elytrium.limboapi.protocol.packets.s2c.MapDataPacket; import net.elytrium.limboapi.protocol.packets.s2c.PlayerAbilitiesPacket; import net.elytrium.limboapi.protocol.packets.s2c.PositionRotationPacket; +import net.elytrium.limboapi.protocol.packets.s2c.SetEntityDataPacket; import net.elytrium.limboapi.protocol.packets.s2c.SetExperiencePacket; import net.elytrium.limboapi.protocol.packets.s2c.SetSlotPacket; import net.elytrium.limboapi.protocol.packets.s2c.TimeUpdatePacket; @@ -242,6 +243,12 @@ public static void init() throws Throwable { createMapping(0x21, ProtocolVersion.MINECRAFT_1_21_5, true), createMapping(0x25, ProtocolVersion.MINECRAFT_1_21_9, true) ); + register(LIMBO_STATE_REGISTRY, PacketDirection.CLIENTBOUND, + SetEntityDataPacket.class, SetEntityDataPacket::new, + // Paper sends the local player's own entity data on join; without it recent clients keep + // their default appearance (right hand, no outer skin layers). + createMapping(0x61, ProtocolVersion.MINECRAFT_1_21_11, true) + ); register(LIMBO_STATE_REGISTRY, PacketDirection.CLIENTBOUND, DefaultSpawnPositionPacket.class, DefaultSpawnPositionPacket::new, createMapping(0x05, ProtocolVersion.MINECRAFT_1_7_2, true), diff --git a/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/SetEntityDataPacket.java b/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/SetEntityDataPacket.java new file mode 100644 index 00000000..d8a078f0 --- /dev/null +++ b/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/SetEntityDataPacket.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2021 - 2025 Elytrium + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package net.elytrium.limboapi.protocol.packets.s2c; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +/** + * Encode-only ClientboundSetEntityDataPacket. Each metadata entry is a pre-encoded byte sequence of + * {@code [byte index][varint serializer id][value]}; the packet is terminated by {@code 0xFF}. + */ +public class SetEntityDataPacket implements MinecraftPacket { + + private final int entityId; + private final byte[][] entries; + + public SetEntityDataPacket(int entityId, byte[][] entries) { + this.entityId = entityId; + this.entries = entries; + } + + public SetEntityDataPacket() { + this(0, new byte[0][]); + throw new IllegalStateException(); + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + throw new IllegalStateException(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeVarInt(buf, this.entityId); + for (byte[] entry : this.entries) { + buf.writeBytes(entry); + } + buf.writeByte(0xFF); // EOF marker. + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + throw new IllegalStateException(); + } +} diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index 24e1566e..6398bb4f 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -44,6 +44,7 @@ import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket; import com.velocitypowered.proxy.protocol.packet.BossBarPacket; +import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.JoinGamePacket; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; @@ -109,6 +110,7 @@ import net.elytrium.limboapi.protocol.packets.s2c.ChunkDataPacket; import net.elytrium.limboapi.protocol.packets.s2c.DefaultSpawnPositionPacket; import net.elytrium.limboapi.protocol.packets.s2c.PositionRotationPacket; +import net.elytrium.limboapi.protocol.packets.s2c.SetEntityDataPacket; import net.elytrium.limboapi.protocol.packets.s2c.TimeUpdatePacket; import net.elytrium.limboapi.protocol.packets.s2c.UpdateViewPositionPacket; import net.elytrium.limboapi.server.world.SimpleTagManager; @@ -601,6 +603,13 @@ protected void onSpawn(Class handlerClass, connection.delayedWrite(playerInfoPacket); + // Recent clients (1.21.11) expect the server to send the local player's own entity data + // (skin parts / main arm) on join; without it they keep the default appearance (right hand, + // no outer skin layers) and ignore the local skin options. + if (connection.getProtocolVersion() == ProtocolVersion.MINECRAFT_1_21_11) { + connection.delayedWrite(this.createSelfEntityDataPacket(sessionHandler.getSettings())); + } + connection.delayedWrite(this.getBrandMessage(handlerClass)); this.plugin.setLimboJoined(player); @@ -1487,6 +1496,27 @@ private PositionRotationPacket createPlayerPosAndLook(double posX, double posY, return new PositionRotationPacket(posX, posY, posZ, yaw, pitch, false, 44, true); } + private SetEntityDataPacket createSelfEntityDataPacket(ClientSettingsPacket clientSettings) { + int skinParts = clientSettings == null ? 0x7F : clientSettings.getSkinParts(); + // Player skin-parts metadata entry: index 16, BYTE serializer (id 0). Entity id 1 is the local + // player (see createJoinGamePacket). Recent clients show the outer skin layers only after the + // server sends the local player's own entity data. + byte[] skinPartsEntry = new byte[] {16, 0, (byte) skinParts}; + // Player main-arm metadata entry: index 15, HUMANOID_ARM serializer (id 38). The value is the + // client main hand (0 = left, 1 = right), which matches the HumanoidArm enum ordinal. + int mainHand = clientSettings == null ? 1 : clientSettings.getMainHand(); + byte[] mainArmEntry = new byte[] {15, 38, (byte) mainHand}; + return new SetEntityDataPacket(1, new byte[][] {skinPartsEntry, mainArmEntry}); + } + + public void sendSelfEntityData(ConnectedPlayer player, ClientSettingsPacket clientSettings) { + MinecraftConnection connection = player.getConnection(); + if (connection.getState() != StateRegistry.CONFIG + && connection.getProtocolVersion() == ProtocolVersion.MINECRAFT_1_21_11) { + connection.write(this.createSelfEntityDataPacket(clientSettings)); + } + } + private UpdateViewPositionPacket createUpdateViewPosition(int posX, int posZ) { return new UpdateViewPositionPacket(posX >> 4, posZ >> 4); } diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 8992fe05..84be2b28 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -119,6 +119,14 @@ public LimboSessionHandlerImpl(LimboAPI plugin, LimboImpl limbo, ConnectedPlayer this.settings = sessionHandler.getSettings(); this.brand = sessionHandler.getBrand(); } + + if (this.settings == null) { + // 1.20.2+ clients don't resend ClientInformation while switching states, so the limbo session + // never receives it. Fall back to the settings the client sent at login (stored on the player + // by Velocity's config handler), otherwise main hand / skin parts default to right-hand and + // no layers. + this.settings = this.player.getClientSettingsPacket(); + } } public void onConfig(LimboPlayer player) { @@ -356,6 +364,11 @@ public void handleUnknown(ByteBuf packet) { public void handleGeneric(MinecraftPacket packet) { if (packet instanceof ClientSettingsPacket clientSettings) { this.settings = clientSettings; + // Mirror the original Velocity behavior: keep the player-level settings (main hand, skin + // parts, ...) in sync while the player is inside the limbo. + this.player.setClientSettings(clientSettings); + // Re-send the local player's own entity data so skin layers / main hand changes apply live. + this.limbo.sendSelfEntityData(this.player, clientSettings); } else if (packet instanceof PlayerChatSessionPacket) { if (this.chatSessionTimeoutTask != null) { this.chatSessionTimeoutTask.cancel(true);