diff --git a/protocol/src/main/java/mc/protocol/State.java b/protocol/src/main/java/mc/protocol/State.java
index 70cfb0f..5bc1735 100644
--- a/protocol/src/main/java/mc/protocol/State.java
+++ b/protocol/src/main/java/mc/protocol/State.java
@@ -59,6 +59,7 @@ public enum State {
ChunkDataPacket.class, 0x20,
JoinGamePacket.class, 0x23,
PlayerAbilitiesPacket.class,0x2C,
+ PlayerListItemPacket.class,0x2E,
SPlayerPositionAndLookPacket.class, 0x2F,
SpawnPositionPacket.class, 0x46
)
diff --git a/protocol/src/main/java/mc/protocol/packets/server/PlayerListItemPacket.java b/protocol/src/main/java/mc/protocol/packets/server/PlayerListItemPacket.java
new file mode 100644
index 0000000..7bf2a8f
--- /dev/null
+++ b/protocol/src/main/java/mc/protocol/packets/server/PlayerListItemPacket.java
@@ -0,0 +1,171 @@
+package mc.protocol.packets.server;
+
+import lombok.*;
+import mc.protocol.io.NetByteBuf;
+import mc.protocol.packets.ServerSidePacket;
+import mc.protocol.utils.GameMode;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.BiConsumer;
+
+/**
+ * Player List Item packet.
+ *
+ *
Обновление списка игроков, по кнопке TAB.
+ *
+ * Структура пакета
+ *
+ * | FIELD | TYPE | NOTES |
+ * |-------------------|-------------|-------------------------------------------------|
+ * | Action | VarInt | Определяет поля для секции "Player" |
+ * | Number of players | VarInt | Количество элементов в секции "Players" |
+ * | Players | Array | Перечисление полей для каждого игрока (player). |
+ * | | | Поля определяются по "Action" |
+ *
+ *
+ * Варианты "Action"
+ *
+ * | ACTION | PLAYER FIELD | TYPE | NOTES |
+ * | VALUE | DESCRIPTION | | | |
+ * |-------|---------------------|----------------------|-------------|----------------------------------------------------|
+ * | 0 | add player | UUID | UUID | |
+ * | | | Name | String (16) | |
+ * | | | Number Of Properties | VarInt | Количество элементов "Properties" |
+ * | | | Properties | - | см. поля ниже |
+ * | | | Gamemode | VarInt | |
+ * | | | Ping | VarInt | в милисекундах |
+ * | | | Has Display Name | Boolean | |
+ * | | | Display Name | Text | Опционально. Только если "Has Display Name" = true |
+ * | 1 | update gamemode | UUID | UUID | |
+ * | | | Gamemode | VarInt | |
+ * | 2 | update latency | UUID | UUID | |
+ * | | | Ping | VarInt | (см. выше) |
+ * | 3 | update display name | UUID | UUID | |
+ * | | | Has Display Name | Boolean | |
+ * | | | Display Name | Text | (см. выше) |
+ * | 4 | remove player | UUID | UUID | |
+ *
+ *
+ * Поля секции "Properties"
+ *
+ * | FIELD | TYPE | NOTES |
+ * |-----------|------------- |---------------------------------------------|
+ * | Name | String (32767) | |
+ * | Value | String (32767) | |
+ * | Is Signed | Boolean | |
+ * | Signature | String (32767) | Опционально. Только если "Is Signed" = true |
+ *
+ *
+ * @see Player List Item
+ */
+@Data
+public class PlayerListItemPacket implements ServerSidePacket {
+
+ private final List playerItems = new ArrayList<>();
+
+ private Action action;
+
+ @Override
+ public void writeSelf(NetByteBuf netByteBuf) {
+ netByteBuf.writeVarInt(this.action.getCode());
+ netByteBuf.writeVarInt(playerItems.size());
+
+ BiConsumer consumer = null;
+ switch (this.action) {
+ //@formatter:off
+ case ADD_PLAYER: consumer = this::addPlayer; break;
+ case UPDATE_GAMEMODE: consumer = this::updateGamemode; break;
+ case UPDATE_LATENCY: consumer = this::updateLatency; break;
+ case UPDATE_DISPLAY_NAME: consumer = this::updateDisplayName; break;
+ case REMOVE_PLAYER: consumer = this::removePlayer; break;
+ //@formatter:on
+ }
+
+ Objects.requireNonNull(consumer);
+
+ for (PlayerItem playerItem : playerItems) {
+ consumer.accept(netByteBuf, playerItem);
+ }
+ }
+
+ private void addPlayer(NetByteBuf netByteBuf, PlayerItem playerItem) {
+ netByteBuf.writeUUID(playerItem.getUuid());
+ netByteBuf.writeString(playerItem.getName());
+ netByteBuf.writeVarInt(playerItem.getProperties().size());
+
+ for (PlayerProperties property : playerItem.getProperties()) {
+ netByteBuf.writeString(property.getName());
+ netByteBuf.writeString(property.getValue());
+ netByteBuf.writeBoolean(property.getIsSigned());
+ if (property.getIsSigned()) {
+ netByteBuf.writeString(property.getSignature());
+ }
+ }
+
+ netByteBuf.writeVarInt(playerItem.getGamemode().getId());
+ netByteBuf.writeVarInt(playerItem.getPing());
+ netByteBuf.writeBoolean(playerItem.getHasDisplayName());
+ if (playerItem.getHasDisplayName()) {
+ netByteBuf.writeString(playerItem.getDisplayName());
+ }
+ }
+
+ private void updateGamemode(NetByteBuf netByteBuf, PlayerItem playerItem) {
+ netByteBuf.writeUUID(playerItem.getUuid());
+ netByteBuf.writeVarInt(playerItem.getGamemode().getId());
+ }
+
+ private void updateLatency(NetByteBuf netByteBuf, PlayerItem playerItem) {
+ netByteBuf.writeUUID(playerItem.getUuid());
+ netByteBuf.writeVarInt(playerItem.getPing());
+ }
+
+ private void updateDisplayName(NetByteBuf netByteBuf, PlayerItem playerItem) {
+ netByteBuf.writeUUID(playerItem.getUuid());
+ netByteBuf.writeBoolean(playerItem.getHasDisplayName());
+ if (playerItem.getHasDisplayName()) {
+ netByteBuf.writeString(playerItem.getDisplayName());
+ }
+ }
+
+ private void removePlayer(NetByteBuf netByteBuf, PlayerItem playerItem) {
+ netByteBuf.writeUUID(playerItem.getUuid());
+ }
+
+ @RequiredArgsConstructor
+ public enum Action {
+ ADD_PLAYER(0),
+ UPDATE_GAMEMODE(1),
+ UPDATE_LATENCY(2),
+ UPDATE_DISPLAY_NAME(3),
+ REMOVE_PLAYER(4);
+
+ @Getter
+ private final int code;
+ }
+
+ @Builder(builderClassName = "Builder")
+ @Value
+ public static class PlayerItem {
+ List properties = new ArrayList<>();
+
+ UUID uuid;
+ String name;
+ GameMode gamemode;
+ Integer ping;
+ Boolean hasDisplayName;
+ String displayName;
+ }
+
+ @Builder(builderClassName = "Builder")
+ @Value
+ public static class PlayerProperties {
+ String name;
+ String value;
+ Boolean isSigned;
+ String signature;
+ }
+}
diff --git a/server/src/main/java/mc/server/PacketHandler.java b/server/src/main/java/mc/server/PacketHandler.java
index fad97df..05cd6ee 100644
--- a/server/src/main/java/mc/server/PacketHandler.java
+++ b/server/src/main/java/mc/server/PacketHandler.java
@@ -68,16 +68,20 @@ public class PacketHandler {
}
public void onLoginStart(ConnectionContext context, LoginStartPacket loginStartPacket) {
+ var playerUuid = UUID.randomUUID();
+ var playerName = loginStartPacket.getName();
+ var playerGamemode = GameMode.SURVIVAL;
+
var loginSuccessPacket = new LoginSuccessPacket();
- loginSuccessPacket.setUuid(UUID.randomUUID());
- loginSuccessPacket.setName(loginStartPacket.getName());
+ loginSuccessPacket.setUuid(playerUuid);
+ loginSuccessPacket.setName(playerName);
context.sendNow(loginSuccessPacket);
context.setState(State.PLAY);
var joinGamePacket = new JoinGamePacket();
joinGamePacket.setEntityId(random.nextInt());
- joinGamePacket.setGameMode(GameMode.SURVIVAL);
+ joinGamePacket.setGameMode(playerGamemode);
joinGamePacket.setDimension(0/*Overworld*/);
joinGamePacket.setDifficulty(Difficulty.PEACEFUL);
joinGamePacket.setLevelType(LevelType.FLAT);
@@ -122,6 +126,20 @@ public class PacketHandler {
context.send(pingPacket);
context.flushSending();
+
+ // -- Эксперименты -- //
+
+ var playerListItemPacket = new PlayerListItemPacket();
+ playerListItemPacket.setAction(PlayerListItemPacket.Action.ADD_PLAYER);
+ playerListItemPacket.getPlayerItems().add(PlayerListItemPacket.PlayerItem.builder()
+ .uuid(playerUuid)
+ .name(playerName)
+ .gamemode(playerGamemode)
+ .ping(100)
+ .hasDisplayName(false)
+ .build());
+
+ context.sendNow(playerListItemPacket);
}
private static String faviconToBase64(Path iconPath) {