Клиент говорит с сервером по нашему протоколу

Раньше клиент умел только показывать меню. Теперь он заходит на сервер,
получает наш мир и рисует его.

Как это устроено:

- Наш сетевой слой (net/rustnotmc/client/net) читает наш протокол и
  переводит его пакеты в пакеты игры. Игра остаётся игрой: у неё есть
  мир, игрок, сущности и рендер, и все они работают. Не переписывать их
  ради того, чтобы нарисовать тот же мир иначе, незачем.
- Пакеты игры, которые не имеют соответствия у нас, не отправляются, а
  наоборот: перемещение игрока и ломание блока переводятся в наши.
- Экран входа игры (GuiConnecting) заменён наш: он спрашивает у лаунчера,
  кем пришёл игрок, и подставляет это в рукопожатие.
- Соединение качается из тика игры, а не из экрана. Рукопожатие, которое
  зависит от того, что экран ещё на экране, ломается alt-tab'ом.

Вход теперь через лаунчер, а не внутри игры:

- Лаунчер уже умеет логинить игрока и отдаёт игре имя, uuid и токен
  (--username, --uuid, --accessToken). Это та же роль, что у Yggdrasil в
  ванилле, только лаунчер наш.
- Сервер спрашивает лаунчер, один из ли наш этот игрок, и заодно получает
  бан-лист. Токен используется один раз и не хранится.
- Свои аккаунты сервера, когда настроен лаунчер, закрыты: сервер с двумя
  дверями не имеет дверей. Открыть их можно флагом, но намеренно.
- Если игру запустили без лаунчера, экран всё равно спрашивает имя и
  пароль: это путь для своего сервера и для разработки.

Проверено живьём: клиент вошёл по токену лаунчера, сервер спросил
лаунчер, лаунчер сказал "свой", игрок получил мир, 210 чанков пришли и
лежат на диске, рельеф нарисован.

Что чинилось по дороге:

- Minecraft.setIngameFocus() возвращал на главное меню, когда окно
  получало фокус. Игрок как раз кликает в окно, чтобы напечатать пароль, и
  экран входа исчезал в этот самый момент.
- Ответ лаунчера разбирается, а не ищется подстрокой: {"valid": true} и
  {"valid":true} это один и тот же ответ, и сервер, который понимает
  только второй, ломает работающий лаунчер.
This commit is contained in:
sasheg dev
2026-10-19 20:10:19 +03:00
parent 68dfea27b5
commit 46784aa88f
8 changed files with 541 additions and 5 deletions
@@ -92,6 +92,16 @@ public final class Packets {
public static final int ENTITY_SPAWN = 0x18;
public static final int ENTITY_REMOVE = 0x19;
/**
* The launcher's session, presented to the game server.
*
* The launcher logs the player in once and hands the game a token. The
* game server asks the launcher whether that token is good, and the player
* is in. This is the part of the handshake that Yggdrasil does in
* vanilla, done with our own launcher instead of Mojang's.
*/
public static final int LAUNCHER_AUTH = 0x1A;
public static final int DISCONNECT = 0x7F;
/** The client introducing itself, and naming the account it wants. */
@@ -740,6 +750,38 @@ public final class Packets {
}
}
/**
* The launcher's session, as the game server sees it.
*
* The token is a bearer credential: whoever holds it is the player. It is
* sent to the game server and nowhere else, and the game server uses it
* once, to ask the launcher whether it is good.
*/
public static final class LauncherAuth implements Packet {
public final String account;
public final String uuid;
public final String token;
public LauncherAuth(String account, String uuid, String token) {
this.account = account;
this.uuid = uuid;
this.token = token;
}
public Buf write() {
Buf buf = new Buf(512);
buf.writeString(this.account);
buf.writeString(this.uuid);
buf.writeString(this.token);
return buf;
}
public static LauncherAuth read(Buf in) {
return new LauncherAuth(in.readString(MAX_TEXT), in.readString(MAX_TEXT),
in.readString(MAX_TEXT));
}
}
/**
* Every packet id this build knows, and what it decodes into.
*
@@ -764,6 +806,7 @@ public final class Packets {
map.put(Integer.valueOf(PONG), "Pong");
map.put(Integer.valueOf(ENTITY_SPAWN), "EntitySpawn");
map.put(Integer.valueOf(ENTITY_REMOVE), "EntityRemove");
map.put(Integer.valueOf(LAUNCHER_AUTH), "LauncherAuth");
map.put(Integer.valueOf(DISCONNECT), "Disconnect");
return map;
}
@@ -470,6 +470,14 @@ public final class ProtocolTest {
Packets.EntityRemove remove = new Packets.EntityRemove(11);
eq("remove", Integer.valueOf(11), Integer.valueOf(
Packets.EntityRemove.read(new Buf(remove.write().toByteArray())).entityId));
Packets.LauncherAuth launcher = new Packets.LauncherAuth("Player",
"00000000-0000-0000-0000-000000000000", "a.b.c");
Packets.LauncherAuth launcherBack =
Packets.LauncherAuth.read(new Buf(launcher.write().toByteArray()));
eq("launcher account", "Player", launcherBack.account);
eq("launcher uuid", "00000000-0000-0000-0000-000000000000", launcherBack.uuid);
eq("launcher token", "a.b.c", launcherBack.token);
}
private static void testChunkCompression() {
@@ -519,7 +527,7 @@ public final class ProtocolTest {
Packets.LOGIN, Packets.LOGIN_ACK, Packets.JOIN_WORLD,
Packets.WORLD_JOIN, Packets.CHUNK_DATA, Packets.BLOCK_CHANGE,
Packets.PLAYER_MOVE, Packets.ENTITY_MOVE, Packets.PING, Packets.PONG,
Packets.ENTITY_SPAWN, Packets.ENTITY_REMOVE,
Packets.ENTITY_SPAWN, Packets.ENTITY_REMOVE, Packets.LAUNCHER_AUTH,
Packets.DISCONNECT,
};
for (int i = 0; i < ids.length; i++) {