Files
RustNotMC/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java
T
sasheg dev 7505ddd84e Броня в семи местах и три зоны урона
- В реестр заведены 46 предметов одежды ремейка: слот, слой, какие части тела
  закрывает и сколько снимает из каждого вида вреда. Данные выгружены в items.csv
  (extract-items.py), в коде у каждого предмета armor(...).
- Семь мест: голова, лицо, грудь (2 слоя), ноги (2 слоя), руки, стопы, полный
  костюм. Пояс не делали - в ремейке его нет, есть только быстрая панель.
- Inventory сервера: семь мест по два слоя, вещи надеваются и снимаются.
- Damage: зона урона по точке попадания (голова верхние 0.4, ноги нижние 0.4),
  множители голова 2, тело 1, ноги 0.75; одежда снимает вред только там, где
  закрывает.
- Пакет Attack (цель, точка попадания, рука) и клиентский патч, который его
  шлёт. У игрока появилась жизнь.
- В items.csv добавлены колонки armor_slot, armor_layer, armor_zones.

Тесты: протокол 24 + 10, сервер 85. Форк собирается чисто, 3279 классов.
2026-10-21 22:43:52 +03:00

521 lines
19 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;
}
case Packets.HITBOXES: {
Packets.Hitboxes boxes = Packets.Hitboxes.read(new Buf(frame.body()));
net.rustnotmc.client.RsxHitboxDebug.accept(boxes);
return;
}
case Packets.HITBOX_INSTANCES: {
Packets.HitboxInstances instances =
Packets.HitboxInstances.read(new Buf(frame.body()));
net.rustnotmc.client.RsxHitboxDebug.accept(instances);
return;
}
default:
onPlay(frame);
}
}
/**
* Tells the server where a hit landed.
*
* The server counts the damage, and it needs to know where the hit
* landed: a hit on the head is worth twice a hit on the body.
*/
public static void reportAttack(Minecraft mc, net.minecraft.entity.Entity target) {
RsxNetworkManager manager = active();
if (manager == null || mc == null || target == null) {
return;
}
if (mc.objectMouseOver == null
|| mc.objectMouseOver.typeOfHit != net.minecraft.util.math.RayTraceResult.Type.ENTITY) {
return;
}
net.minecraft.util.math.Vec3d hit = mc.objectMouseOver.hitVec;
manager.send(Packets.ATTACK, new Packets.Attack(target.getEntityId(),
(float) hit.x, (float) hit.y, (float) hit.z, Packets.Attack.HAND_MAIN));
}
/**
* Asks the server to read its box file again.
*
* The file is the editor, so after a change the client wants the new
* numbers without the server having to be restarted.
*/
public static void sendHitboxesReload() {
RsxNetworkManager manager = active();
if (manager != null) {
manager.send(Packets.HITBOXES_RELOAD, new Packets.HitboxesReload());
}
}
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
}
}