477 lines
17 KiB
Java
477 lines
17 KiB
Java
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.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;
|
|
|
|
/** How long one tick may spend on incoming chunks, in nanoseconds. */
|
|
private static final long BUDGET_NANOS = 3000000L;
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* The work is limited to a slice of a tick. Walking into new ground brings
|
|
* a column of chunks at once, and doing all of them in one tick is what
|
|
* makes the world stutter. What does not fit waits for the next tick,
|
|
* which is a fifth of a second later and nobody can see the difference.
|
|
*/
|
|
@Override
|
|
public void processReceivedPackets() {
|
|
long deadline = System.nanoTime() + BUDGET_NANOS;
|
|
Frame frame;
|
|
while ((frame = this.inbox.peek()) != null) {
|
|
if (frame.packetId() == Packets.CHUNK_DATA && System.nanoTime() > deadline) {
|
|
// the chunks wait their turn; the rest of the queue waits with
|
|
// them, which is fine
|
|
return;
|
|
}
|
|
this.inbox.poll();
|
|
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;
|
|
case Packets.SET_GAME_MODE: {
|
|
Packets.GameMode gameMode = Packets.GameMode.read(new Buf(frame.body()));
|
|
RsxClientMode.set(gameMode.mode);
|
|
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;
|
|
}
|
|
RsxClientMode.set(world.gameMode);
|
|
List<Packet<INetHandlerPlayClient>> packets = RsxWorldBridge.join(world.entityId, 200,
|
|
world.spawnX, world.spawnY, world.spawnZ, world.gameMode);
|
|
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;
|
|
}
|
|
long started = System.nanoTime();
|
|
RsxWorldBridge.chunk(this.mc.world, chunk.chunkX, chunk.chunkZ, chunk.blocks)
|
|
.processPacket(handler);
|
|
long millis = (System.nanoTime() - started) / 1000000L;
|
|
if (millis > 4L) {
|
|
System.out.println("[rsx] chunk " + chunk.chunkX + "," + chunk.chunkZ
|
|
+ " took " + millis + " ms");
|
|
}
|
|
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.
|
|
*
|
|
* The dig packets are the one thing that is deliberately not translated.
|
|
* The game sends one for every frame the button is held, so a held button
|
|
* becomes a stream of block changes and a line of blocks disappears across
|
|
* the world instead of the one under the crosshair. The client breaks
|
|
* blocks on its own click instead, with the game's own raycast.
|
|
*/
|
|
@Override
|
|
public void sendPacket(Packet<?> packet) {
|
|
if (packet instanceof CPacketPlayer.Position
|
|
|| packet instanceof CPacketPlayer.Rotation
|
|
|| packet instanceof CPacketPlayer.PositionRotation) {
|
|
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;
|
|
}
|
|
// everything else the game sends has no place in our protocol yet
|
|
}
|
|
|
|
/**
|
|
* Asks the server to break a block.
|
|
*
|
|
* The server decides whether it happens: the player has to be close
|
|
* enough to it, and in a survival world the player does not break blocks
|
|
* at all.
|
|
*/
|
|
public void sendBlockChange(int x, int y, int z, byte id) {
|
|
send(Packets.BLOCK_CHANGE, new Packets.BlockChange(x, y, z, id));
|
|
}
|
|
|
|
// ------------------------------------------------------------- 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
|
|
}
|
|
}
|