From fbd01df88439958391b442525230173ee2264f32 Mon Sep 17 00:00:00 2001 From: sasheg dev <152582704+SashegDev@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:10:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A7=D0=B0=D1=82=20=D0=B8=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BE=D0=BA=20=D0=B8=D0=B3=D1=80=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Протокол: Chat (что сказал сервер), ChatSend (что сказал клиент) и PlayerList (весь список игроков с их режимами). - Сервер: сообщения уходят всем, а имя отправителя ставит сервер, поэтому подделать его нельзя. Список игроков уходит всем целиком, когда он меняется. Команды сервера: /help и /players. - Клиент: перевод наших пакетов в игровые (SPacketChat и SPacketPlayerListItem), уходящий чат перехватывается патчем, список игроков обновляется по разнице (ушедших убираем, пришедших добавляем). - Тесты: два игрока, один пишет - второй слышит с правильным именем; список игроков приходит обоим и меняется, когда кто-то выходит. Попутно: run-server.ps1 теперь знает про rsx-plugin-api, иначе сервер не запускался (нет класса Hitboxes в classpath). Тесты: протокол 34, сервер 87. Форк собирается чисто. --- game-fork/patches/decompiler-fixes.txt | 12 ++ .../client/net/RsxNetworkManager.java | 72 +++++++++++ .../rustnotmc/client/net/RsxWorldBridge.java | 37 +++++- .../java/net/rustnotmc/protocol/Packets.java | 106 ++++++++++++++++ .../java/net/rustnotmc/protocol/ItemTest.java | 17 +++ .../net/rustnotmc/protocol/ProtocolTest.java | 2 +- rsx-server-core/scripts/run-server.ps1 | 7 +- .../net/rustnotmc/server/ClientSession.java | 44 +++++++ .../java/net/rustnotmc/server/Server.java | 38 ++++++ .../java/net/rustnotmc/server/PlayTest.java | 115 ++++++++++++++++++ world/hitboxes.json | 33 +++++ 11 files changed, 477 insertions(+), 6 deletions(-) create mode 100644 world/hitboxes.json diff --git a/game-fork/patches/decompiler-fixes.txt b/game-fork/patches/decompiler-fixes.txt index 3425dc9..7b19c41 100644 --- a/game-fork/patches/decompiler-fixes.txt +++ b/game-fork/patches/decompiler-fixes.txt @@ -1290,3 +1290,15 @@ import org.lwjgl.opengl.ContextCapabilities; this.connection.sendPacket(new CPacketUseEntity(entity)); net.rustnotmc.client.net.RsxNetworkManager.reportAttack(this.mc, entity); +# The chat goes to our server, which is the one who decides who said what. +=== FILE net/minecraft/client/entity/EntityPlayerSP.java +--- BEFORE + public void sendChatMessage(String s) { + this.connection.sendPacket(new CPacketChatMessage(s)); + } +--- AFTER + public void sendChatMessage(String s) { + this.connection.sendPacket(new CPacketChatMessage(s)); + net.rustnotmc.client.net.RsxNetworkManager.sendChat(s); + } + diff --git a/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java b/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java index 67601d2..5882e4f 100644 --- a/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java +++ b/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java @@ -74,6 +74,9 @@ public final class RsxNetworkManager extends NetworkManager { private final String uuid; private final BlockingQueue inbox = new LinkedBlockingQueue(); + + /** The names of the players the server told us about last time. */ + private java.util.List knownPlayers = new java.util.ArrayList(); private final FrameDecoder decoder = new FrameDecoder(); private volatile Socket socket; @@ -260,11 +263,67 @@ public final class RsxNetworkManager extends NetworkManager { net.rustnotmc.client.RsxHitboxDebug.accept(instances); return; } + case Packets.CHAT: { + Packets.Chat chat = Packets.Chat.read(new Buf(frame.body())); + if (this.play != null) { + net.rustnotmc.client.net.RsxWorldBridge.chat(chat.from, chat.text) + .processPacket(this.play); + } + return; + } + case Packets.PLAYER_LIST: { + Packets.PlayerList list = Packets.PlayerList.read(new Buf(frame.body())); + this.updatePlayerList(list); + return; + } default: onPlay(frame); } } + /** + * Brings the list of players up to what the server says. + * + * The server sends the whole list every time it changes, so the client + * only has to take away the names that are gone and add the ones that + * are new. + */ + private void updatePlayerList(Packets.PlayerList list) { + if (this.play == null) { + return; + } + try { + for (String gone : this.knownPlayers) { + if (!contains(list.names, gone)) { + net.rustnotmc.client.net.RsxWorldBridge.playerListItem( + net.minecraft.network.play.server.SPacketPlayerListItem.Action + .REMOVE_PLAYER, + gone, 0).processPacket(this.play); + } + } + for (int i = 0; i < list.names.length; i++) { + int mode = i < list.gameModes.length ? list.gameModes[i] : 0; + net.rustnotmc.client.net.RsxWorldBridge.playerListItem( + net.minecraft.network.play.server.SPacketPlayerListItem.Action + .ADD_PLAYER, + list.names[i], mode).processPacket(this.play); + } + this.knownPlayers = new java.util.ArrayList( + java.util.Arrays.asList(list.names)); + } catch (java.io.IOException broken) { + // the list is a nicety: a broken one must not stop the game + } + } + + private static boolean contains(String[] names, String name) { + for (int i = 0; i < names.length; i++) { + if (names[i].equals(name)) { + return true; + } + } + return false; + } + /** * Tells the server where a hit landed. * @@ -285,6 +344,19 @@ public final class RsxNetworkManager extends NetworkManager { (float) hit.x, (float) hit.y, (float) hit.z, Packets.Attack.HAND_MAIN)); } + /** + * Says something to the server. + * + * The server is the one who decides who said what, so the line goes to + * the server and not into the vanilla's own channel. + */ + public static void sendChat(String text) { + RsxNetworkManager manager = active(); + if (manager != null) { + manager.send(Packets.CHAT_SEND, new Packets.ChatSend(text)); + } + } + /** * Asks the server to read its box file again. * diff --git a/game-fork/src-overlay/net/rustnotmc/client/net/RsxWorldBridge.java b/game-fork/src-overlay/net/rustnotmc/client/net/RsxWorldBridge.java index 5b503af..7fb2f63 100644 --- a/game-fork/src-overlay/net/rustnotmc/client/net/RsxWorldBridge.java +++ b/game-fork/src-overlay/net/rustnotmc/client/net/RsxWorldBridge.java @@ -11,6 +11,7 @@ import net.minecraft.network.Packet; import net.minecraft.network.PacketBuffer; import net.minecraft.network.play.INetHandlerPlayClient; import net.minecraft.network.play.server.SPacketBlockChange; +import net.minecraft.network.play.server.SPacketChat; import net.minecraft.network.play.server.SPacketChunkData; import net.minecraft.network.play.server.SPacketDestroyEntities; import net.minecraft.network.play.server.SPacketEntityTeleport; @@ -225,13 +226,31 @@ public final class RsxWorldBridge { * derives it for an offline account. */ public static SPacketPlayerListItem playerList(String name) throws java.io.IOException { + return playerListItem(SPacketPlayerListItem.Action.ADD_PLAYER, name, 0); + } + + /** + * One entry of the list of players. + * + * The game wants a name, an identity and a game mode, and the server has + * only the name, so the identity is derived the same way the game derives + * it for an offline account. + */ + public static SPacketPlayerListItem playerListItem(SPacketPlayerListItem.Action action, + String name, int gameMode) + throws java.io.IOException { PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); - buffer.writeEnumValue(SPacketPlayerListItem.Action.ADD_PLAYER); + buffer.writeEnumValue(action); buffer.writeVarInt(1); buffer.writeUniqueId(uuidFor(name)); + if (action == SPacketPlayerListItem.Action.REMOVE_PLAYER) { + SPacketPlayerListItem packet = new SPacketPlayerListItem(); + packet.readPacketData(buffer); + return packet; + } buffer.writeString(name); buffer.writeVarInt(0); // no profile properties - buffer.writeVarInt(GameType.SURVIVAL.getID()); + buffer.writeVarInt(gameMode); buffer.writeVarInt(0); // latency buffer.writeBoolean(false); // no display name SPacketPlayerListItem packet = new SPacketPlayerListItem(); @@ -239,6 +258,20 @@ public final class RsxWorldBridge { return packet; } + /** + * The game's packet for a line of chat. + * + * The name of the sender is put in by the server, so the client only has + * to write the line. + */ + public static SPacketChat chat(String from, String text) { + String line = from == null || from.isEmpty() + ? text + : "<" + from + "> " + text; + return new SPacketChat(new net.minecraft.util.text.TextComponentString(line), + net.minecraft.util.text.ChatType.CHAT); + } + /** The identity an offline account gets. */ public static UUID uuidFor(String name) { return UUID.nameUUIDFromBytes( diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java index 7788a8c..67db3bf 100644 --- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java +++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java @@ -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; } diff --git a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ItemTest.java b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ItemTest.java index d3f6315..127cc24 100644 --- a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ItemTest.java +++ b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ItemTest.java @@ -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) { diff --git a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java index fd21b02..a82c00a 100644 --- a/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java +++ b/rsx-protocol/src/test/java/net/rustnotmc/protocol/ProtocolTest.java @@ -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++) { diff --git a/rsx-server-core/scripts/run-server.ps1 b/rsx-server-core/scripts/run-server.ps1 index 4119bc7..3b40cc3 100644 --- a/rsx-server-core/scripts/run-server.ps1 +++ b/rsx-server-core/scripts/run-server.ps1 @@ -24,8 +24,9 @@ $Workspace = Split-Path -Parent $Repo $Protocol = Join-Path $Workspace 'rsx-protocol\build\classes' $Auth = Join-Path $Workspace 'rsx-auth\build\classes' +$PluginApi = Join-Path $Workspace 'rsx-plugin-api\build\classes' $Server = Join-Path $Repo 'build\classes' -foreach ($dep in @($Protocol, $Auth, $Server)) { +foreach ($dep in @($Protocol, $Auth, $PluginApi, $Server)) { if (-not (Test-Path $dep)) { Write-Host " Missing: $dep" -ForegroundColor Red Write-Host ' Build rsx-protocol, rsx-auth and rsx-server-core first.' -ForegroundColor Red @@ -37,7 +38,7 @@ if (-not $Accounts) { $Accounts = Join-Path $Repo 'accounts.txt' } if (-not $World) { $World = Join-Path $Repo 'world' } if ($Register) { - & java -cp "$Protocol;$Auth;$Server" net.rustnotmc.server.ServerMain ` + & java -cp "$Protocol;$Auth;$PluginApi;$Server" net.rustnotmc.server.ServerMain ` --accounts $Accounts --register $Register --password $Password exit $LASTEXITCODE } @@ -56,7 +57,7 @@ Write-Host " port : $Port" Write-Host " capacity : $Capacity" Write-Host '' -& java -cp "$Protocol;$Auth;$Server" net.rustnotmc.server.ServerMain ` +& java -cp "$Protocol;$Auth;$PluginApi;$Server" net.rustnotmc.server.ServerMain ` --port $Port --accounts $Accounts --capacity $Capacity --name $Name --motd $Motd ` --world $World --seed $Seed exit $LASTEXITCODE \ No newline at end of file diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java b/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java index 4dd2906..f9e7853 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java @@ -235,6 +235,9 @@ public final class ClientSession implements Runnable { case Packets.ATTACK: onAttack(frame); return true; + case Packets.CHAT_SEND: + onChatSend(frame); + return true; case Packets.PLAYER_MOVE: onPlayerMove(frame); return true; @@ -640,6 +643,47 @@ public final class ClientSession implements Runnable { send(Packets.INVENTORY, this.player.inventory().toPacket()); } + /** + * The client says something. + * + * The server puts the name of the sender in, so that nobody can claim to + * be somebody else, and then everybody hears it. + */ + private void onChatSend(Frame frame) { + if (this.state != SessionState.PLAYING || this.player == null) { + return; + } + Packets.ChatSend chat = Packets.ChatSend.read(new Buf(frame.body())); + String text = chat.text.trim(); + if (text.isEmpty()) { + return; + } + if (text.startsWith("/")) { + onServerCommand(text); + return; + } + this.server.broadcastChat(this.account, text); + } + + /** + * A command of the server. + * + * There are only a few of them, and they answer everybody, because a + * command that only the one who typed it can see is of no use to anyone. + */ + private void onServerCommand(String command) { + String name = command.split("\\s+")[0].toLowerCase(); + if ("/help".equals(name)) { + this.server.broadcastChat("", "commands: /help, /players"); + return; + } + if ("/players".equals(name)) { + this.server.broadcastChat("", "players: " + this.server.online()); + return; + } + this.server.broadcastChat("", "there is no command " + name); + } + /** * The client hits something. * diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java b/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java index 693f309..993531e 100644 --- a/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java +++ b/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java @@ -437,6 +437,43 @@ public final class Server { if (n > this.peakOnline.get()) { this.peakOnline.set(n); } + sendPlayerList(); + } + + /** + * Tells everybody who is in the world. + * + * The whole list, every time it changes: a list of a handful of players + * is small, and a full list cannot get out of step with itself. + */ + private void sendPlayerList() { + List here = new ArrayList(this.players.values()); + String[] names = new String[here.size()]; + int[] modes = new int[here.size()]; + for (int i = 0; i < here.size(); i++) { + names[i] = here.get(i).account(); + modes[i] = here.get(i).gameMode(); + } + Packets.PlayerList list = new Packets.PlayerList(names, modes); + for (Player player : here) { + player.session().send(Packets.PLAYER_LIST, list); + } + } + + /** + * Says something to everybody who is in the world. + * + * The name of the sender is put in by the server, so that a client cannot + * claim to be somebody else. + */ + public void broadcastChat(String from, String text) { + if (text == null || text.isEmpty()) { + return; + } + Packets.Chat chat = new Packets.Chat(from, text); + for (Player player : this.players.values()) { + player.session().send(Packets.CHAT, chat); + } } void closed(ClientSession session) { @@ -452,6 +489,7 @@ public final class Server { if (session.lastError() != null) { log("closed " + session.address() + ": " + session.lastError()); } + sendPlayerList(); } private static Packets.EntitySpawn spawnPacket(Player player) { diff --git a/rsx-server-core/src/test/java/net/rustnotmc/server/PlayTest.java b/rsx-server-core/src/test/java/net/rustnotmc/server/PlayTest.java index 3549936..4839cdc 100644 --- a/rsx-server-core/src/test/java/net/rustnotmc/server/PlayTest.java +++ b/rsx-server-core/src/test/java/net/rustnotmc/server/PlayTest.java @@ -72,6 +72,12 @@ public final class PlayTest { run("the server's own accounts are shut when a launcher is set", new Throwing() { public void go() throws Exception { testLocalAccountsShut(work); } }); + run("what one player says the other hears", new Throwing() { + public void go() throws Exception { testChat(work); } + }); + run("the list of players is told to everybody", new Throwing() { + public void go() throws Exception { testPlayerList(work); } + }); System.out.println(); for (int i = 0; i < failures.size(); i++) { @@ -374,6 +380,115 @@ public final class PlayTest { } } + /** + * What one player says, the other hears. + * + * The server puts the name of the sender in, so that nobody can claim to + * be somebody else. + */ + private static void testChat(Path work) throws Exception { + Server server = boot(work, "First"); + try { + server.accounts().create("Second", "password1".toCharArray(), 1L); + PlayClient first = new PlayClient("127.0.0.1", server.port(), 5000); + yes("the first player signed in", first.signIn("First", "password1") != null); + PlayClient second = new PlayClient("127.0.0.1", server.port(), 5000); + yes("the second player signed in", second.signIn("Second", "password1") != null); + + second.send(Packets.CHAT_SEND, new Packets.ChatSend("hello there")); + Frame heard = first.await(Packets.CHAT, 5000L); + yes("the other player heard it", heard != null); + if (heard != null) { + Packets.Chat chat = Packets.Chat.read(new Buf(heard.body())); + eq("with the words", "hello there", chat.text); + eq("and the name of the sender", "Second", chat.from); + } + + // the server is the one who names the sender + second.send(Packets.CHAT_SEND, new Packets.ChatSend("spoof")); + Frame again = first.await(Packets.CHAT, 5000L); + if (again != null) { + eq("the name cannot be made up", "Second", + Packets.Chat.read(new Buf(again.body())).from); + } + + first.close(); + second.close(); + } finally { + server.stop(); + } + } + + /** + * The list of players reaches everybody. + * + * The whole list is sent every time it changes, so a client cannot fall + * out of step with it. + */ + private static void testPlayerList(Path work) throws Exception { + Server server = boot(work, "First"); + try { + server.accounts().create("Second", "password1".toCharArray(), 1L); + PlayClient first = new PlayClient("127.0.0.1", server.port(), 5000); + yes("the first player signed in", first.signIn("First", "password1") != null); + PlayClient second = new PlayClient("127.0.0.1", server.port(), 5000); + yes("the second player signed in", second.signIn("Second", "password1") != null); + + Frame list = awaitPlayerList(first, 2, 5000L); + yes("the list arrives", list != null); + if (list != null) { + Packets.PlayerList players = Packets.PlayerList.read(new Buf(list.body())); + eq("with two names", Integer.valueOf(2), + Integer.valueOf(players.names.length)); + boolean hasFirst = false; + boolean hasSecond = false; + for (int i = 0; i < players.names.length; i++) { + hasFirst |= "First".equals(players.names[i]); + hasSecond |= "Second".equals(players.names[i]); + } + yes("the first is in it", hasFirst); + yes("the second is in it", hasSecond); + } + + second.close(); + waitFor(() -> server.players().size() == 1, 3000L); + Frame after = awaitPlayerList(first, 1, 5000L); + yes("and the list changes when somebody leaves", after != null); + if (after != null) { + Packets.PlayerList players = Packets.PlayerList.read(new Buf(after.body())); + eq("with one name left", Integer.valueOf(1), + Integer.valueOf(players.names.length)); + } + first.close(); + } finally { + server.stop(); + } + } + + /** + * Waits for the list of players to have that many names. + * + * The server sends the whole list every time it changes, so the list that + * says two names comes after the one that says one. + */ + private static Frame awaitPlayerList(PlayClient client, int names, long millis) + throws IOException { + long end = System.currentTimeMillis() + millis; + Frame last = null; + while (System.currentTimeMillis() < end) { + Frame frame = client.await(Packets.PLAYER_LIST, 500L); + if (frame == null) { + continue; + } + last = frame; + Packets.PlayerList list = Packets.PlayerList.read(new Buf(frame.body())); + if (list.names.length == names) { + return frame; + } + } + return last; + } + /** * The world outlives the server. * diff --git a/world/hitboxes.json b/world/hitboxes.json new file mode 100644 index 0000000..db2254f --- /dev/null +++ b/world/hitboxes.json @@ -0,0 +1,33 @@ +{ + "models": { + "player": [ + { "name": "head", "part": "head", "min": [-0.3, 1.4, -0.3], "max": [0.3, 1.8, 0.3] }, + { "name": "body", "part": "body", "min": [-0.3, 0.4, -0.3], "max": [0.3, 1.4, 0.3] }, + { "name": "legs", "part": "legs", "min": [-0.3, 0.0, -0.3], "max": [0.3, 0.4, 0.3] } + ], + "ore_stone": [ + { "name": "ore", "min": [-0.45, 0.0, -0.45], "max": [0.45, 0.6, 0.45] } + ], + "ore_metal": [ + { "name": "ore", "min": [-0.45, 0.0, -0.45], "max": [0.45, 0.6, 0.45] } + ], + "ore_sulfur": [ + { "name": "ore", "min": [-0.45, 0.0, -0.45], "max": [0.45, 0.6, 0.45] } + ], + "barrel_blue": [ + { "name": "barrel", "min": [-0.35, 0.0, -0.35], "max": [0.35, 0.95, 0.35] } + ], + "barrel_red": [ + { "name": "barrel", "min": [-0.35, 0.0, -0.35], "max": [0.35, 0.95, 0.35] } + ], + "barrel_white": [ + { "name": "barrel", "min": [-0.35, 0.0, -0.35], "max": [0.35, 0.95, 0.35] } + ], + "crate": [ + { "name": "crate", "min": [-0.45, 0.0, -0.45], "max": [0.45, 0.5, 0.45] } + ], + "food_crate": [ + { "name": "crate", "min": [-0.45, 0.0, -0.45], "max": [0.45, 0.5, 0.45] } + ] + } +}