Чат и список игроков
- Протокол: Chat (что сказал сервер), ChatSend (что сказал клиент) и PlayerList (весь список игроков с их режимами). - Сервер: сообщения уходят всем, а имя отправителя ставит сервер, поэтому подделать его нельзя. Список игроков уходит всем целиком, когда он меняется. Команды сервера: /help и /players. - Клиент: перевод наших пакетов в игровые (SPacketChat и SPacketPlayerListItem), уходящий чат перехватывается патчем, список игроков обновляется по разнице (ушедших убираем, пришедших добавляем). - Тесты: два игрока, один пишет - второй слышит с правильным именем; список игроков приходит обоим и меняется, когда кто-то выходит. Попутно: run-server.ps1 теперь знает про rsx-plugin-api, иначе сервер не запускался (нет класса Hitboxes в classpath). Тесты: протокол 34, сервер 87. Форк собирается чисто.
This commit is contained in:
@@ -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
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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<Player> here = new ArrayList<Player>(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) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user