mirror of
https://github.com/ViaVersion/ViaVersion.git
synced 2024-11-02 08:39:59 +01:00
Merge pull request #1723 from creeper123123321/backendinjection
Velocity Backend injection / Velocity 1.1.0
This commit is contained in:
commit
9821cb1b16
@ -61,7 +61,7 @@ public class BukkitEncodeHandler extends MessageToByteEncoder implements ViaHand
|
||||
|
||||
@Override
|
||||
public void transform(ByteBuf bytebuf) throws Exception {
|
||||
info.checkOutgoingPacket();
|
||||
if (!info.checkOutgoingPacket()) throw CancelEncoderException.generate(null);
|
||||
if (!info.shouldTransformPacket()) return;
|
||||
info.transformOutgoing(bytebuf, CancelEncoderException::generate);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ public class BungeeEncodeHandler extends MessageToMessageEncoder<ByteBuf> {
|
||||
|
||||
@Override
|
||||
protected void encode(final ChannelHandlerContext ctx, ByteBuf bytebuf, List<Object> out) throws Exception {
|
||||
info.checkOutgoingPacket();
|
||||
if (!info.checkOutgoingPacket()) throw CancelEncoderException.generate(null);
|
||||
if (!info.shouldTransformPacket()) {
|
||||
out.add(bytebuf.retain());
|
||||
return;
|
||||
|
@ -12,14 +12,6 @@
|
||||
|
||||
<artifactId>viaversion-common</artifactId>
|
||||
<dependencies>
|
||||
<!-- Snake YAML -->
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.18</version>
|
||||
<scope>compile</scope> <!-- Velocity doesn't have snakeyaml -->
|
||||
</dependency>
|
||||
|
||||
<!-- FastUtil for performance increases -->
|
||||
<dependency>
|
||||
<groupId>it.unimi.dsi</groupId>
|
||||
|
@ -91,7 +91,7 @@ public class ViaManager {
|
||||
// Check if there are any pipes to this version
|
||||
if (ProtocolRegistry.SERVER_PROTOCOL != -1) {
|
||||
platform.getLogger().info("ViaVersion detected server version: " + ProtocolVersion.getProtocol(ProtocolRegistry.SERVER_PROTOCOL));
|
||||
if (!ProtocolRegistry.isWorkingPipe()) {
|
||||
if (!ProtocolRegistry.isWorkingPipe() && !platform.isProxy()) {
|
||||
platform.getLogger().warning("ViaVersion does not have any compatible versions for this server version!");
|
||||
platform.getLogger().warning("Please remember that ViaVersion only adds support for versions newer than the server version.");
|
||||
platform.getLogger().warning("If you need support for older versions you may need to use one or more ViaVersion addons too.");
|
||||
|
@ -3,7 +3,6 @@ package us.myles.ViaVersion.api.data;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -18,7 +17,6 @@ import us.myles.ViaVersion.protocols.base.ProtocolInfo;
|
||||
import us.myles.ViaVersion.util.PipelineUtil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
@ -27,8 +25,9 @@ public class UserConnection {
|
||||
private static final AtomicLong IDS = new AtomicLong();
|
||||
private final long id = IDS.incrementAndGet();
|
||||
private final Channel channel;
|
||||
private ProtocolInfo protocolInfo;
|
||||
private final boolean clientSide;
|
||||
Map<Class, StoredObject> storedObjects = new ConcurrentHashMap<>();
|
||||
private ProtocolInfo protocolInfo;
|
||||
private boolean active = true;
|
||||
private boolean pendingDisconnect;
|
||||
private Object lastPacket;
|
||||
@ -42,8 +41,23 @@ public class UserConnection {
|
||||
private int secondsObserved;
|
||||
private int warnings;
|
||||
|
||||
public UserConnection(@Nullable Channel channel) {
|
||||
/**
|
||||
* Creates an UserConnection. When it's a client-side connection, some method behaviors are modified.
|
||||
*
|
||||
* @param channel netty channel.
|
||||
* @param clientSide true if it's a client-side connection
|
||||
*/
|
||||
public UserConnection(@Nullable Channel channel, boolean clientSide) {
|
||||
this.channel = channel;
|
||||
this.clientSide = clientSide;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param channel
|
||||
* @see #UserConnection(Channel, boolean)
|
||||
*/
|
||||
public UserConnection(@Nullable Channel channel) {
|
||||
this(channel, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,12 +105,24 @@ public class UserConnection {
|
||||
* @param packet The raw packet to send
|
||||
* @param currentThread Should it run in the same thread
|
||||
*/
|
||||
public void sendRawPacket(ByteBuf packet, boolean currentThread) {
|
||||
ChannelHandler handler = channel.pipeline().get(Via.getManager().getInjector().getEncoderName());
|
||||
if (currentThread) {
|
||||
channel.pipeline().context(handler).writeAndFlush(packet);
|
||||
public void sendRawPacket(final ByteBuf packet, boolean currentThread) {
|
||||
Runnable act;
|
||||
if (clientSide) {
|
||||
// We'll just assume that Via decoder isn't wrapping the original decoder
|
||||
act = () -> getChannel().pipeline()
|
||||
.context(Via.getManager().getInjector().getDecoderName()).fireChannelRead(packet);
|
||||
} else {
|
||||
channel.eventLoop().submit(() -> channel.pipeline().context(handler).writeAndFlush(packet));
|
||||
act = () -> channel.pipeline().context(Via.getManager().getInjector().getEncoderName()).writeAndFlush(packet);
|
||||
}
|
||||
if (currentThread) {
|
||||
act.run();
|
||||
} else {
|
||||
try {
|
||||
channel.eventLoop().submit(act);
|
||||
} catch (Throwable e) {
|
||||
packet.release(); // Couldn't schedule
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,9 +132,22 @@ public class UserConnection {
|
||||
* @param packet The raw packet to send
|
||||
* @return ChannelFuture of the packet being sent
|
||||
*/
|
||||
public ChannelFuture sendRawPacketFuture(ByteBuf packet) {
|
||||
ChannelHandler handler = channel.pipeline().get(Via.getManager().getInjector().getEncoderName());
|
||||
return channel.pipeline().context(handler).writeAndFlush(packet);
|
||||
public ChannelFuture sendRawPacketFuture(final ByteBuf packet) {
|
||||
if (clientSide) {
|
||||
return sendRawPacketFutureClientSide(packet);
|
||||
} else {
|
||||
return sendRawPacketFutureServerSide(packet);
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelFuture sendRawPacketFutureServerSide(final ByteBuf packet) {
|
||||
return channel.pipeline().context(Via.getManager().getInjector().getEncoderName()).writeAndFlush(packet);
|
||||
}
|
||||
|
||||
private ChannelFuture sendRawPacketFutureClientSide(final ByteBuf packet) {
|
||||
// Assume that decoder isn't wrapping
|
||||
getChannel().pipeline().context(Via.getManager().getInjector().getDecoderName()).fireChannelRead(packet);
|
||||
return getChannel().newSucceededFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,6 +195,7 @@ public class UserConnection {
|
||||
* @see #incrementReceived()
|
||||
*/
|
||||
public boolean exceedsMaxPPS() {
|
||||
if (clientSide) return false; // Don't apply PPS limiting for client-side
|
||||
ViaVersionConfig conf = Via.getConfig();
|
||||
// Max PPS Checker
|
||||
if (conf.getMaxPPS() > 0) {
|
||||
@ -195,14 +235,8 @@ public class UserConnection {
|
||||
if (!channel.isOpen() || pendingDisconnect) return;
|
||||
|
||||
pendingDisconnect = true;
|
||||
UUID uuid = protocolInfo.getUuid();
|
||||
if (uuid == null) {
|
||||
channel.close(); // Just disconnect, we don't know what the connection is
|
||||
return;
|
||||
}
|
||||
|
||||
Via.getPlatform().runSync(() -> {
|
||||
if (!Via.getPlatform().kickPlayer(uuid, ChatColor.translateAlternateColorCodes('&', reason))) {
|
||||
if (!Via.getPlatform().disconnect(this, ChatColor.translateAlternateColorCodes('&', reason))) {
|
||||
channel.close(); // =)
|
||||
}
|
||||
});
|
||||
@ -214,9 +248,20 @@ public class UserConnection {
|
||||
* @param packet Raw packet to be sent
|
||||
* @param currentThread If {@code true} executes immediately, {@code false} submits a task to EventLoop
|
||||
*/
|
||||
public void sendRawPacketToServer(ByteBuf packet, boolean currentThread) {
|
||||
ByteBuf buf = packet.alloc().buffer();
|
||||
public void sendRawPacketToServer(final ByteBuf packet, boolean currentThread) {
|
||||
if (clientSide) {
|
||||
sendRawPacketToServerClientSide(packet, currentThread);
|
||||
} else {
|
||||
sendRawPacketToServerServerSide(packet, currentThread);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRawPacketToServerServerSide(final ByteBuf packet, boolean currentThread) {
|
||||
final ByteBuf buf = packet.alloc().buffer();
|
||||
try {
|
||||
// We'll use passing through because there are some encoder wrappers
|
||||
ChannelHandlerContext context = PipelineUtil
|
||||
.getPreviousContext(Via.getManager().getInjector().getDecoderName(), channel.pipeline());
|
||||
try {
|
||||
Type.VAR_INT.writePrimitive(buf, PacketWrapper.PASSTHROUGH_ID);
|
||||
} catch (Exception e) {
|
||||
@ -224,23 +269,18 @@ public class UserConnection {
|
||||
Via.getPlatform().getLogger().warning("Type.VAR_INT.write thrown an exception: " + e);
|
||||
}
|
||||
buf.writeBytes(packet);
|
||||
ChannelHandlerContext context = PipelineUtil
|
||||
.getPreviousContext(Via.getManager().getInjector().getDecoderName(), channel.pipeline());
|
||||
if (currentThread) {
|
||||
Runnable act = () -> {
|
||||
if (context != null) {
|
||||
context.fireChannelRead(buf);
|
||||
} else {
|
||||
channel.pipeline().fireChannelRead(buf);
|
||||
}
|
||||
};
|
||||
if (currentThread) {
|
||||
act.run();
|
||||
} else {
|
||||
try {
|
||||
channel.eventLoop().submit(() -> {
|
||||
if (context != null) {
|
||||
context.fireChannelRead(buf);
|
||||
} else {
|
||||
channel.pipeline().fireChannelRead(buf);
|
||||
}
|
||||
});
|
||||
channel.eventLoop().submit(act);
|
||||
} catch (Throwable t) {
|
||||
// Couldn't schedule
|
||||
buf.release();
|
||||
@ -252,6 +292,21 @@ public class UserConnection {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRawPacketToServerClientSide(final ByteBuf packet, boolean currentThread) {
|
||||
Runnable act = () -> getChannel().pipeline()
|
||||
.context(Via.getManager().getInjector().getEncoderName()).writeAndFlush(packet);
|
||||
if (currentThread) {
|
||||
act.run();
|
||||
} else {
|
||||
try {
|
||||
getChannel().eventLoop().submit(act);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
packet.release(); // Couldn't schedule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a raw packet to the server. It will submit a task to EventLoop.
|
||||
*
|
||||
@ -262,11 +317,24 @@ public class UserConnection {
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors serverbound packets.
|
||||
* Monitors incoming packets
|
||||
*
|
||||
* @return false if this packet should be cancelled
|
||||
*/
|
||||
public boolean checkIncomingPacket() {
|
||||
if (clientSide) {
|
||||
return checkClientBound();
|
||||
} else {
|
||||
return checkServerBound();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkClientBound() {
|
||||
incrementSent();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean checkServerBound() {
|
||||
// Ignore if pending disconnect
|
||||
if (pendingDisconnect) return false;
|
||||
// Increment received + Check PPS
|
||||
@ -274,10 +342,16 @@ public class UserConnection {
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors clientbound packets.
|
||||
* Monitors outgoing packets
|
||||
*
|
||||
* @return false if this packet should be cancelled
|
||||
*/
|
||||
public void checkOutgoingPacket() {
|
||||
incrementSent();
|
||||
public boolean checkOutgoingPacket() {
|
||||
if (clientSide) {
|
||||
return checkServerBound();
|
||||
} else {
|
||||
return checkClientBound();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -290,7 +364,8 @@ public class UserConnection {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the clientbound packet contained in an outgoing ByteBuf.
|
||||
* Transforms the outgoing packet contained in ByteBuf. When clientSide is true, this packet is considered
|
||||
* serverbound.
|
||||
*
|
||||
* @param buf ByteBuf with packet id and packet contents
|
||||
* @param cancelSupplier Function called with original CancelException for generating the Exception used when
|
||||
@ -301,11 +376,12 @@ public class UserConnection {
|
||||
*/
|
||||
public void transformOutgoing(ByteBuf buf, Function<Throwable, Exception> cancelSupplier) throws Exception {
|
||||
if (!buf.isReadable()) return;
|
||||
transform(buf, Direction.OUTGOING, cancelSupplier);
|
||||
transform(buf, clientSide ? Direction.INCOMING : Direction.OUTGOING, cancelSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the serverbound packet contained in an incoming ByteBuf.
|
||||
* Transforms the incoming packet contained in ByteBuf. When clientSide is true, this packet is considered
|
||||
* clientbound
|
||||
*
|
||||
* @param buf ByteBuf with packet id and packet contents
|
||||
* @param cancelSupplier Function called with original CancelException for generating the Exception used when
|
||||
@ -316,7 +392,7 @@ public class UserConnection {
|
||||
*/
|
||||
public void transformIncoming(ByteBuf buf, Function<Throwable, Exception> cancelSupplier) throws Exception {
|
||||
if (!buf.isReadable()) return;
|
||||
transform(buf, Direction.INCOMING, cancelSupplier);
|
||||
transform(buf, clientSide ? Direction.OUTGOING : Direction.INCOMING, cancelSupplier);
|
||||
}
|
||||
|
||||
private void transform(ByteBuf buf, Direction direction, Function<Throwable, Exception> cancelSupplier) throws Exception {
|
||||
@ -459,4 +535,12 @@ public class UserConnection {
|
||||
public int hashCode() {
|
||||
return Long.hashCode(id);
|
||||
}
|
||||
|
||||
public boolean isClientSide() {
|
||||
return clientSide;
|
||||
}
|
||||
|
||||
public boolean shouldApplyBlockProtocol() {
|
||||
return !clientSide; // Don't apply protocol blocking on client-side
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ public class ViaConnectionManager {
|
||||
* UUIDs can't be duplicate between frontend connections.
|
||||
*/
|
||||
public boolean isFrontEnd(UserConnection conn) {
|
||||
return true;
|
||||
return !conn.isClientSide();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,10 +1,13 @@
|
||||
package us.myles.ViaVersion.api.platform;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import us.myles.ViaVersion.api.Via;
|
||||
import us.myles.ViaVersion.api.ViaAPI;
|
||||
import us.myles.ViaVersion.api.ViaVersionConfig;
|
||||
import us.myles.ViaVersion.api.command.ViaCommandSender;
|
||||
import us.myles.ViaVersion.api.configuration.ConfigurationProvider;
|
||||
import us.myles.ViaVersion.api.data.UserConnection;
|
||||
import us.myles.ViaVersion.protocols.base.ProtocolInfo;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.UUID;
|
||||
@ -121,6 +124,20 @@ public interface ViaPlatform<T> {
|
||||
*/
|
||||
boolean kickPlayer(UUID uuid, String message);
|
||||
|
||||
/**
|
||||
* Disconnects an UserConnection for a reason
|
||||
*
|
||||
* @param connection The UserConnection
|
||||
* @param message The message to kick them with
|
||||
* @return True if it was successful
|
||||
*/
|
||||
default boolean disconnect(UserConnection connection, String message) {
|
||||
if (connection.isClientSide()) return false;
|
||||
UUID uuid = connection.get(ProtocolInfo.class).getUuid();
|
||||
if (uuid == null) return false;
|
||||
return kickPlayer(uuid, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the plugin is enabled.
|
||||
*
|
||||
|
@ -165,6 +165,7 @@ public class BaseProtocol1_7 extends SimpleProtocol {
|
||||
int protocol = wrapper.user().getProtocolInfo().getProtocolVersion();
|
||||
if (Via.getConfig().getBlockedProtocols().contains(protocol)) {
|
||||
if (!wrapper.user().getChannel().isOpen()) return;
|
||||
if (!wrapper.user().shouldApplyBlockProtocol()) return;
|
||||
|
||||
PacketWrapper disconnectPacket = new PacketWrapper(0x00, null, wrapper.user()); // Disconnect Packet
|
||||
Protocol1_9To1_8.FIX_JSON.write(disconnectPacket, ChatColor.translateAlternateColorCodes('&', Via.getConfig().getBlockedDisconnectMsg()));
|
||||
|
@ -123,10 +123,6 @@
|
||||
<pattern>javassist</pattern>
|
||||
<shadedPattern>us.myles.viaversion.libs.javassist</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>org.yaml.snakeyaml</pattern>
|
||||
<shadedPattern>us.myles.viaversion.libs.snakeyaml</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>net.md_5.bungee</pattern>
|
||||
<shadedPattern>us.myles.viaversion.libs.bungeecordchat</shadedPattern>
|
||||
|
14
pom.xml
14
pom.xml
@ -70,6 +70,12 @@
|
||||
<id>viaversion-repo</id>
|
||||
<url>https://repo.viaversion.com</url>
|
||||
</repository>
|
||||
|
||||
<!-- Mojang's Minecraft Libraries -->
|
||||
<repository>
|
||||
<id>mojang</id>
|
||||
<url>https://libraries.minecraft.net</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
@ -141,6 +147,14 @@
|
||||
<version>5.3.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Snake YAML -->
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>1.18</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -42,7 +42,7 @@ public class SpongeEncodeHandler extends MessageToByteEncoder<Object> implements
|
||||
|
||||
@Override
|
||||
public void transform(ByteBuf bytebuf) throws Exception {
|
||||
info.checkOutgoingPacket();
|
||||
if (!info.checkOutgoingPacket()) throw CancelEncoderException.generate(null);
|
||||
if (!info.shouldTransformPacket()) return;
|
||||
info.transformOutgoing(bytebuf, CancelEncoderException::generate);
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
<dependency>
|
||||
<groupId>com.velocitypowered</groupId>
|
||||
<artifactId>velocity-api</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
@ -26,11 +26,7 @@ import us.myles.ViaVersion.util.GsonUtil;
|
||||
import us.myles.ViaVersion.velocity.VersionInfo;
|
||||
import us.myles.ViaVersion.velocity.command.VelocityCommandHandler;
|
||||
import us.myles.ViaVersion.velocity.command.VelocityCommandSender;
|
||||
import us.myles.ViaVersion.velocity.platform.VelocityTaskId;
|
||||
import us.myles.ViaVersion.velocity.platform.VelocityViaAPI;
|
||||
import us.myles.ViaVersion.velocity.platform.VelocityViaConfig;
|
||||
import us.myles.ViaVersion.velocity.platform.VelocityViaInjector;
|
||||
import us.myles.ViaVersion.velocity.platform.VelocityViaLoader;
|
||||
import us.myles.ViaVersion.velocity.platform.*;
|
||||
import us.myles.ViaVersion.velocity.service.ProtocolDetectorService;
|
||||
import us.myles.ViaVersion.velocity.util.LoggerWrapper;
|
||||
|
||||
|
@ -7,12 +7,14 @@ import us.myles.ViaVersion.api.protocol.ProtocolPipeline;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class VelocityChannelInitializer extends ChannelInitializer {
|
||||
private final ChannelInitializer original;
|
||||
public class VelocityChannelInitializer extends ChannelInitializer<Channel> {
|
||||
private final ChannelInitializer<?> original;
|
||||
private final boolean clientSide;
|
||||
private static Method initChannel;
|
||||
|
||||
public VelocityChannelInitializer(ChannelInitializer original) {
|
||||
public VelocityChannelInitializer(ChannelInitializer<?> original, boolean clientSide) {
|
||||
this.original = original;
|
||||
this.clientSide = clientSide;
|
||||
}
|
||||
|
||||
static {
|
||||
@ -28,7 +30,7 @@ public class VelocityChannelInitializer extends ChannelInitializer {
|
||||
protected void initChannel(Channel channel) throws Exception {
|
||||
initChannel.invoke(original, channel);
|
||||
|
||||
UserConnection user = new UserConnection(channel);
|
||||
UserConnection user = new UserConnection(channel, clientSide);
|
||||
new ProtocolPipeline(user);
|
||||
|
||||
// We need to add a separated handler because Velocity uses pipeline().get(MINECRAFT_DECODER)
|
||||
|
@ -3,16 +3,21 @@ package us.myles.ViaVersion.velocity.handlers;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToByteEncoder;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import us.myles.ViaVersion.api.data.UserConnection;
|
||||
import us.myles.ViaVersion.exception.CancelDecoderException;
|
||||
import us.myles.ViaVersion.exception.CancelCodecException;
|
||||
import us.myles.ViaVersion.exception.CancelDecoderException;
|
||||
import us.myles.ViaVersion.util.PipelineUtil;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
@ChannelHandler.Sharable
|
||||
public class VelocityDecodeHandler extends MessageToMessageDecoder<ByteBuf> {
|
||||
private final UserConnection info;
|
||||
private boolean handledCompression;
|
||||
private boolean skipDoubleTransform;
|
||||
|
||||
public VelocityDecodeHandler(UserConnection info) {
|
||||
this.info = info;
|
||||
@ -20,6 +25,12 @@ public class VelocityDecodeHandler extends MessageToMessageDecoder<ByteBuf> {
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf bytebuf, List<Object> out) throws Exception {
|
||||
if (skipDoubleTransform) {
|
||||
skipDoubleTransform = false;
|
||||
out.add(bytebuf.retain());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.checkIncomingPacket()) throw CancelDecoderException.generate(null);
|
||||
if (!info.shouldTransformPacket()) {
|
||||
out.add(bytebuf.retain());
|
||||
@ -28,13 +39,55 @@ public class VelocityDecodeHandler extends MessageToMessageDecoder<ByteBuf> {
|
||||
|
||||
ByteBuf transformedBuf = ctx.alloc().buffer().writeBytes(bytebuf);
|
||||
try {
|
||||
boolean needsCompress = handleCompressionOrder(ctx, transformedBuf);
|
||||
|
||||
info.transformIncoming(transformedBuf, CancelDecoderException::generate);
|
||||
|
||||
if (needsCompress) {
|
||||
recompress(ctx, transformedBuf);
|
||||
skipDoubleTransform = true;
|
||||
}
|
||||
out.add(transformedBuf.retain());
|
||||
} finally {
|
||||
transformedBuf.release();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleCompressionOrder(ChannelHandlerContext ctx, ByteBuf buf) throws InvocationTargetException {
|
||||
if (handledCompression) return false;
|
||||
|
||||
int decoderIndex = ctx.pipeline().names().indexOf("compression-decoder");
|
||||
if (decoderIndex == -1) return false;
|
||||
handledCompression = true;
|
||||
if (decoderIndex > ctx.pipeline().names().indexOf("via-decoder")) {
|
||||
// Need to decompress this packet due to bad order
|
||||
ByteBuf decompressed = (ByteBuf) PipelineUtil.callDecode((MessageToMessageDecoder<?>) ctx.pipeline().get("compression-decoder"), ctx, buf).get(0);
|
||||
try {
|
||||
buf.clear().writeBytes(decompressed);
|
||||
} finally {
|
||||
decompressed.release();
|
||||
}
|
||||
ChannelHandler encoder = ctx.pipeline().get("via-encoder");
|
||||
ChannelHandler decoder = ctx.pipeline().get("via-decoder");
|
||||
ctx.pipeline().remove(encoder);
|
||||
ctx.pipeline().remove(decoder);
|
||||
ctx.pipeline().addAfter("compression-encoder", "via-encoder", encoder);
|
||||
ctx.pipeline().addAfter("compression-decoder", "via-decoder", decoder);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void recompress(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
|
||||
ByteBuf compressed = ctx.alloc().buffer();
|
||||
try {
|
||||
PipelineUtil.callEncode((MessageToByteEncoder<?>) ctx.pipeline().get("compression-encoder"), ctx, buf, compressed);
|
||||
buf.clear().writeBytes(compressed);
|
||||
} finally {
|
||||
compressed.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
if (cause instanceof CancelCodecException) return;
|
||||
|
@ -25,7 +25,7 @@ public class VelocityEncodeHandler extends MessageToMessageEncoder<ByteBuf> {
|
||||
|
||||
@Override
|
||||
protected void encode(final ChannelHandlerContext ctx, ByteBuf bytebuf, List<Object> out) throws Exception {
|
||||
info.checkOutgoingPacket();
|
||||
if (!info.checkOutgoingPacket()) throw CancelEncoderException.generate(null);
|
||||
if (!info.shouldTransformPacket()) {
|
||||
out.add(bytebuf.retain());
|
||||
return;
|
||||
@ -47,9 +47,12 @@ public class VelocityEncodeHandler extends MessageToMessageEncoder<ByteBuf> {
|
||||
}
|
||||
|
||||
private boolean handleCompressionOrder(ChannelHandlerContext ctx, ByteBuf buf) throws InvocationTargetException {
|
||||
boolean needsCompress = false;
|
||||
if (!handledCompression
|
||||
&& ctx.pipeline().names().indexOf("compression-encoder") > ctx.pipeline().names().indexOf("via-encoder")) {
|
||||
if (handledCompression) return false;
|
||||
|
||||
int encoderIndex = ctx.pipeline().names().indexOf("compression-encoder");
|
||||
if (encoderIndex == -1) return false;
|
||||
handledCompression = true;
|
||||
if (encoderIndex > ctx.pipeline().names().indexOf("via-encoder")) {
|
||||
// Need to decompress this packet due to bad order
|
||||
ByteBuf decompressed = (ByteBuf) PipelineUtil.callDecode((MessageToMessageDecoder<?>) ctx.pipeline().get("compression-decoder"), ctx, buf).get(0);
|
||||
try {
|
||||
@ -63,10 +66,9 @@ public class VelocityEncodeHandler extends MessageToMessageEncoder<ByteBuf> {
|
||||
ctx.pipeline().remove(decoder);
|
||||
ctx.pipeline().addAfter("compression-encoder", "via-encoder", encoder);
|
||||
ctx.pipeline().addAfter("compression-decoder", "via-decoder", decoder);
|
||||
needsCompress = true;
|
||||
handledCompression = true;
|
||||
return true;
|
||||
}
|
||||
return needsCompress;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void recompress(ChannelHandlerContext ctx, ByteBuf buf) throws InvocationTargetException {
|
||||
|
@ -1,182 +0,0 @@
|
||||
package us.myles.ViaVersion.velocity.handlers;
|
||||
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import us.myles.ViaVersion.api.PacketWrapper;
|
||||
import us.myles.ViaVersion.api.Pair;
|
||||
import us.myles.ViaVersion.api.Via;
|
||||
import us.myles.ViaVersion.api.data.UserConnection;
|
||||
import us.myles.ViaVersion.api.protocol.Protocol;
|
||||
import us.myles.ViaVersion.api.protocol.ProtocolPipeline;
|
||||
import us.myles.ViaVersion.api.protocol.ProtocolRegistry;
|
||||
import us.myles.ViaVersion.api.type.Type;
|
||||
import us.myles.ViaVersion.protocols.base.ProtocolInfo;
|
||||
import us.myles.ViaVersion.protocols.protocol1_13to1_12_2.packets.InventoryPackets;
|
||||
import us.myles.ViaVersion.protocols.protocol1_9to1_8.Protocol1_9To1_8;
|
||||
import us.myles.ViaVersion.velocity.service.ProtocolDetectorService;
|
||||
import us.myles.ViaVersion.velocity.storage.VelocityStorage;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class VelocityServerHandler {
|
||||
private static Method setProtocolVersion;
|
||||
private static Method setNextProtocolVersion;
|
||||
private static Method getMinecraftConnection;
|
||||
private static Method getNextProtocolVersion;
|
||||
private static Method getKnownChannels;
|
||||
|
||||
static {
|
||||
try {
|
||||
setProtocolVersion = Class.forName("com.velocitypowered.proxy.connection.MinecraftConnection")
|
||||
.getDeclaredMethod("setProtocolVersion", ProtocolVersion.class);
|
||||
setNextProtocolVersion = Class.forName("com.velocitypowered.proxy.connection.MinecraftConnection")
|
||||
.getDeclaredMethod("setNextProtocolVersion", ProtocolVersion.class);
|
||||
Class<?> connectedPlayer = Class.forName("com.velocitypowered.proxy.connection.client.ConnectedPlayer");
|
||||
getMinecraftConnection = connectedPlayer.getDeclaredMethod("getMinecraftConnection");
|
||||
getNextProtocolVersion = Class.forName("com.velocitypowered.proxy.connection.MinecraftConnection")
|
||||
.getDeclaredMethod("getNextProtocolVersion");
|
||||
getKnownChannels = connectedPlayer.getDeclaredMethod("getKnownChannels");
|
||||
} catch (NoSuchMethodException | ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void preServerConnect(ServerPreConnectEvent e) {
|
||||
try {
|
||||
UserConnection user = Via.getManager().getConnection(e.getPlayer().getUniqueId());
|
||||
if (user == null) return;
|
||||
if (!user.has(VelocityStorage.class)) {
|
||||
user.put(new VelocityStorage(user, e.getPlayer()));
|
||||
}
|
||||
|
||||
int protocolId = ProtocolDetectorService.getProtocolId(e.getOriginalServer().getServerInfo().getName());
|
||||
List<Pair<Integer, Protocol>> protocols = ProtocolRegistry.getProtocolPath(user.getProtocolInfo().getProtocolVersion(), protocolId);
|
||||
|
||||
// Check if ViaVersion can support that version
|
||||
Object connection = getMinecraftConnection.invoke(e.getPlayer());
|
||||
setNextProtocolVersion.invoke(connection, ProtocolVersion.getProtocolVersion(protocols == null
|
||||
? user.getProtocolInfo().getProtocolVersion()
|
||||
: protocolId));
|
||||
|
||||
} catch (IllegalAccessException | InvocationTargetException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LATE)
|
||||
public void connectedEvent(ServerConnectedEvent e) {
|
||||
UserConnection user = Via.getManager().getConnection(e.getPlayer().getUniqueId());
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
checkServerChange(e, Via.getManager().getConnection(e.getPlayer().getUniqueId()));
|
||||
} catch (Exception e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
}, user.getChannel().eventLoop()).join();
|
||||
}
|
||||
|
||||
public void checkServerChange(ServerConnectedEvent e, UserConnection user) throws Exception {
|
||||
if (user == null) return;
|
||||
// Handle server/version change
|
||||
if (user.has(VelocityStorage.class)) {
|
||||
VelocityStorage storage = user.get(VelocityStorage.class);
|
||||
|
||||
if (e.getServer() != null) {
|
||||
if (!e.getServer().getServerInfo().getName().equals(storage.getCurrentServer())) {
|
||||
String serverName = e.getServer().getServerInfo().getName();
|
||||
|
||||
storage.setCurrentServer(serverName);
|
||||
|
||||
int protocolId = ProtocolDetectorService.getProtocolId(serverName);
|
||||
|
||||
if (protocolId <= ProtocolVersion.MINECRAFT_1_8.getProtocol()) { // 1.8 doesn't have BossBar packet
|
||||
if (storage.getBossbar() != null) {
|
||||
// TODO: Verify whether this packet needs to be sent when 1.8 -> 1.9 protocol isn't present in the pipeline
|
||||
// This ensures we can encode it properly as only the 1.9 protocol is currently implemented.
|
||||
if (user.getProtocolInfo().getPipeline().contains(Protocol1_9To1_8.class)) {
|
||||
for (UUID uuid : storage.getBossbar()) {
|
||||
PacketWrapper wrapper = new PacketWrapper(0x0C, null, user);
|
||||
wrapper.write(Type.UUID, uuid);
|
||||
wrapper.write(Type.VAR_INT, 1); // remove
|
||||
wrapper.send(Protocol1_9To1_8.class, true, true);
|
||||
}
|
||||
}
|
||||
storage.getBossbar().clear();
|
||||
}
|
||||
}
|
||||
|
||||
ProtocolInfo info = user.getProtocolInfo();
|
||||
int previousServerProtocol = info.getServerProtocolVersion();
|
||||
|
||||
// Refresh the pipes
|
||||
List<Pair<Integer, Protocol>> protocols = ProtocolRegistry.getProtocolPath(info.getProtocolVersion(), protocolId);
|
||||
ProtocolPipeline pipeline = info.getPipeline();
|
||||
user.clearStoredObjects();
|
||||
pipeline.cleanPipes();
|
||||
if (protocols == null) {
|
||||
// TODO Check Bungee Supported Protocols? *shrugs*
|
||||
protocolId = info.getProtocolVersion();
|
||||
} else {
|
||||
for (Pair<Integer, Protocol> prot : protocols) {
|
||||
pipeline.add(prot.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
info.setServerProtocolVersion(protocolId);
|
||||
// Add version-specific base Protocol
|
||||
pipeline.add(ProtocolRegistry.getBaseProtocol(protocolId));
|
||||
|
||||
Collection<String> knownChannels = (Collection<String>) getKnownChannels.invoke(e.getPlayer());
|
||||
if (previousServerProtocol != -1) {
|
||||
int id1_13 = ProtocolVersion.MINECRAFT_1_13.getProtocol();
|
||||
if (previousServerProtocol < id1_13 && protocolId >= id1_13) {
|
||||
List<String> newChannels = new ArrayList<>();
|
||||
for (String oldChannel : knownChannels) {
|
||||
String transformed = InventoryPackets.getNewPluginChannelId(oldChannel);
|
||||
if (transformed != null) {
|
||||
newChannels.add(transformed);
|
||||
}
|
||||
}
|
||||
knownChannels.clear();
|
||||
knownChannels.addAll(newChannels);
|
||||
} else if (previousServerProtocol >= id1_13 && protocolId < id1_13) {
|
||||
List<String> newChannels = new ArrayList<>();
|
||||
for (String oldChannel : knownChannels) {
|
||||
String transformed = InventoryPackets.getOldPluginChannelId(oldChannel);
|
||||
if (transformed != null) {
|
||||
newChannels.add(transformed);
|
||||
}
|
||||
}
|
||||
knownChannels.clear();
|
||||
knownChannels.addAll(newChannels);
|
||||
}
|
||||
}
|
||||
|
||||
user.put(info);
|
||||
user.put(storage);
|
||||
|
||||
user.setActive(protocols != null);
|
||||
|
||||
// Init all protocols TODO check if this can get moved up to the previous for loop, and doesn't require the pipeline to already exist.
|
||||
for (Protocol protocol : pipeline.pipes()) {
|
||||
protocol.init(user);
|
||||
}
|
||||
|
||||
Object connection = getMinecraftConnection.invoke(e.getPlayer());
|
||||
ProtocolVersion version = (ProtocolVersion) getNextProtocolVersion.invoke(connection);
|
||||
setProtocolVersion.invoke(connection, version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package us.myles.ViaVersion.velocity.listeners;
|
||||
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||
import us.myles.ViaVersion.api.PacketWrapper;
|
||||
import us.myles.ViaVersion.api.Via;
|
||||
import us.myles.ViaVersion.api.data.UserConnection;
|
||||
import us.myles.ViaVersion.api.minecraft.metadata.Metadata;
|
||||
import us.myles.ViaVersion.api.minecraft.metadata.types.MetaType1_9;
|
||||
import us.myles.ViaVersion.api.type.Type;
|
||||
import us.myles.ViaVersion.api.type.types.version.Types1_9;
|
||||
import us.myles.ViaVersion.protocols.base.ProtocolInfo;
|
||||
import us.myles.ViaVersion.protocols.protocol1_9to1_8.Protocol1_9To1_8;
|
||||
import us.myles.ViaVersion.protocols.protocol1_9to1_8.storage.EntityTracker1_9;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/*
|
||||
* This patches https://github.com/ViaVersion/ViaVersion/issues/555
|
||||
*/
|
||||
public class ElytraPatch {
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void onServerConnected(ServerConnectedEvent event) {
|
||||
UserConnection user = Via.getManager().getConnection(event.getPlayer().getUniqueId());
|
||||
if (user == null) return;
|
||||
|
||||
try {
|
||||
if (user.getProtocolInfo().getPipeline().contains(Protocol1_9To1_8.class)) {
|
||||
int entityId = user.get(EntityTracker1_9.class).getProvidedEntityId();
|
||||
|
||||
PacketWrapper wrapper = new PacketWrapper(0x39, null, user);
|
||||
|
||||
wrapper.write(Type.VAR_INT, entityId);
|
||||
wrapper.write(Types1_9.METADATA_LIST, Collections.singletonList(new Metadata(0, MetaType1_9.Byte, (byte) 0)));
|
||||
|
||||
wrapper.send(Protocol1_9To1_8.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
@ -30,13 +30,25 @@ public class VelocityViaInjector implements ViaInjector {
|
||||
return (ChannelInitializer) ReflectionUtil.invoke(channelInitializerHolder, "get");
|
||||
}
|
||||
|
||||
private ChannelInitializer getBackendInitializer() throws Exception {
|
||||
Object connectionManager = ReflectionUtil.get(VelocityPlugin.PROXY, "cm", Object.class);
|
||||
Object channelInitializerHolder = ReflectionUtil.invoke(connectionManager, "getBackendChannelInitializer");
|
||||
return (ChannelInitializer) ReflectionUtil.invoke(channelInitializerHolder, "get");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inject() throws Exception {
|
||||
Object connectionManager = ReflectionUtil.get(VelocityPlugin.PROXY, "cm", Object.class);
|
||||
Object channelInitializerHolder = ReflectionUtil.invoke(connectionManager, "getServerChannelInitializer");
|
||||
ChannelInitializer originalInitializer = getInitializer();
|
||||
channelInitializerHolder.getClass().getMethod("set", ChannelInitializer.class)
|
||||
.invoke(channelInitializerHolder, new VelocityChannelInitializer(originalInitializer));
|
||||
.invoke(channelInitializerHolder, new VelocityChannelInitializer(originalInitializer, false));
|
||||
|
||||
|
||||
Object backendInitializerHolder = ReflectionUtil.invoke(connectionManager, "getBackendChannelInitializer");
|
||||
ChannelInitializer backendInitializer = getBackendInitializer();
|
||||
backendInitializerHolder.getClass().getMethod("set", ChannelInitializer.class)
|
||||
.invoke(backendInitializerHolder, new VelocityChannelInitializer(backendInitializer, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -9,8 +9,6 @@ import us.myles.ViaVersion.api.protocol.ProtocolVersion;
|
||||
import us.myles.ViaVersion.protocols.base.VersionProvider;
|
||||
import us.myles.ViaVersion.protocols.protocol1_9to1_8.providers.BossBarProvider;
|
||||
import us.myles.ViaVersion.protocols.protocol1_9to1_8.providers.MovementTransmitterProvider;
|
||||
import us.myles.ViaVersion.velocity.handlers.VelocityServerHandler;
|
||||
import us.myles.ViaVersion.velocity.listeners.ElytraPatch;
|
||||
import us.myles.ViaVersion.velocity.listeners.UpdateListener;
|
||||
import us.myles.ViaVersion.velocity.providers.VelocityBossBarProvider;
|
||||
import us.myles.ViaVersion.velocity.providers.VelocityMovementTransmitter;
|
||||
@ -26,7 +24,6 @@ public class VelocityViaLoader implements ViaPlatformLoader {
|
||||
if (ProtocolRegistry.SERVER_PROTOCOL < ProtocolVersion.v1_9.getVersion()) {
|
||||
Via.getManager().getProviders().use(MovementTransmitterProvider.class, new VelocityMovementTransmitter());
|
||||
Via.getManager().getProviders().use(BossBarProvider.class, new VelocityBossBarProvider());
|
||||
VelocityPlugin.PROXY.getEventManager().register(plugin, new ElytraPatch());
|
||||
}
|
||||
|
||||
Via.getManager().getProviders().use(VersionProvider.class, new VelocityVersionProvider());
|
||||
@ -34,7 +31,6 @@ public class VelocityViaLoader implements ViaPlatformLoader {
|
||||
// We don't need main hand patch because Join Game packet makes client send hand data again
|
||||
|
||||
VelocityPlugin.PROXY.getEventManager().register(plugin, new UpdateListener());
|
||||
VelocityPlugin.PROXY.getEventManager().register(plugin, new VelocityServerHandler());
|
||||
|
||||
int pingInterval = ((VelocityViaConfig) Via.getPlatform().getConf()).getVelocityPingInterval();
|
||||
if (pingInterval > 0) {
|
||||
|
@ -1,19 +1,42 @@
|
||||
package us.myles.ViaVersion.velocity.providers;
|
||||
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import us.myles.ViaVersion.VelocityPlugin;
|
||||
import us.myles.ViaVersion.api.Via;
|
||||
import us.myles.ViaVersion.api.data.UserConnection;
|
||||
import us.myles.ViaVersion.api.protocol.ProtocolVersion;
|
||||
import us.myles.ViaVersion.protocols.base.VersionProvider;
|
||||
import us.myles.ViaVersion.velocity.platform.VelocityViaInjector;
|
||||
import us.myles.ViaVersion.velocity.service.ProtocolDetectorService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class VelocityVersionProvider extends VersionProvider {
|
||||
private static Method getAssociation;
|
||||
|
||||
static {
|
||||
try {
|
||||
getAssociation = Class.forName("com.velocitypowered.proxy.connection.MinecraftConnection").getMethod("getAssociation");
|
||||
} catch (NoSuchMethodException | ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getServerProtocol(UserConnection user) throws Exception {
|
||||
return user.isClientSide() ? getBackProtocol(user) : getFrontProtocol(user);
|
||||
}
|
||||
|
||||
private int getBackProtocol(UserConnection user) throws Exception {
|
||||
ChannelHandler mcHandler = user.getChannel().pipeline().get("handler");
|
||||
return ProtocolDetectorService.getProtocolId(
|
||||
((ServerConnection) getAssociation.invoke(mcHandler)).getServerInfo().getName());
|
||||
}
|
||||
|
||||
private int getFrontProtocol(UserConnection user) throws Exception {
|
||||
int playerVersion = user.getProtocolInfo().getProtocolVersion();
|
||||
|
||||
IntStream versions = com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS.stream()
|
||||
|
Loading…
Reference in New Issue
Block a user