Клиент говорит с сервером по нашему протоколу
Раньше клиент умел только показывать меню. Теперь он заходит на сервер,
получает наш мир и рисует его.
Как это устроено:
- Наш сетевой слой (net/rustnotmc/client/net) читает наш протокол и
переводит его пакеты в пакеты игры. Игра остаётся игрой: у неё есть
мир, игрок, сущности и рендер, и все они работают. Не переписывать их
ради того, чтобы нарисовать тот же мир иначе, незачем.
- Пакеты игры, которые не имеют соответствия у нас, не отправляются, а
наоборот: перемещение игрока и ломание блока переводятся в наши.
- Экран входа игры (GuiConnecting) заменён наш: он спрашивает у лаунчера,
кем пришёл игрок, и подставляет это в рукопожатие.
- Соединение качается из тика игры, а не из экрана. Рукопожатие, которое
зависит от того, что экран ещё на экране, ломается alt-tab'ом.
Вход теперь через лаунчер, а не внутри игры:
- Лаунчер уже умеет логинить игрока и отдаёт игре имя, uuid и токен
(--username, --uuid, --accessToken). Это та же роль, что у Yggdrasil в
ванилле, только лаунчер наш.
- Сервер спрашивает лаунчер, один из ли наш этот игрок, и заодно получает
бан-лист. Токен используется один раз и не хранится.
- Свои аккаунты сервера, когда настроен лаунчер, закрыты: сервер с двумя
дверями не имеет дверей. Открыть их можно флагом, но намеренно.
- Если игру запустили без лаунчера, экран всё равно спрашивает имя и
пароль: это путь для своего сервера и для разработки.
Проверено живьём: клиент вошёл по токену лаунчера, сервер спросил
лаунчер, лаунчер сказал "свой", игрок получил мир, 210 чанков пришли и
лежат на диске, рельеф нарисован.
Что чинилось по дороге:
- Minecraft.setIngameFocus() возвращал на главное меню, когда окно
получало фокус. Игрок как раз кликает в окно, чтобы напечатать пароль, и
экран входа исчезал в этот самый момент.
- Ответ лаунчера разбирается, а не ищется подстрокой: {"valid": true} и
{"valid":true} это один и тот же ответ, и сервер, который понимает
только второй, ломает работающий лаунчер.
This commit is contained in:
@@ -1117,3 +1117,19 @@ this.framebuffer.framebufferRender(l * k, i1 * k);
|
||||
net.rustnotmc.client.RsxBootstrap.renderLoadingScreen(this.mc);
|
||||
|
||||
this.mc.updateDisplay();
|
||||
|
||||
=== FILE net/minecraft/client/Minecraft.java
|
||||
--- BEFORE
|
||||
this.mouseHelper.grabMouseCursor();
|
||||
this.displayGuiScreen((GuiScreen) null);
|
||||
this.leftClickCounter = 10000;
|
||||
--- AFTER
|
||||
this.mouseHelper.grabMouseCursor();
|
||||
// the login screen has to survive the window being focused.
|
||||
// Focusing the window is exactly what a player does before
|
||||
// typing their password, and the game would otherwise put the
|
||||
// main menu up in the moment the cursor lands in the box.
|
||||
if (!(this.currentScreen instanceof GuiConnecting)) {
|
||||
this.displayGuiScreen((GuiScreen) null);
|
||||
}
|
||||
this.leftClickCounter = 10000;
|
||||
|
||||
@@ -32,7 +32,14 @@ foreach ($j in (Get-ChildItem (Join-Path $Astral 'libraries') -Recurse -Filter '
|
||||
# from drifting apart.
|
||||
$Loader = Join-Path $Client 'dist\rsx-loader.jar'
|
||||
|
||||
$cp = @($good + $(if (Test-Path $Loader) { $Loader -replace '\\', '/' } else { $null })) -join ';'
|
||||
# The client speaks the server's own protocol, so the protocol and the account
|
||||
# code are compiled against as well. They are the same jars the game runs with.
|
||||
$Protocol = Join-Path $Root 'rsx-protocol\dist\rsx-protocol.jar'
|
||||
$Auth = Join-Path $Root 'rsx-auth\dist\rsx-auth.jar'
|
||||
|
||||
$cp = @($good + $(if (Test-Path $Loader) { $Loader -replace '\\', '/' } else { $null }) `
|
||||
+ $(if (Test-Path $Protocol) { $Protocol -replace '\\', '/' } else { $null }) `
|
||||
+ $(if (Test-Path $Auth) { $Auth -replace '\\', '/' } else { $null })) -join ';'
|
||||
$argf = Join-Path $Temp 'fork-build.txt'
|
||||
$errf = Join-Path $Temp 'fork-build-errors.txt'
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
|
||||
@@ -128,9 +128,14 @@ foreach ($j in (Get-ChildItem (Join-Path $Astral 'libraries') -Recurse -Filter '
|
||||
if ($ok) { $good.Add(($j.FullName -replace '\\', '/')) }
|
||||
}
|
||||
# The fork calls into the mod loader, so the loader has to be visible to javac
|
||||
# exactly as it is at runtime.
|
||||
# exactly as it is at runtime. The protocol and the account code are compiled
|
||||
# against too: the client speaks the server's protocol, not the vanilla one.
|
||||
$Loader = Join-Path $Client 'dist\rsx-loader.jar'
|
||||
$cp = @($good + $(if (Test-Path $Loader) { $Loader -replace '\\', '/' } else { $null })) -join ';'
|
||||
$Protocol = Join-Path $Root 'rsx-protocol\dist\rsx-protocol.jar'
|
||||
$Auth = Join-Path $Root 'rsx-auth\dist\rsx-auth.jar'
|
||||
$cp = @($good + $(if (Test-Path $Loader) { $Loader -replace '\\', '/' } else { $null }) `
|
||||
+ $(if (Test-Path $Protocol) { $Protocol -replace '\\', '/' } else { $null }) `
|
||||
+ $(if (Test-Path $Auth) { $Auth -replace '\\', '/' } else { $null })) -join ';'
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($a in @('-nowarn', '-proc:none', '-encoding', 'UTF-8', '--release', '17', '-d', ($Out -replace '\\', '/'))) { [void]$sb.AppendLine($a) }
|
||||
[void]$sb.AppendLine('-cp'); [void]$sb.AppendLine('"' + $cp + '"')
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package net.minecraft.client.multiplayer;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import java.io.IOException;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.gui.GuiDisconnected;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.gui.GuiTextField;
|
||||
import net.minecraft.util.text.TextComponentTranslation;
|
||||
import net.rustnotmc.client.gui.RsxPasswordField;
|
||||
import net.rustnotmc.client.net.RsxNetworkManager;
|
||||
import net.rustnotmc.client.net.RsxWorldBridge;
|
||||
|
||||
/**
|
||||
* The screen that gets the player into a server.
|
||||
*
|
||||
* The launcher is where accounts live, so the launcher signs the player in and
|
||||
* hands the game a token. This screen then has nothing to ask: it takes the
|
||||
* token out of the session the launcher built and offers it to the server.
|
||||
*
|
||||
* When the game was started without a launcher there is no session and
|
||||
* somebody has to say who they are, so the screen asks for a name and a
|
||||
* password. That is the way in for a server of our own and the way in for
|
||||
* development; a server with a launcher in front of it does not accept it.
|
||||
*
|
||||
* This replaces the game's own connecting screen, which is where the player
|
||||
* already is when they press "join server", so the flow stays the one they
|
||||
* know.
|
||||
*/
|
||||
public class GuiConnecting extends GuiScreen {
|
||||
|
||||
/** What the launcher leaves in the session when nobody signed in. */
|
||||
private static final String NO_TOKEN = "0";
|
||||
|
||||
private final GuiScreen previousGuiScreen;
|
||||
private final String host;
|
||||
private final int port;
|
||||
|
||||
private GuiTextField accountField;
|
||||
private RsxPasswordField passwordField;
|
||||
private RsxNetworkManager networkManager;
|
||||
private volatile String status = "";
|
||||
|
||||
/** The launcher's session, if the game was started by the launcher. */
|
||||
private String launcherAccount;
|
||||
private String launcherUuid;
|
||||
private String launcherToken;
|
||||
|
||||
public GuiConnecting(GuiScreen guiscreen, Minecraft minecraft, ServerData serverdata) {
|
||||
this.mc = minecraft;
|
||||
this.previousGuiScreen = guiscreen;
|
||||
ServerAddress serveraddress = ServerAddress.fromString(serverdata.serverIP);
|
||||
minecraft.loadWorld((WorldClient) null);
|
||||
minecraft.setServerData(serverdata);
|
||||
this.host = serveraddress.getIP();
|
||||
this.port = serveraddress.getPort();
|
||||
}
|
||||
|
||||
public GuiConnecting(GuiScreen guiscreen, Minecraft minecraft, String s, int i) {
|
||||
this.mc = minecraft;
|
||||
this.previousGuiScreen = guiscreen;
|
||||
minecraft.loadWorld((WorldClient) null);
|
||||
this.host = s;
|
||||
this.port = i;
|
||||
}
|
||||
|
||||
public void initGui() {
|
||||
this.buttonList.clear();
|
||||
|
||||
this.launcherAccount = sessionValue(this.mc, 0);
|
||||
this.launcherUuid = sessionValue(this.mc, 1);
|
||||
this.launcherToken = sessionValue(this.mc, 2);
|
||||
|
||||
this.accountField = new GuiTextField(0, this.fontRenderer, this.width / 2 - 100,
|
||||
this.height / 4 + 20, 200, 20);
|
||||
this.accountField.setMaxStringLength(32);
|
||||
this.accountField.setText(this.launcherAccount == null
|
||||
? this.mc.getSession().getProfile().getName() : this.launcherAccount);
|
||||
|
||||
this.passwordField = new RsxPasswordField(1, this.fontRenderer, this.width / 2 - 100,
|
||||
this.height / 4 + 50, 200, 20);
|
||||
this.passwordField.setMaxStringLength(128);
|
||||
|
||||
// two buttons side by side, and they must not lie on top of each other:
|
||||
// the plain constructor makes a button two hundred wide
|
||||
this.buttonList.add(new GuiButton(0, this.width / 2 - 100, this.height / 4 + 90,
|
||||
98, 20, "Connect"));
|
||||
this.buttonList.add(new GuiButton(1, this.width / 2 + 2, this.height / 4 + 90,
|
||||
98, 20, "Cancel"));
|
||||
|
||||
if (this.hasLauncherSession()) {
|
||||
// the launcher already signed the player in, so there is nothing to
|
||||
// ask for and nothing to wait for
|
||||
this.passwordField.setFocused(true);
|
||||
this.start();
|
||||
} else {
|
||||
this.passwordField.setFocused(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateScreen() {
|
||||
if (this.networkManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.networkManager.isChannelOpen()) {
|
||||
this.networkManager.processReceivedPackets();
|
||||
return;
|
||||
}
|
||||
|
||||
String reason = this.networkManager.failure();
|
||||
this.mc.displayGuiScreen(new GuiDisconnected(this.previousGuiScreen, "connect.failed",
|
||||
new TextComponentTranslation("disconnect.genericReason", new Object[]{
|
||||
reason == null ? "the connection was closed" : reason})));
|
||||
}
|
||||
|
||||
protected void keyTyped(char c0, int i) {
|
||||
if (c0 == '\r' || c0 == '\n') {
|
||||
this.start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.accountField.isFocused()) {
|
||||
this.accountField.textboxKeyTyped(c0, i);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.passwordField.isFocused()) {
|
||||
this.passwordField.textboxKeyTyped(c0, i);
|
||||
}
|
||||
}
|
||||
|
||||
public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
|
||||
super.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
this.accountField.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
this.passwordField.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
}
|
||||
|
||||
protected void actionPerformed(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
this.start();
|
||||
} else if (guibutton.id == 1) {
|
||||
this.mc.displayGuiScreen(this.previousGuiScreen);
|
||||
}
|
||||
}
|
||||
|
||||
public void drawScreen(int i, int j, float f) {
|
||||
this.drawDefaultBackground();
|
||||
this.drawCenteredString(this.fontRenderer, "RustNotMC", this.width / 2,
|
||||
this.height / 4 - 20, 16777215);
|
||||
|
||||
if (this.hasLauncherSession()) {
|
||||
this.drawCenteredString(this.fontRenderer, "signed in as " + this.launcherAccount,
|
||||
this.width / 2, this.height / 4 + 10, 8421504);
|
||||
} else {
|
||||
this.drawString(this.fontRenderer, "Account", this.width / 2 - 100,
|
||||
this.height / 4 + 10, 8421504);
|
||||
this.accountField.drawTextBox();
|
||||
this.drawString(this.fontRenderer, "Password", this.width / 2 - 100,
|
||||
this.height / 4 + 40, 8421504);
|
||||
this.passwordField.drawTextBox();
|
||||
}
|
||||
|
||||
if (!this.status.isEmpty()) {
|
||||
this.drawCenteredString(this.fontRenderer, this.status, this.width / 2,
|
||||
this.height / 4 + 120, 16777215);
|
||||
}
|
||||
|
||||
super.drawScreen(i, j, f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the connection.
|
||||
*
|
||||
* The socket is opened on a thread of its own, because a server that does
|
||||
* not answer must not freeze the game.
|
||||
*/
|
||||
private void start() {
|
||||
if (this.networkManager != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String account;
|
||||
String token = null;
|
||||
String uuid = null;
|
||||
char[] password = new char[0];
|
||||
|
||||
if (this.hasLauncherSession()) {
|
||||
account = this.launcherAccount;
|
||||
token = this.launcherToken;
|
||||
uuid = this.launcherUuid;
|
||||
} else {
|
||||
account = this.accountField.getText().trim();
|
||||
password = this.passwordField.getText().toCharArray();
|
||||
if (account.isEmpty()) {
|
||||
this.status = "an account name is needed";
|
||||
return;
|
||||
}
|
||||
if (password.length == 0) {
|
||||
this.status = "a password is needed";
|
||||
return;
|
||||
}
|
||||
this.passwordField.setText("");
|
||||
}
|
||||
|
||||
this.status = "connecting to " + this.host + ":" + this.port;
|
||||
System.out.println("[rsx] connecting to " + this.host + ":" + this.port + " as " + account);
|
||||
|
||||
final RsxNetworkManager manager = new RsxNetworkManager(this.mc, this.previousGuiScreen,
|
||||
new GameProfile(RsxWorldBridge.uuidFor(account), account),
|
||||
account, password, token, uuid);
|
||||
this.networkManager = manager;
|
||||
|
||||
(new Thread("Rsx Connect") {
|
||||
public void run() {
|
||||
try {
|
||||
manager.connect(GuiConnecting.this.host, GuiConnecting.this.port);
|
||||
} catch (IOException e) {
|
||||
manager.fail("could not connect: " + e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/** True when the launcher gave the game a session to use. */
|
||||
private boolean hasLauncherSession() {
|
||||
return this.launcherToken != null && !this.launcherToken.isEmpty()
|
||||
&& !NO_TOKEN.equals(this.launcherToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* A piece of the session the launcher built.
|
||||
*
|
||||
* Zero is the name, one is the identity, two is the token.
|
||||
*/
|
||||
private static String sessionValue(Minecraft minecraft, int which) {
|
||||
if (minecraft == null || minecraft.getSession() == null) {
|
||||
return null;
|
||||
}
|
||||
switch (which) {
|
||||
case 0:
|
||||
return minecraft.getSession().getUsername();
|
||||
case 1:
|
||||
return minecraft.getSession().getPlayerID();
|
||||
case 2:
|
||||
return minecraft.getSession().getToken();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,11 @@ public final class RsxBootstrap {
|
||||
* mod that counts ticks would disagree with the world it is drawn over.
|
||||
*/
|
||||
public static synchronized void clientTick() {
|
||||
// the connection is pumped from here, not from the screen that started
|
||||
// it. A handshake that only runs while a screen is up is a handshake
|
||||
// that the player can break by moving the window.
|
||||
net.rustnotmc.client.net.RsxNetworkManager.pump();
|
||||
|
||||
if (loader == null || broken || tickFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.rustnotmc.client.gui;
|
||||
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiTextField;
|
||||
|
||||
/**
|
||||
* A text field that shows dots instead of what is in it.
|
||||
*
|
||||
* The game has no password field, and a password that appears on the screen
|
||||
* while it is typed is a password that ends up in a screenshot.
|
||||
*
|
||||
* The game draws the text field from its own private text, so the only way to
|
||||
* mask it without copying the whole field is to put the dots in for the moment
|
||||
* it takes to draw and put the real text back afterwards. The cursor ends up
|
||||
* at the end of the text, which for a password is where it is wanted anyway.
|
||||
*/
|
||||
public class RsxPasswordField extends GuiTextField {
|
||||
|
||||
public RsxPasswordField(int id, FontRenderer font, int x, int y, int width, int height) {
|
||||
super(id, font, x, y, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTextBox() {
|
||||
String real = getText();
|
||||
if (real.isEmpty()) {
|
||||
super.drawTextBox();
|
||||
return;
|
||||
}
|
||||
StringBuilder dots = new StringBuilder(real.length());
|
||||
for (int i = 0; i < real.length(); i++) {
|
||||
dots.append('*');
|
||||
}
|
||||
setText(dots.toString());
|
||||
super.drawTextBox();
|
||||
setText(real);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.rustnotmc.client.net;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.state.IBlockState;
|
||||
import net.minecraft.init.Blocks;
|
||||
|
||||
/**
|
||||
* What a block id from the server looks like in the game.
|
||||
*
|
||||
* The server has a small vocabulary of blocks and the game has a large one.
|
||||
* The server's ids are what travels on the wire, so they are the ones that
|
||||
* have to be stable; the game's blocks are what gets drawn.
|
||||
*
|
||||
* A block the game has and the server does not is not sent, and a block the
|
||||
* server has and the game does not is drawn as air. Both directions lose
|
||||
* something and both are honest about it: the server's world is a small one
|
||||
* for now, and pretending otherwise would mean sending ids the client cannot
|
||||
* draw.
|
||||
*/
|
||||
public final class RsxBlockMap {
|
||||
|
||||
private RsxBlockMap() {
|
||||
}
|
||||
|
||||
/** The game state for a block id from the server. */
|
||||
public static IBlockState stateFor(byte id) {
|
||||
switch (id) {
|
||||
case net.rustnotmc.protocol.Blocks.STONE:
|
||||
return Blocks.STONE.getDefaultState();
|
||||
case net.rustnotmc.protocol.Blocks.DIRT:
|
||||
return Blocks.DIRT.getDefaultState();
|
||||
case net.rustnotmc.protocol.Blocks.GRASS:
|
||||
return Blocks.GRASS.getDefaultState();
|
||||
case net.rustnotmc.protocol.Blocks.SAND:
|
||||
return Blocks.SAND.getDefaultState();
|
||||
case net.rustnotmc.protocol.Blocks.WATER:
|
||||
return Blocks.WATER.getDefaultState();
|
||||
case net.rustnotmc.protocol.Blocks.WOOD:
|
||||
return Blocks.LOG.getDefaultState();
|
||||
case net.rustnotmc.protocol.Blocks.LEAVES:
|
||||
return Blocks.LEAVES.getDefaultState();
|
||||
default:
|
||||
return Blocks.AIR.getDefaultState();
|
||||
}
|
||||
}
|
||||
|
||||
/** The server id for a game block, or -1 when the server has no such block. */
|
||||
public static byte idFor(IBlockState state) {
|
||||
if (state == null) {
|
||||
return -1;
|
||||
}
|
||||
Block block = state.getBlock();
|
||||
if (block == Blocks.STONE) {
|
||||
return net.rustnotmc.protocol.Blocks.STONE;
|
||||
}
|
||||
if (block == Blocks.DIRT) {
|
||||
return net.rustnotmc.protocol.Blocks.DIRT;
|
||||
}
|
||||
if (block == Blocks.GRASS) {
|
||||
return net.rustnotmc.protocol.Blocks.GRASS;
|
||||
}
|
||||
if (block == Blocks.SAND) {
|
||||
return net.rustnotmc.protocol.Blocks.SAND;
|
||||
}
|
||||
if (block == Blocks.WATER) {
|
||||
return net.rustnotmc.protocol.Blocks.WATER;
|
||||
}
|
||||
if (block == Blocks.LOG) {
|
||||
return net.rustnotmc.protocol.Blocks.WOOD;
|
||||
}
|
||||
if (block == Blocks.LEAVES) {
|
||||
return net.rustnotmc.protocol.Blocks.LEAVES;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
package net.rustnotmc.client.net;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.network.NetHandlerPlayClient;
|
||||
import net.minecraft.network.EnumConnectionState;
|
||||
import net.minecraft.network.EnumPacketDirection;
|
||||
import net.minecraft.network.INetHandler;
|
||||
import net.minecraft.network.NetworkManager;
|
||||
import net.minecraft.network.Packet;
|
||||
import net.minecraft.network.play.INetHandlerPlayClient;import net.minecraft.network.play.client.CPacketPlayer;
|
||||
import net.minecraft.network.play.client.CPacketPlayerDigging;
|
||||
import net.minecraft.util.text.ITextComponent;
|
||||
import net.minecraft.util.text.TextComponentTranslation;
|
||||
import net.rustnotmc.protocol.Buf;
|
||||
import net.rustnotmc.protocol.Frame;
|
||||
import net.rustnotmc.protocol.FrameDecoder;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
import net.rustnotmc.protocol.ProtocolException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* The client's side of the wire.
|
||||
*
|
||||
* The game is a Minecraft client and it will stay one: the world, the player,
|
||||
* the entities and the renderer all work, and there is no sense in writing a
|
||||
* second set of them. What the game does not have is the server's packets, so
|
||||
* this class speaks the server's protocol and hands the game the packets it
|
||||
* already understands.
|
||||
*
|
||||
* The transport is a plain socket with a thread reading it. The game's own
|
||||
* transport is Netty, and Netty is left alone: the packets are read here and
|
||||
* delivered on the thread the game runs on, which is what the game expects.
|
||||
*/
|
||||
public final class RsxNetworkManager extends NetworkManager {
|
||||
|
||||
/** How long to wait for the server to answer the connection. */
|
||||
public static final int CONNECT_TIMEOUT_MILLIS = 15000;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger();
|
||||
|
||||
/** Set with -Drsx.trace=true to see every packet that arrives. */
|
||||
private static final boolean TRACE = System.getProperty("rsx.trace") != null;
|
||||
|
||||
/**
|
||||
* The connection that is open, if any.
|
||||
*
|
||||
* The game pumps the connection from its own tick, so that a packet is
|
||||
* dealt with even when the screen that started the connection is gone. A
|
||||
* handshake that depends on a screen surviving is a handshake that can be
|
||||
* interrupted by the player alt-tabbing away.
|
||||
*/
|
||||
private static volatile RsxNetworkManager active;
|
||||
|
||||
private final Minecraft mc;
|
||||
private final GuiScreen previousScreen;
|
||||
private final GameProfile profile;
|
||||
private final String account;
|
||||
private final char[] password;
|
||||
private final String token;
|
||||
private final String uuid;
|
||||
|
||||
private final BlockingQueue<Frame> inbox = new LinkedBlockingQueue<Frame>();
|
||||
private final FrameDecoder decoder = new FrameDecoder();
|
||||
|
||||
private volatile Socket socket;
|
||||
private volatile OutputStream out;
|
||||
private volatile boolean open;
|
||||
private volatile boolean disconnected;
|
||||
private volatile ITextComponent exitMessage;
|
||||
private volatile String failure;
|
||||
private volatile NetHandlerPlayClient play;
|
||||
|
||||
private byte[] nonce;
|
||||
private byte[] salt;
|
||||
|
||||
public RsxNetworkManager(Minecraft mc, GuiScreen previousScreen, GameProfile profile,
|
||||
String account, char[] password,
|
||||
String token, String uuid) {
|
||||
super(EnumPacketDirection.CLIENTBOUND);
|
||||
this.mc = mc;
|
||||
this.previousScreen = previousScreen;
|
||||
this.profile = profile;
|
||||
this.account = account;
|
||||
this.password = password;
|
||||
this.token = token;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
/** The connection that is open, if any. */
|
||||
public static RsxNetworkManager active() {
|
||||
return active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals with whatever arrived, if there is a connection.
|
||||
*
|
||||
* Called from the game's tick. During the play phase the game pumps the
|
||||
* connection itself, and doing it twice costs nothing: the queue is
|
||||
* emptied by whoever gets there first.
|
||||
*/
|
||||
public static void pump() {
|
||||
RsxNetworkManager manager = active;
|
||||
if (manager != null) {
|
||||
manager.processReceivedPackets();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- connecting
|
||||
|
||||
/**
|
||||
* Opens the socket and says hello.
|
||||
*
|
||||
* The rest of the handshake happens on the thread that reads the socket,
|
||||
* so that a slow server does not freeze the game while the password is
|
||||
* being checked.
|
||||
*/
|
||||
public void connect(String host, int port) throws IOException {
|
||||
Socket socket = new Socket();
|
||||
socket.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT_MILLIS);
|
||||
socket.setTcpNoDelay(true);
|
||||
this.socket = socket;
|
||||
this.out = socket.getOutputStream();
|
||||
this.open = true;
|
||||
active = this;
|
||||
|
||||
Thread reader = new Thread(new Runnable() {
|
||||
public void run() {
|
||||
readLoop();
|
||||
}
|
||||
}, "rsx-client-reader");
|
||||
reader.setDaemon(true);
|
||||
reader.start();
|
||||
|
||||
send(Packets.HELLO, new Packets.Hello(Frame.VERSION, RsxNetworkManager.this.account,
|
||||
"RustNotMC", 1));
|
||||
}
|
||||
|
||||
private void readLoop() {
|
||||
try {
|
||||
InputStream in = this.socket.getInputStream();
|
||||
byte[] buffer = new byte[16384];
|
||||
while (this.open) {
|
||||
int read = in.read(buffer);
|
||||
if (read < 0) {
|
||||
lost("the server closed the connection");
|
||||
return;
|
||||
}
|
||||
for (Frame frame : this.decoder.offer(buffer, 0, read)) {
|
||||
this.inbox.add(frame);
|
||||
}
|
||||
}
|
||||
} catch (ProtocolException broken) {
|
||||
lost(broken.getMessage());
|
||||
} catch (IOException e) {
|
||||
if (this.open) {
|
||||
lost(String.valueOf(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The reason the connection ended, or null while it is alive. */
|
||||
public String failure() {
|
||||
return this.failure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives up on the connection, with a reason.
|
||||
*
|
||||
* Called from the thread that was opening the socket, when the socket
|
||||
* could not be opened at all.
|
||||
*/
|
||||
public void fail(String reason) {
|
||||
lost(reason);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- packets
|
||||
|
||||
/**
|
||||
* Takes what arrived off the socket and hands it to the game.
|
||||
*
|
||||
* Called by the game on its own thread, which is the thread everything
|
||||
* else in the game happens on. A packet that arrives on the reader thread
|
||||
* is only put in a queue; it is dealt with here.
|
||||
*/
|
||||
@Override
|
||||
public void processReceivedPackets() {
|
||||
Frame frame;
|
||||
while ((frame = this.inbox.poll()) != null) {
|
||||
try {
|
||||
handle(frame);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("RustNotMC: could not handle a packet", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handle(Frame frame) throws java.io.IOException {
|
||||
if (TRACE) {
|
||||
System.out.println("[rsx] packet " + frame.packetId() + " ("
|
||||
+ frame.body().length + " bytes)");
|
||||
}
|
||||
switch (frame.packetId()) {
|
||||
case Packets.HELLO_ACK:
|
||||
onHelloAck(frame);
|
||||
return;
|
||||
case Packets.AUTH_ACK:
|
||||
onAuthAck(frame);
|
||||
return;
|
||||
case Packets.LOGIN_ACK:
|
||||
onLoginAck(frame);
|
||||
return;
|
||||
case Packets.DISCONNECT: {
|
||||
Packets.Disconnect goodbye =
|
||||
Packets.Disconnect.read(new Buf(frame.body()));
|
||||
lost(goodbye.reason);
|
||||
return;
|
||||
}
|
||||
case Packets.WORLD_JOIN:
|
||||
onWorldJoin(frame);
|
||||
return;
|
||||
default:
|
||||
onPlay(frame);
|
||||
}
|
||||
}
|
||||
|
||||
private void onHelloAck(Frame frame) {
|
||||
Packets.HelloAck hello = Packets.HelloAck.read(new Buf(frame.body()));
|
||||
this.nonce = hello.nonce;
|
||||
this.salt = hello.salt;
|
||||
|
||||
if (this.token != null && !this.token.isEmpty()) {
|
||||
// the launcher already signed the player in, and the token it
|
||||
// handed the game is all the game server needs
|
||||
send(Packets.LAUNCHER_AUTH,
|
||||
new Packets.LauncherAuth(this.account, this.uuid, this.token));
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] proof = Packets.Auth.proofFor(this.account, this.password, this.salt, this.nonce);
|
||||
java.util.Arrays.fill(this.password, '\0');
|
||||
send(Packets.AUTH, new Packets.Auth(this.account, proof));
|
||||
}
|
||||
|
||||
private void onAuthAck(Frame frame) {
|
||||
Packets.AuthAck auth = Packets.AuthAck.read(new Buf(frame.body()));
|
||||
if (!auth.accepted) {
|
||||
lost(auth.reason);
|
||||
return;
|
||||
}
|
||||
send(Packets.LOGIN, new Packets.Login(this.account, auth.token));
|
||||
}
|
||||
|
||||
private void onLoginAck(Frame frame) {
|
||||
Packets.LoginAck login = Packets.LoginAck.read(new Buf(frame.body()));
|
||||
if (!login.accepted) {
|
||||
lost(login.reason);
|
||||
return;
|
||||
}
|
||||
// the game takes over from here: from this moment the packets are the
|
||||
// game's own, and only the transport is ours
|
||||
this.play = new NetHandlerPlayClient(this.mc, this.previousScreen, this, this.profile);
|
||||
setNetHandler(this.play);
|
||||
send(Packets.JOIN_WORLD, Packets.JoinWorld.fresh());
|
||||
}
|
||||
|
||||
private void onWorldJoin(Frame frame) {
|
||||
Packets.WorldJoin world = Packets.WorldJoin.read(new Buf(frame.body()));
|
||||
if (this.play == null) {
|
||||
return;
|
||||
}
|
||||
List<Packet<INetHandlerPlayClient>> packets = RsxWorldBridge.join(world.entityId, 200,
|
||||
world.spawnX, world.spawnY, world.spawnZ);
|
||||
for (int i = 0; i < packets.size(); i++) {
|
||||
packets.get(i).processPacket(this.play);
|
||||
}
|
||||
}
|
||||
|
||||
private void onPlay(Frame frame) throws java.io.IOException {
|
||||
NetHandlerPlayClient handler = this.play;
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
if (frame.packetId() == Packets.CHUNK_DATA) {
|
||||
Packets.ChunkData chunk = Packets.ChunkData.read(new Buf(frame.body()));
|
||||
if (this.mc.world == null) {
|
||||
return;
|
||||
}
|
||||
RsxWorldBridge.chunk(this.mc.world, chunk.chunkX, chunk.chunkZ, chunk.blocks)
|
||||
.processPacket(handler);
|
||||
return;
|
||||
}
|
||||
if (frame.packetId() == Packets.ENTITY_SPAWN) {
|
||||
Packets.EntitySpawn spawn = Packets.EntitySpawn.read(new Buf(frame.body()));
|
||||
// the game will not draw a player it has never heard of, so the
|
||||
// name has to arrive before the player does
|
||||
RsxWorldBridge.playerList(spawn.name).processPacket(handler);
|
||||
RsxWorldBridge.spawnPlayer(spawn.entityId, spawn.name,
|
||||
spawn.x, spawn.y, spawn.z, spawn.yaw, spawn.pitch).processPacket(handler);
|
||||
return;
|
||||
}
|
||||
Packet<INetHandlerPlayClient> packet = RsxWorldBridge.translate(frame, this.mc.world);
|
||||
if (packet != null) {
|
||||
packet.processPacket(handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends one of ours.
|
||||
*
|
||||
* The socket is written from one place at a time, because a half written
|
||||
* frame is a stream the server cannot read.
|
||||
*/
|
||||
public void send(int packetId, net.rustnotmc.protocol.Packet packet) {
|
||||
OutputStream stream = this.out;
|
||||
if (stream == null || !this.open) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Frame frame = new Frame(packetId, packet.write().toByteArray());
|
||||
byte[] bytes = frame.encode().toByteArray();
|
||||
synchronized (this) {
|
||||
stream.write(bytes);
|
||||
stream.flush();
|
||||
}
|
||||
} catch (IOException gone) {
|
||||
lost("the connection was lost");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The game is sending something; the server does not speak the game's
|
||||
* language, so it is translated.
|
||||
*
|
||||
* Most of what the game sends has no counterpart on the server and is
|
||||
* dropped. The two that matter are where the player is and what the
|
||||
* player is breaking.
|
||||
*/
|
||||
@Override
|
||||
public void sendPacket(Packet<?> packet) {
|
||||
if (packet instanceof CPacketPlayer) {
|
||||
CPacketPlayer move = (CPacketPlayer) packet;
|
||||
send(Packets.PLAYER_MOVE, new Packets.PlayerMove(
|
||||
move.getX(0.0D), move.getY(0.0D), move.getZ(0.0D),
|
||||
move.getYaw(0.0F), move.getPitch(0.0F), move.isOnGround()));
|
||||
return;
|
||||
}
|
||||
if (packet instanceof CPacketPlayerDigging) {
|
||||
CPacketPlayerDigging dig = (CPacketPlayerDigging) packet;
|
||||
if (dig.getAction() == CPacketPlayerDigging.Action.START_DESTROY_BLOCK) {
|
||||
net.minecraft.util.math.BlockPos pos = dig.getPosition();
|
||||
send(Packets.BLOCK_CHANGE, new Packets.BlockChange(pos.getX(), pos.getY(),
|
||||
pos.getZ(), net.rustnotmc.protocol.Blocks.AIR));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// everything else the game sends has no place in our protocol yet
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- teardown
|
||||
|
||||
private void lost(String reason) {
|
||||
this.failure = reason == null ? "the connection was lost" : reason;
|
||||
this.open = false;
|
||||
Socket socket = this.socket;
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignored) {
|
||||
// it is going away anyway
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeChannel(ITextComponent reason) {
|
||||
if (this.disconnected) {
|
||||
return;
|
||||
}
|
||||
this.disconnected = true;
|
||||
this.exitMessage = reason;
|
||||
this.open = false;
|
||||
Socket socket = this.socket;
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignored) {
|
||||
// it is going away anyway
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDisconnection() {
|
||||
if (this.disconnected) {
|
||||
return;
|
||||
}
|
||||
this.disconnected = true;
|
||||
ITextComponent reason = this.exitMessage;
|
||||
if (reason == null) {
|
||||
reason = new TextComponentTranslation("multiplayer.disconnect.generic");
|
||||
}
|
||||
INetHandler handler = getNetHandler();
|
||||
if (handler instanceof INetHandlerPlayClient) {
|
||||
((INetHandlerPlayClient) handler).onDisconnect(reason);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChannelOpen() {
|
||||
return this.open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNoChannel() {
|
||||
return !this.open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getRemoteAddress() {
|
||||
return this.socket == null ? null : this.socket.getRemoteSocketAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITextComponent getExitMessage() {
|
||||
return this.exitMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConnectionState(EnumConnectionState state) {
|
||||
// the state machine belongs to the protocol, not to the game's
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package net.rustnotmc.client.net;
|
||||
|
||||
import io.netty.buffer.Unpooled;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.block.Block;
|
||||
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.SPacketChunkData;
|
||||
import net.minecraft.network.play.server.SPacketDestroyEntities;
|
||||
import net.minecraft.network.play.server.SPacketEntityTeleport;
|
||||
import net.minecraft.network.play.server.SPacketJoinGame;
|
||||
import net.minecraft.network.play.server.SPacketPlayerListItem;
|
||||
import net.minecraft.network.play.server.SPacketPlayerPosLook;
|
||||
import net.minecraft.network.play.server.SPacketSpawnPlayer;
|
||||
import net.minecraft.network.play.server.SPacketSpawnPosition;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.EnumDifficulty;
|
||||
import net.minecraft.world.GameType;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.WorldType;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
|
||||
import net.rustnotmc.protocol.ChunkFormat;
|
||||
import net.rustnotmc.protocol.Frame;
|
||||
import net.rustnotmc.protocol.Packets;
|
||||
|
||||
/**
|
||||
* Turns the server's packets into the game's own.
|
||||
*
|
||||
* The client already knows how to be a Minecraft client: it has a world, a
|
||||
* player, entities and a renderer, and all of that works. The only thing it
|
||||
* does not have is the server's packets. So instead of writing a second
|
||||
* renderer and a second world, the server's packets are translated into the
|
||||
* game's own and handed to the game, which does what it already knows how to
|
||||
* do.
|
||||
*
|
||||
* That is the whole reason the wire format is ours: it lets the client keep
|
||||
* the parts that are worth keeping.
|
||||
*/
|
||||
public final class RsxWorldBridge {
|
||||
|
||||
private RsxWorldBridge() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The packets that put a client into a world.
|
||||
*
|
||||
* Three of them, in this order: the world is created, the spawn point is
|
||||
* set, and the player is put on it. The player cannot be moved before the
|
||||
* world exists, because the player belongs to the world.
|
||||
*/
|
||||
public static List<Packet<INetHandlerPlayClient>> join(int entityId, int maxPlayers,
|
||||
double spawnX, double spawnY, double spawnZ) {
|
||||
List<Packet<INetHandlerPlayClient>> packets =
|
||||
new ArrayList<Packet<INetHandlerPlayClient>>();
|
||||
packets.add(new SPacketJoinGame(entityId, GameType.SURVIVAL, false, 0,
|
||||
EnumDifficulty.NORMAL, maxPlayers, WorldType.DEFAULT, false));
|
||||
packets.add(new SPacketSpawnPosition(
|
||||
new BlockPos((int) Math.floor(spawnX), (int) Math.floor(spawnY),
|
||||
(int) Math.floor(spawnZ))));
|
||||
packets.add(new SPacketPlayerPosLook(spawnX, spawnY, spawnZ, 0.0F, 0.0F,
|
||||
EnumSet.noneOf(SPacketPlayerPosLook.EnumFlags.class), 0));
|
||||
return packets;
|
||||
}
|
||||
|
||||
/**
|
||||
* One chunk, as the game wants it.
|
||||
*
|
||||
* The server sends a flat array of block ids; the game wants sections with
|
||||
* palettes, light and biome data. The chunk is built by hand and then
|
||||
* given to the game's own serialiser, which is the only way to be sure the
|
||||
* two agree about the format.
|
||||
*
|
||||
* The light is set to full. The server does not compute light yet, and a
|
||||
* world with no light in it is a world nobody can see.
|
||||
*/
|
||||
public static SPacketChunkData chunk(World world, int chunkX, int chunkZ, byte[] blocks) {
|
||||
Chunk chunk = new Chunk(world, chunkX, chunkZ);
|
||||
for (int y = 0; y < ChunkFormat.HEIGHT; y++) {
|
||||
for (int z = 0; z < ChunkFormat.WIDTH; z++) {
|
||||
for (int x = 0; x < ChunkFormat.WIDTH; x++) {
|
||||
byte id = blocks[ChunkFormat.index(x, y, z)];
|
||||
if (id == net.rustnotmc.protocol.Blocks.AIR) {
|
||||
continue;
|
||||
}
|
||||
chunk.setBlockState(new BlockPos(x, y, z), RsxBlockMap.stateFor(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExtendedBlockStorage[] storages = chunk.getBlockStorageArray();
|
||||
for (int i = 0; i < storages.length; i++) {
|
||||
ExtendedBlockStorage storage = storages[i];
|
||||
if (storage == Chunk.NULL_BLOCK_STORAGE) {
|
||||
continue;
|
||||
}
|
||||
java.util.Arrays.fill(storage.getBlockLight().getData(), (byte) 0xFF);
|
||||
if (storage.getSkyLight() != null) {
|
||||
java.util.Arrays.fill(storage.getSkyLight().getData(), (byte) 0xFF);
|
||||
}
|
||||
}
|
||||
return new SPacketChunkData(chunk, 65535);
|
||||
}
|
||||
|
||||
/** One block, changed. */
|
||||
public static SPacketBlockChange blockChange(int x, int y, int z, byte id) throws java.io.IOException {
|
||||
// the game's packet has no constructor that takes the values, so the
|
||||
// bytes are written the way the game writes them and read back by the
|
||||
// game's own reader. One format, one implementation of it.
|
||||
PacketBuffer buffer = new PacketBuffer(Unpooled.buffer());
|
||||
buffer.writeBlockPos(new BlockPos(x, y, z));
|
||||
buffer.writeVarInt(Block.BLOCK_STATE_IDS.get(RsxBlockMap.stateFor(id)));
|
||||
SPacketBlockChange packet = new SPacketBlockChange();
|
||||
packet.readPacketData(buffer);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/** Somebody else is here. */
|
||||
public static SPacketSpawnPlayer spawnPlayer(int entityId, String name,
|
||||
double x, double y, double z,
|
||||
float yaw, float pitch) throws java.io.IOException {
|
||||
// the game's packet has no constructor that takes the values, so the
|
||||
// bytes are written the way the game writes them and read back by the
|
||||
// game's own reader. One format, one implementation of it.
|
||||
PacketBuffer buffer = new PacketBuffer(Unpooled.buffer());
|
||||
buffer.writeVarInt(entityId);
|
||||
buffer.writeUniqueId(uuidFor(name));
|
||||
buffer.writeDouble(x);
|
||||
buffer.writeDouble(y);
|
||||
buffer.writeDouble(z);
|
||||
buffer.writeByte(angle(yaw));
|
||||
buffer.writeByte(angle(pitch));
|
||||
buffer.writeVarInt(0); // no entity data entries
|
||||
SPacketSpawnPlayer packet = new SPacketSpawnPlayer();
|
||||
packet.readPacketData(buffer);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/** Somebody else moved. */
|
||||
public static SPacketEntityTeleport entityMove(int entityId, double x, double y,
|
||||
double z, float yaw, float pitch)
|
||||
throws java.io.IOException {
|
||||
PacketBuffer buffer = new PacketBuffer(Unpooled.buffer());
|
||||
buffer.writeVarInt(entityId);
|
||||
buffer.writeDouble(x);
|
||||
buffer.writeDouble(y);
|
||||
buffer.writeDouble(z);
|
||||
buffer.writeByte(angle(yaw));
|
||||
buffer.writeByte(angle(pitch));
|
||||
buffer.writeBoolean(true);
|
||||
SPacketEntityTeleport packet = new SPacketEntityTeleport();
|
||||
packet.readPacketData(buffer);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/** Somebody else has left. */
|
||||
public static SPacketDestroyEntities entityRemove(int entityId) {
|
||||
return new SPacketDestroyEntities(new int[]{entityId});
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry that tells the game who a player is.
|
||||
*
|
||||
* The game looks a player up by their identity when one appears, and
|
||||
* refuses to draw a player it has never heard of. The server sends a name
|
||||
* and no identity, so the identity is derived the same way the game
|
||||
* derives it for an offline account.
|
||||
*/
|
||||
public static SPacketPlayerListItem playerList(String name) throws java.io.IOException {
|
||||
PacketBuffer buffer = new PacketBuffer(Unpooled.buffer());
|
||||
buffer.writeEnumValue(SPacketPlayerListItem.Action.ADD_PLAYER);
|
||||
buffer.writeVarInt(1);
|
||||
buffer.writeUniqueId(uuidFor(name));
|
||||
buffer.writeString(name);
|
||||
buffer.writeVarInt(0); // no profile properties
|
||||
buffer.writeVarInt(GameType.SURVIVAL.getID());
|
||||
buffer.writeVarInt(0); // latency
|
||||
buffer.writeBoolean(false); // no display name
|
||||
SPacketPlayerListItem packet = new SPacketPlayerListItem();
|
||||
packet.readPacketData(buffer);
|
||||
return packet;
|
||||
}
|
||||
|
||||
/** The identity an offline account gets. */
|
||||
public static UUID uuidFor(String name) {
|
||||
return UUID.nameUUIDFromBytes(
|
||||
("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/** An angle in the eighths of a turn the game uses. */
|
||||
private static byte angle(float degrees) {
|
||||
return (byte) (int) (degrees * 256.0F / 360.0F);
|
||||
}
|
||||
|
||||
/**
|
||||
* The game's packet for one of ours, or null when we have no counterpart.
|
||||
*
|
||||
* A packet we cannot translate is dropped rather than guessed at.
|
||||
*/
|
||||
public static Packet<INetHandlerPlayClient> translate(Frame frame, World world)
|
||||
throws java.io.IOException {
|
||||
switch (frame.packetId()) {
|
||||
case Packets.BLOCK_CHANGE: {
|
||||
Packets.BlockChange change =
|
||||
Packets.BlockChange.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
||||
return blockChange(change.x, change.y, change.z, change.blockId);
|
||||
}
|
||||
case Packets.ENTITY_MOVE: {
|
||||
Packets.EntityMove move =
|
||||
Packets.EntityMove.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
||||
return entityMove(move.entityId, move.x, move.y, move.z, move.yaw, move.pitch);
|
||||
}
|
||||
case Packets.ENTITY_REMOVE: {
|
||||
Packets.EntityRemove remove =
|
||||
Packets.EntityRemove.read(new net.rustnotmc.protocol.Buf(frame.body()));
|
||||
return entityRemove(remove.entityId);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,17 @@ def main():
|
||||
else:
|
||||
print('NOTE: %s missing, run scripts\\build.ps1' % loader_jar)
|
||||
|
||||
# the client speaks the server's protocol, not the vanilla one, so the
|
||||
# protocol and the account code have to be in the same JVM as the game
|
||||
workspace = os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__))))
|
||||
for name in ('rsx-protocol', 'rsx-auth'):
|
||||
jar = os.path.join(workspace, name, 'dist', name + '.jar')
|
||||
if os.path.isfile(jar):
|
||||
cp.append(jar)
|
||||
else:
|
||||
print('NOTE: %s missing, build %s first' % (jar, name))
|
||||
|
||||
with open(os.path.join(run, 'classpath.txt'), 'w', encoding='utf-8') as handle:
|
||||
handle.write(os.pathsep.join(cp))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user