Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-11-17 05:20:14 +01:00
Merge branch 'dev/1.1.0' into dev/2.0.0
Dieser Commit ist enthalten in:
Commit
2c07d00f18
@ -122,7 +122,7 @@ public enum ProtocolVersion {
|
||||
* @return the protocol version
|
||||
*/
|
||||
public int getProtocol() {
|
||||
return protocol;
|
||||
return protocol == -1 ? snapshotProtocol : protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -223,20 +223,21 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
*/
|
||||
public void closeWith(Object msg) {
|
||||
if (channel.isActive()) {
|
||||
boolean is1Point8 = this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0;
|
||||
boolean isLegacyOrPing = this.getState() == StateRegistry.HANDSHAKE
|
||||
|| this.getState() == StateRegistry.STATUS;
|
||||
if (channel.eventLoop().inEventLoop() && (is1Point8 || isLegacyOrPing)) {
|
||||
boolean is17 = this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17 && this.getState() != StateRegistry.STATUS) {
|
||||
channel.eventLoop().execute(() -> {
|
||||
// 1.7.x versions have a race condition with switching protocol states, so just explicitly
|
||||
// close the connection after a short while.
|
||||
this.setAutoReading(false);
|
||||
channel.eventLoop().schedule(() -> {
|
||||
knownDisconnect = true;
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
knownDisconnect = true;
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
} else {
|
||||
// 1.7.x versions have a race condition with switching protocol states, so just explicitly
|
||||
// close the connection after a short while.
|
||||
this.setAutoReading(false);
|
||||
channel.eventLoop().schedule(() -> {
|
||||
knownDisconnect = true;
|
||||
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import com.velocitypowered.proxy.connection.ConnectionTypes;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
|
||||
@ -69,19 +70,21 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
||||
MinecraftConnection smc = serverConn.ensureConnected();
|
||||
VelocityServerConnection existingConnection = serverConn.getPlayer().getConnectedServer();
|
||||
|
||||
final ConnectedPlayer player = serverConn.getPlayer();
|
||||
|
||||
if (existingConnection != null) {
|
||||
// Shut down the existing server connection.
|
||||
serverConn.getPlayer().setConnectedServer(null);
|
||||
player.setConnectedServer(null);
|
||||
existingConnection.disconnect();
|
||||
|
||||
// Send keep alive to try to avoid timeouts
|
||||
serverConn.getPlayer().sendKeepAlive();
|
||||
player.sendKeepAlive();
|
||||
}
|
||||
|
||||
// The goods are in hand! We got JoinGame. Let's transition completely to the new state.
|
||||
smc.setAutoReading(false);
|
||||
server.getEventManager()
|
||||
.fire(new ServerConnectedEvent(serverConn.getPlayer(), serverConn.getServer(),
|
||||
.fire(new ServerConnectedEvent(player, serverConn.getServer(),
|
||||
existingConnection != null ? existingConnection.getServer() : null))
|
||||
.whenCompleteAsync((x, error) -> {
|
||||
// Make sure we can still transition (player might have disconnected here).
|
||||
@ -93,17 +96,15 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
// Change the client to use the ClientPlaySessionHandler if required.
|
||||
ClientPlaySessionHandler playHandler;
|
||||
if (serverConn.getPlayer().getConnection().getSessionHandler()
|
||||
instanceof ClientPlaySessionHandler) {
|
||||
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getConnection()
|
||||
.getSessionHandler();
|
||||
if (player.getConnection().getSessionHandler() instanceof ClientPlaySessionHandler) {
|
||||
playHandler = (ClientPlaySessionHandler) player.getConnection().getSessionHandler();
|
||||
} else {
|
||||
playHandler = new ClientPlaySessionHandler(server, serverConn.getPlayer());
|
||||
serverConn.getPlayer().getConnection().setSessionHandler(playHandler);
|
||||
playHandler = new ClientPlaySessionHandler(server, player);
|
||||
player.getConnection().setSessionHandler(playHandler);
|
||||
}
|
||||
playHandler.handleBackendJoinGame(packet, serverConn);
|
||||
|
||||
// Strap on the correct session handler for the server. We will have nothing more to do
|
||||
// Set the new play session handler for the server. We will have nothing more to do
|
||||
// with this connection once this task finishes up.
|
||||
smc.setSessionHandler(new BackendPlaySessionHandler(server, serverConn));
|
||||
|
||||
@ -114,15 +115,15 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
||||
serverConn.getPlayer().setConnectedServer(serverConn);
|
||||
|
||||
// We're done! :)
|
||||
server.getEventManager().fireAndForget(new ServerPostConnectEvent(serverConn.getPlayer(),
|
||||
server.getEventManager().fireAndForget(new ServerPostConnectEvent(player,
|
||||
existingConnection == null ? null : existingConnection.getServer()));
|
||||
resultFuture.complete(ConnectionRequestResults.successful(serverConn.getServer()));
|
||||
}, smc.eventLoop())
|
||||
.exceptionally(exc -> {
|
||||
logger.error("Unable to switch to new server {} for {}",
|
||||
serverConn.getServerInfo().getName(),
|
||||
serverConn.getPlayer().getUsername(), exc);
|
||||
serverConn.getPlayer().disconnect(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
|
||||
player.getUsername(), exc);
|
||||
player.disconnect(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
|
||||
resultFuture.completeExceptionally(exc);
|
||||
return null;
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.backend.BackendConnectionPhases;
|
||||
@ -333,30 +334,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
} else {
|
||||
// Clear tab list to avoid duplicate entries
|
||||
player.getTabList().clearAll();
|
||||
|
||||
// In order to handle switching to another server, you will need to send two packets:
|
||||
//
|
||||
// - The join game packet from the backend server, with a different dimension
|
||||
// - A respawn with the correct dimension
|
||||
//
|
||||
// Most notably, by having the client accept the join game packet, we can work around the need
|
||||
// to perform entity ID rewrites, eliminating potential issues from rewriting packets and
|
||||
// improving compatibility with mods.
|
||||
|
||||
int sentOldDim = joinGame.getDimension();
|
||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_16) < 0) {
|
||||
// Before Minecraft 1.16, we could not switch to the same dimension without sending an
|
||||
// additional respawn. On older versions of Minecraft this forces the client to perform
|
||||
// garbage collection which adds additional latency.
|
||||
joinGame.setDimension(joinGame.getDimension() == 0 ? -1 : 0);
|
||||
if (player.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
||||
this.doSafeClientServerSwitch(joinGame);
|
||||
} else {
|
||||
this.doFastClientServerSwitch(joinGame);
|
||||
}
|
||||
player.getConnection().delayedWrite(joinGame);
|
||||
|
||||
player.getConnection().delayedWrite(
|
||||
new Respawn(sentOldDim, joinGame.getPartialHashedSeed(),
|
||||
joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(),
|
||||
false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(),
|
||||
joinGame.getCurrentDimensionData()));
|
||||
destination.setActiveDimensionRegistry(joinGame.getDimensionRegistry()); // 1.16
|
||||
}
|
||||
|
||||
@ -394,6 +376,55 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
destination.completeJoin();
|
||||
}
|
||||
|
||||
private void doFastClientServerSwitch(JoinGame joinGame) {
|
||||
// In order to handle switching to another server, you will need to send two packets:
|
||||
//
|
||||
// - The join game packet from the backend server, with a different dimension
|
||||
// - A respawn with the correct dimension
|
||||
//
|
||||
// Most notably, by having the client accept the join game packet, we can work around the need
|
||||
// to perform entity ID rewrites, eliminating potential issues from rewriting packets and
|
||||
// improving compatibility with mods.
|
||||
int sentOldDim = joinGame.getDimension();
|
||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_16) < 0) {
|
||||
// Before Minecraft 1.16, we could not switch to the same dimension without sending an
|
||||
// additional respawn. On older versions of Minecraft this forces the client to perform
|
||||
// garbage collection which adds additional latency.
|
||||
joinGame.setDimension(joinGame.getDimension() == 0 ? -1 : 0);
|
||||
}
|
||||
player.getConnection().delayedWrite(joinGame);
|
||||
|
||||
player.getConnection().delayedWrite(
|
||||
new Respawn(sentOldDim, joinGame.getPartialHashedSeed(),
|
||||
joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(),
|
||||
false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(),
|
||||
joinGame.getCurrentDimensionData()));
|
||||
}
|
||||
|
||||
private void doSafeClientServerSwitch(JoinGame joinGame) {
|
||||
// Some clients do not behave well with the "fast" respawn sequence. In this case we will use
|
||||
// a "safe" respawn sequence that involves sending three packets to the client. They have the
|
||||
// same effect but tend to work better with buggier clients (Forge 1.8 in particular).
|
||||
|
||||
// Send the JoinGame packet itself, unmodified.
|
||||
player.getConnection().delayedWrite(joinGame);
|
||||
|
||||
// Send a respawn packet in a different dimension.
|
||||
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
|
||||
player.getConnection().delayedWrite(
|
||||
new Respawn(tempDim, joinGame.getPartialHashedSeed(), joinGame.getDifficulty(),
|
||||
joinGame.getGamemode(), joinGame.getLevelType(),
|
||||
false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(),
|
||||
joinGame.getCurrentDimensionData()));
|
||||
|
||||
// Now send a respawn packet in the correct dimension.
|
||||
player.getConnection().delayedWrite(
|
||||
new Respawn(joinGame.getDimension(), joinGame.getPartialHashedSeed(),
|
||||
joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(),
|
||||
false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(),
|
||||
joinGame.getCurrentDimensionData()));
|
||||
}
|
||||
|
||||
public List<UUID> getServerBossBars() {
|
||||
return serverBossBars;
|
||||
}
|
||||
|
@ -208,7 +208,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
onlineMode);
|
||||
final GameProfile finalProfile = profile;
|
||||
|
||||
server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
|
||||
server.getEventManager().fire(profileRequestEvent).thenComposeAsync(profileEvent -> {
|
||||
if (mcConnection.isClosed()) {
|
||||
// The player disconnected after we authenticated them.
|
||||
return CompletableFuture.completedFuture(null);
|
||||
@ -234,7 +234,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
completeLoginProtocolPhaseAndInitialize(player);
|
||||
}
|
||||
}, mcConnection.eventLoop());
|
||||
}).exceptionally((ex) -> {
|
||||
}, mcConnection.eventLoop()).exceptionally((ex) -> {
|
||||
logger.error("Exception during connection of {}", finalProfile, ex);
|
||||
return null;
|
||||
});
|
||||
|
@ -23,7 +23,7 @@ public class LegacyPingDecoder extends ByteToMessageDecoder {
|
||||
}
|
||||
|
||||
if (!ctx.channel().isActive()) {
|
||||
in.skipBytes(in.readableBytes());
|
||||
in.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
package com.velocitypowered.proxy.protocol.netty;
|
||||
|
||||
import com.velocitypowered.proxy.protocol.netty.VarintByteDecoder.DecodeResult;
|
||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.util.ByteProcessor;
|
||||
import java.util.List;
|
||||
|
||||
public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
@ -13,16 +13,15 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
new QuietDecoderException("Bad packet length");
|
||||
private static final QuietDecoderException VARINT_BIG_CACHED =
|
||||
new QuietDecoderException("VarInt too big");
|
||||
private final VarintByteDecoder reader = new VarintByteDecoder();
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
if (!ctx.channel().isActive()) {
|
||||
in.skipBytes(in.readableBytes());
|
||||
in.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
reader.reset();
|
||||
final VarintByteDecoder reader = new VarintByteDecoder();
|
||||
|
||||
int varintEnd = in.forEachByte(reader);
|
||||
if (varintEnd == -1) {
|
||||
@ -31,53 +30,23 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reader.result == DecodeResult.SUCCESS) {
|
||||
if (reader.readVarint < 0) {
|
||||
if (reader.getResult() == DecodeResult.SUCCESS) {
|
||||
int readVarint = reader.getReadVarint();
|
||||
int bytesRead = reader.getBytesRead();
|
||||
if (readVarint < 0) {
|
||||
throw BAD_LENGTH_CACHED;
|
||||
} else if (reader.readVarint == 0) {
|
||||
} else if (readVarint == 0) {
|
||||
// skip over the empty packet and ignore it
|
||||
in.readerIndex(varintEnd + 1);
|
||||
} else {
|
||||
int minimumRead = reader.bytesRead + reader.readVarint;
|
||||
int minimumRead = bytesRead + readVarint;
|
||||
if (in.isReadable(minimumRead)) {
|
||||
out.add(in.retainedSlice(varintEnd + 1, reader.readVarint));
|
||||
out.add(in.retainedSlice(varintEnd + 1, readVarint));
|
||||
in.skipBytes(minimumRead);
|
||||
}
|
||||
}
|
||||
} else if (reader.result == DecodeResult.TOO_BIG) {
|
||||
} else if (reader.getResult() == DecodeResult.TOO_BIG) {
|
||||
throw VARINT_BIG_CACHED;
|
||||
}
|
||||
}
|
||||
|
||||
private static class VarintByteDecoder implements ByteProcessor {
|
||||
private int readVarint;
|
||||
private int bytesRead;
|
||||
private DecodeResult result = DecodeResult.TOO_SHORT;
|
||||
|
||||
@Override
|
||||
public boolean process(byte k) {
|
||||
readVarint |= (k & 0x7F) << bytesRead++ * 7;
|
||||
if (bytesRead > 3) {
|
||||
result = DecodeResult.TOO_BIG;
|
||||
return false;
|
||||
}
|
||||
if ((k & 0x80) != 128) {
|
||||
result = DecodeResult.SUCCESS;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
readVarint = 0;
|
||||
bytesRead = 0;
|
||||
result = DecodeResult.TOO_SHORT;
|
||||
}
|
||||
}
|
||||
|
||||
private enum DecodeResult {
|
||||
SUCCESS,
|
||||
TOO_SHORT,
|
||||
TOO_BIG
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
package com.velocitypowered.proxy.protocol.netty;
|
||||
|
||||
import io.netty.util.ByteProcessor;
|
||||
|
||||
class VarintByteDecoder implements ByteProcessor {
|
||||
|
||||
private int readVarint;
|
||||
private int bytesRead;
|
||||
private DecodeResult result = DecodeResult.TOO_SHORT;
|
||||
|
||||
@Override
|
||||
public boolean process(byte k) {
|
||||
readVarint |= (k & 0x7F) << bytesRead++ * 7;
|
||||
if (bytesRead > 3) {
|
||||
result = DecodeResult.TOO_BIG;
|
||||
return false;
|
||||
}
|
||||
if ((k & 0x80) != 128) {
|
||||
result = DecodeResult.SUCCESS;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getReadVarint() {
|
||||
return readVarint;
|
||||
}
|
||||
|
||||
public int getBytesRead() {
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
public DecodeResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public enum DecodeResult {
|
||||
SUCCESS,
|
||||
TOO_SHORT,
|
||||
TOO_BIG
|
||||
}
|
||||
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren