Repository: Minestom/Minestom (2025-08-28T23:45:33Z)

Issue:
Missing .optional() and Update Hat action from PlayerInfoUpdatePacket
This line caused an error:
https://github.com/Minestom/Minestom/blob/9344b7b258936a910c1bf48e0152c3c36b73f776/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java#L107

Because based on protocol description:
https://minecraft.wiki/w/Java_Edition_protocol/Packets#player-info:player-actions

<img width="751" height="122" alt="Image" src="https://github.com/user-attachments/assets/6dc1df22-eb89-4e47-a57d-965728fd0eb0" />

Here can be seen, optional should used

So the code should changed to this:
```java
ChatSession.SERIALIZER.optional().read(buffer)
```

Steps to reproducate:

- Try parsing a PlayerInfoUpdatePacket cames from latest original vanilla server (which online mode disabled)
- With net.minestom.server.network.packet.PacketReading.readServer function
- Error thrown:
```
java.lang.RuntimeException: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: Missing key encoding
	at net.minestom.server.utils.crypto.KeyUtils.publicRSAKeyFrom(KeyUtils.java:47)
	at net.minestom.server.network.NetworkBufferTypeImpl$TransformType.read(NetworkBufferTypeImpl.java:737)
	at net.minestom.server.network.NetworkBufferTemplate$4.read(NetworkBufferTemplate.java:169)
	at net.minestom.server.network.NetworkBufferTemplate$3.read(NetworkBufferTemplate.java:150)
	at net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket$Entry$1.read(PlayerInfoUpdatePacket.java:108)
	at net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket$Entry$1.read(PlayerInfoUpdatePacket.java:84)
	at net.minestom.server.network.NetworkBufferImpl.read(NetworkBufferImpl.java:83)
	at net.minestom.server.network.NetworkBufferTypeImpl$ListType.read(NetworkBufferTypeImpl.java:784)
	at net.minestom.server.network.NetworkBufferTypeImpl$ListType.read(NetworkBufferTypeImpl.java:767)
	at net.minestom.server.network.NetworkBufferImpl.read(NetworkBufferImpl.java:83)
	at net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket$1.read(PlayerInfoUpdatePacket.java:43)
	at net.minestom.server.network.packet.server.play.PlayerInfoUpdatePacket$1.read(PlayerInfoUpdatePacket.java:33)
```

Furthermore "Update Hat" boolean action completely missing from packet reading and writing



Discussion:

Patch:
diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java
index 47ee664fc23..890038bdc26 100644
--- a/src/main/java/net/minestom/server/entity/Player.java
+++ b/src/main/java/net/minestom/server/entity/Player.java
@@ -2244,8 +2244,9 @@ private PlayerInfoUpdatePacket.Entry infoEntry() {
         List<PlayerInfoUpdatePacket.Property> prop = skin != null ?
                 List.of(new PlayerInfoUpdatePacket.Property("textures", skin.textures(), skin.signature())) :
                 List.of();
+        byte hatIndex = ((MetadataDef.Entry.BitMask) MetadataDef.Player.IS_HAT_ENABLED).bitMask();
         return new PlayerInfoUpdatePacket.Entry(getUuid(), getUsername(), prop,
-                true, getLatency(), getGameMode(), displayName, null, 0);
+                true, getLatency(), getGameMode(), displayName, null, 0, (settings.displayedSkinParts() & hatIndex) == hatIndex);
     }
 
     /**
diff --git a/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java b/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java
index 300a27d6328..8208ef927a2 100644
--- a/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java
+++ b/src/main/java/net/minestom/server/network/packet/server/play/PlayerInfoUpdatePacket.java
@@ -63,7 +63,7 @@ public ServerPacket copyWithOperator(UnaryOperator<Component> operator) {
                 newEntries.add(new Entry(entry.uuid, entry.username,
                         entry.properties, entry.listed, entry.latency,
                         entry.gameMode, operator.apply(displayName),
-                        entry.chatSession, entry.listOrder));
+                        entry.chatSession, entry.listOrder, entry.displayHat));
             } else {
                 newEntries.add(entry);
             }
@@ -74,7 +74,7 @@ public ServerPacket copyWithOperator(UnaryOperator<Component> operator) {
     public record Entry(UUID uuid, String username, List<Property> properties,
                         boolean listed, int latency, GameMode gameMode,
                         @Nullable Component displayName, @Nullable ChatSession chatSession,
-                        int listOrder) {
+                        int listOrder, boolean displayHat) {
         public Entry {
             properties = List.copyOf(properties);
         }
@@ -98,21 +98,23 @@ public Entry read(NetworkBuffer buffer) {
                     Component displayName = null;
                     ChatSession chatSession = null;
                     int listOrder = 0;
+                    boolean displayHat = true;
                     for (Action action : actions) {
                         switch (action) {
                             case ADD_PLAYER -> {
                                 username = buffer.read(STRING);
                                 properties = buffer.read(Property.SERIALIZER.list(GameProfile.MAX_PROPERTIES));
                             }
-                            case INITIALIZE_CHAT -> chatSession = ChatSession.SERIALIZER.read(buffer);
+                            case INITIALIZE_CHAT -> chatSession = ChatSession.SERIALIZER.optional().read(buffer);
                             case UPDATE_GAME_MODE -> gameMode = buffer.read(NetworkBuffer.Enum(GameMode.class));
                             case UPDATE_LISTED -> listed = buffer.read(BOOLEAN);
                             case UPDATE_LATENCY -> latency = buffer.read(VAR_INT);
                             case UPDATE_DISPLAY_NAME -> displayName = buffer.read(COMPONENT.optional());
                             case UPDATE_LIST_ORDER -> listOrder = buffer.read(VAR_INT);
+                            case UPDATE_HAT -> displayHat = buffer.read(BOOLEAN);
                         }
                     }
-                    return new Entry(uuid, username, properties, listed, latency, gameMode, displayName, chatSession, listOrder);
+                    return new Entry(uuid, username, properties, listed, latency, gameMode, displayName, chatSession, listOrder, displayHat);
                 }
             };
         }
@@ -140,7 +142,8 @@ public enum Action {
         UPDATE_LISTED((writer, entry) -> writer.write(BOOLEAN, entry.listed)),
         UPDATE_LATENCY((writer, entry) -> writer.write(VAR_INT, entry.latency)),
         UPDATE_DISPLAY_NAME((writer, entry) -> writer.write(COMPONENT.optional(), entry.displayName)),
-        UPDATE_LIST_ORDER((writer, entry) -> writer.write(VAR_INT, entry.listOrder));
+        UPDATE_LIST_ORDER((writer, entry) -> writer.write(VAR_INT, entry.listOrder)),
+        UPDATE_HAT((writer, entry) -> writer.write(BOOLEAN, entry.displayHat));
 
         final Writer writer;
 


Test patch:
diff --git a/src/test/java/net/minestom/server/network/PacketWriteReadTest.java b/src/test/java/net/minestom/server/network/PacketWriteReadTest.java
index a990c6b003c..aeeac16c575 100644
--- a/src/test/java/net/minestom/server/network/PacketWriteReadTest.java
+++ b/src/test/java/net/minestom/server/network/PacketWriteReadTest.java
@@ -126,17 +126,19 @@ public static void setupServer() {
         List<PlayerInfoUpdatePacket.Property> prop = List.of(new PlayerInfoUpdatePacket.Property("textures", skin.textures(), skin.signature()));
 
         SERVER_PACKETS.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.ADD_PLAYER,
-                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "TheMode911", prop, false, 0, GameMode.SURVIVAL, null, null, 0)));
+                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "TheMode911", prop, false, 0, GameMode.SURVIVAL, null, null, 0, true)));
         SERVER_PACKETS.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME,
-                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 0, GameMode.SURVIVAL, Component.text("NotTheMode911"), null, 0)));
+                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 0, GameMode.SURVIVAL, Component.text("NotTheMode911"), null, 0, true)));
         SERVER_PACKETS.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE,
-                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 0, GameMode.CREATIVE, null, null, 0)));
+                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 0, GameMode.CREATIVE, null, null, 0, true)));
         SERVER_PACKETS.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LATENCY,
-                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 20, GameMode.SURVIVAL, null, null, 0)));
+                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 20, GameMode.SURVIVAL, null, null, 0, true)));
         SERVER_PACKETS.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LISTED,
-                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), true, 0, GameMode.SURVIVAL, null, null, 0)));
+                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), true, 0, GameMode.SURVIVAL, null, null, 0, true)));
         SERVER_PACKETS.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER,
-                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 0, GameMode.SURVIVAL, null, null, 42)));
+                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 0, GameMode.SURVIVAL, null, null, 42, true)));
+        SERVER_PACKETS.add(new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_HAT,
+                new PlayerInfoUpdatePacket.Entry(UUID.randomUUID(), "", List.of(), false, 0, GameMode.SURVIVAL, null, null, 0, false)));
         SERVER_PACKETS.add(new PlayerInfoRemovePacket(UUID.randomUUID()));
     }