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

- Протокол: 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;
}