Чат и список игроков

- Протокол: Chat (что сказал сервер), ChatSend (что сказал клиент) и PlayerList
  (весь список игроков с их режимами).
- Сервер: сообщения уходят всем, а имя отправителя ставит сервер, поэтому подделать
  его нельзя. Список игроков уходит всем целиком, когда он меняется. Команды
  сервера: /help и /players.
- Клиент: перевод наших пакетов в игровые (SPacketChat и SPacketPlayerListItem),
  уходящий чат перехватывается патчем, список игроков обновляется по разнице
  (ушедших убираем, пришедших добавляем).
- Тесты: два игрока, один пишет - второй слышит с правильным именем; список
  игроков приходит обоим и меняется, когда кто-то выходит.

Попутно: run-server.ps1 теперь знает про rsx-plugin-api, иначе сервер не
запускался (нет класса Hitboxes в classpath).

Тесты: протокол 34, сервер 87. Форк собирается чисто.
This commit is contained in:
sasheg dev
2026-10-21 23:10:21 +03:00
parent 7505ddd84e
commit fbd01df884
11 changed files with 477 additions and 6 deletions
@@ -161,6 +161,15 @@ public final class Packets {
/** The client hits something. */
public static final int ATTACK = 0x2D;
/** What somebody said. */
public static final int CHAT = 0x2E;
/** The client says something. */
public static final int CHAT_SEND = 0x2F;
/** Who is in the world. */
public static final int PLAYER_LIST = 0x30;
/**
* One slot of the inventory and what is in it now.
*
@@ -1604,6 +1613,100 @@ public final class Packets {
}
}
/**
* What somebody said.
*
* The name of the sender is here, because the server is the one who
* decides who said what: a client cannot claim to be somebody else.
*/
public static final class Chat implements Packet {
/** The name of the sender, or empty when the server speaks. */
public final String from;
public final String text;
public Chat(String from, String text) {
this.from = from == null ? "" : from;
this.text = text == null ? "" : text;
}
public Buf write() {
Buf buf = new Buf(64 + this.text.length());
buf.writeString(this.from);
buf.writeString(this.text);
return buf;
}
public static Chat read(Buf in) {
return new Chat(in.readString(MAX_TEXT), in.readString(MAX_TEXT));
}
}
/** The client says something. */
public static final class ChatSend implements Packet {
public final String text;
public ChatSend(String text) {
this.text = text == null ? "" : text;
}
public Buf write() {
Buf buf = new Buf(64 + this.text.length());
buf.writeString(this.text);
return buf;
}
public static ChatSend read(Buf in) {
return new ChatSend(in.readString(MAX_TEXT));
}
}
/**
* Who is in the world.
*
* The whole list, every time it changes: the list of a server with a
* handful of players is small, and a full list cannot get out of step
* with itself the way a difference can.
*/
public static final class PlayerList implements Packet {
public final String[] names;
/** The game mode of each player, in the same order. */
public final int[] gameModes;
public PlayerList(String[] names, int[] gameModes) {
this.names = names;
this.gameModes = gameModes;
}
public Buf write() {
Buf buf = new Buf(32 + this.names.length * 24);
buf.writeVarInt(this.names.length);
for (int i = 0; i < this.names.length; i++) {
buf.writeString(this.names[i]);
buf.writeVarInt(i < this.gameModes.length ? this.gameModes[i] : 0);
}
return buf;
}
public static PlayerList read(Buf in) {
int count = in.readVarInt();
if (count < 0 || count > 4096) {
throw new ProtocolException("that many players is not real");
}
String[] names = new String[count];
int[] gameModes = new int[count];
for (int i = 0; i < count; i++) {
names[i] = in.readString(MAX_TEXT);
gameModes[i] = in.readVarInt();
}
return new PlayerList(names, gameModes);
}
}
/**
* Every packet id this build knows, and what it decodes into.
*
@@ -1648,6 +1751,9 @@ public final class Packets {
map.put(Integer.valueOf(HITBOX_INSTANCES), "HitboxInstances");
map.put(Integer.valueOf(HITBOXES_RELOAD), "HitboxesReload");
map.put(Integer.valueOf(ATTACK), "Attack");
map.put(Integer.valueOf(CHAT), "Chat");
map.put(Integer.valueOf(CHAT_SEND), "ChatSend");
map.put(Integer.valueOf(PLAYER_LIST), "PlayerList");
map.put(Integer.valueOf(DISCONNECT), "Disconnect");
return map;
}
@@ -326,6 +326,23 @@ public final class ItemTest {
near("the place of the hit survives", 2.5, attackBack.y);
eq("the hand survives", Integer.valueOf(Packets.Attack.HAND_MAIN),
Integer.valueOf(attackBack.hand));
Packets.Chat chat = new Packets.Chat("Player", "hello there");
Packets.Chat chatBack = Packets.Chat.read(new Buf(chat.write().toByteArray()));
eq("the sender survives", "Player", chatBack.from);
eq("the line survives", "hello there", chatBack.text);
Packets.ChatSend send = new Packets.ChatSend("hi");
Packets.ChatSend sendBack = Packets.ChatSend.read(new Buf(send.write().toByteArray()));
eq("the sent line survives", "hi", sendBack.text);
Packets.PlayerList list = new Packets.PlayerList(
new String[]{"One", "Two"}, new int[]{0, 1});
Packets.PlayerList listBack =
Packets.PlayerList.read(new Buf(list.write().toByteArray()));
eq("two names", Integer.valueOf(2), Integer.valueOf(listBack.names.length));
eq("the first name", "One", listBack.names[0]);
eq("the second mode", Integer.valueOf(1), Integer.valueOf(listBack.gameModes[1]));
}
private static void near(Object what, double expected, double actual) {
@@ -552,7 +552,7 @@ public final class ProtocolTest {
Packets.CONTAINER_OPEN, Packets.CONTAINER_TAKE,
Packets.LEARN_BLUEPRINT,
Packets.HITBOXES, Packets.HITBOX_INSTANCES, Packets.HITBOXES_RELOAD,
Packets.ATTACK,
Packets.ATTACK, Packets.CHAT, Packets.CHAT_SEND, Packets.PLAYER_LIST,
Packets.DISCONNECT,
};
for (int i = 0; i < ids.length; i++) {