Add support for Minecraft 1.8.x
This commit allows BungeeCord to support Minecraft clients both of versions 1.7.x and of 1.8.x. There should be no breakages to any other support, however following their deprecation and uselessness within 1.8, the Tab list APIs have been removed. Please report any issues to GitHub and be sure to mention client, server and BungeeCord versions. When used with an appropriate server jar (such as multi protocol Spigot), this will allow clients of many versions to concurrently be connected to the same set of servers.
This commit is contained in:
@@ -36,6 +36,7 @@ import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -65,7 +66,6 @@ import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.api.plugin.PluginManager;
|
||||
import net.md_5.bungee.api.tab.CustomTabList;
|
||||
import net.md_5.bungee.command.*;
|
||||
import net.md_5.bungee.conf.YamlConfig;
|
||||
import net.md_5.bungee.log.LoggingOutputStream;
|
||||
@@ -76,7 +76,6 @@ import net.md_5.bungee.protocol.ProtocolConstants;
|
||||
import net.md_5.bungee.protocol.packet.Chat;
|
||||
import net.md_5.bungee.protocol.packet.PluginMessage;
|
||||
import net.md_5.bungee.query.RemoteQuery;
|
||||
import net.md_5.bungee.tab.Custom;
|
||||
import net.md_5.bungee.util.CaseInsensitiveMap;
|
||||
import org.fusesource.jansi.AnsiConsole;
|
||||
|
||||
@@ -113,6 +112,8 @@ public class BungeeCord extends ProxyServer
|
||||
* Fully qualified connections.
|
||||
*/
|
||||
private final Map<String, UserConnection> connections = new CaseInsensitiveMap<>();
|
||||
// Used to help with packet rewriting
|
||||
private final Map<UUID, UserConnection> connectionsByOfflineUUID = new HashMap<>();
|
||||
private final ReadWriteLock connectionLock = new ReentrantReadWriteLock();
|
||||
/**
|
||||
* Plugin manager.
|
||||
@@ -144,7 +145,7 @@ public class BungeeCord extends ProxyServer
|
||||
private ConnectionThrottle connectionThrottle;
|
||||
private final ModuleManager moduleManager = new ModuleManager();
|
||||
|
||||
|
||||
|
||||
{
|
||||
// TODO: Proper fallback when we interface the manager
|
||||
getPluginManager().registerCommand( null, new CommandReload() );
|
||||
@@ -468,6 +469,18 @@ public class BungeeCord extends ProxyServer
|
||||
}
|
||||
}
|
||||
|
||||
public UserConnection getPlayerByOfflineUUID(UUID name)
|
||||
{
|
||||
connectionLock.readLock().lock();
|
||||
try
|
||||
{
|
||||
return connectionsByOfflineUUID.get( name );
|
||||
} finally
|
||||
{
|
||||
connectionLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProxiedPlayer getPlayer(UUID uuid)
|
||||
{
|
||||
@@ -577,6 +590,7 @@ public class BungeeCord extends ProxyServer
|
||||
try
|
||||
{
|
||||
connections.put( con.getName(), con );
|
||||
connectionsByOfflineUUID.put( con.getPendingConnection().getOfflineId(), con );
|
||||
} finally
|
||||
{
|
||||
connectionLock.writeLock().unlock();
|
||||
@@ -589,19 +603,13 @@ public class BungeeCord extends ProxyServer
|
||||
try
|
||||
{
|
||||
connections.remove( con.getName() );
|
||||
connectionsByOfflineUUID.remove( con.getPendingConnection().getOfflineId() );
|
||||
} finally
|
||||
{
|
||||
connectionLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomTabList customTabList(ProxiedPlayer player)
|
||||
{
|
||||
return new Custom( player );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getDisabledCommands()
|
||||
{
|
||||
return config.getDisabledCommands();
|
||||
|
@@ -31,6 +31,7 @@ import net.md_5.bungee.protocol.packet.ScoreboardObjective;
|
||||
import net.md_5.bungee.protocol.packet.PluginMessage;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginSuccess;
|
||||
import net.md_5.bungee.protocol.packet.SetCompression;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ServerConnector extends PacketHandler
|
||||
@@ -102,6 +103,13 @@ public class ServerConnector extends PacketHandler
|
||||
throw CancelSendSignal.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(SetCompression setCompression) throws Exception
|
||||
{
|
||||
user.setCompressionThreshold( setCompression.getThreshold() );
|
||||
ch.setCompressionThreshold( setCompression.getThreshold() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(Login login) throws Exception
|
||||
{
|
||||
@@ -139,7 +147,7 @@ public class ServerConnector extends PacketHandler
|
||||
|
||||
// Set tab list size, this sucks balls, TODO: what shall we do about packet mutability
|
||||
Login modLogin = new Login( login.getEntityId(), login.getGameMode(), (byte) login.getDimension(), login.getDifficulty(),
|
||||
(byte) user.getPendingConnection().getListener().getTabListSize(), login.getLevelType() );
|
||||
(byte) user.getPendingConnection().getListener().getTabListSize(), login.getLevelType(), login.isReducedDebugInfo() );
|
||||
|
||||
user.unsafe().sendPacket( modLogin );
|
||||
|
||||
@@ -148,7 +156,7 @@ public class ServerConnector extends PacketHandler
|
||||
user.unsafe().sendPacket( new PluginMessage( "MC|Brand", out.toArray() ) );
|
||||
} else
|
||||
{
|
||||
user.getTabList().onServerChange();
|
||||
user.getTabListHandler().onServerChange();
|
||||
|
||||
Scoreboard serverScoreboard = user.getServerSentScoreboard();
|
||||
for ( Objective objective : serverScoreboard.getObjectives() )
|
||||
|
@@ -30,7 +30,6 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.PermissionCheckEvent;
|
||||
import net.md_5.bungee.api.event.ServerConnectEvent;
|
||||
import net.md_5.bungee.api.score.Scoreboard;
|
||||
import net.md_5.bungee.api.tab.TabListHandler;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.entitymap.EntityMap;
|
||||
@@ -46,6 +45,11 @@ import net.md_5.bungee.protocol.packet.Chat;
|
||||
import net.md_5.bungee.protocol.packet.ClientSettings;
|
||||
import net.md_5.bungee.protocol.packet.PluginMessage;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.SetCompression;
|
||||
import net.md_5.bungee.tab.Global;
|
||||
import net.md_5.bungee.tab.GlobalPing;
|
||||
import net.md_5.bungee.tab.ServerUnique;
|
||||
import net.md_5.bungee.tab.TabList;
|
||||
import net.md_5.bungee.util.CaseInsensitiveSet;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@@ -73,8 +77,6 @@ public final class UserConnection implements ProxiedPlayer
|
||||
private final Collection<ServerInfo> pendingConnects = new HashSet<>();
|
||||
/*========================================================================*/
|
||||
@Getter
|
||||
private TabListHandler tabList;
|
||||
@Getter
|
||||
@Setter
|
||||
private int sentPingId;
|
||||
@Getter
|
||||
@@ -86,6 +88,13 @@ public final class UserConnection implements ProxiedPlayer
|
||||
@Getter
|
||||
@Setter
|
||||
private ServerInfo reconnectServer;
|
||||
@Getter
|
||||
private TabList tabListHandler;
|
||||
@Getter
|
||||
@Setter
|
||||
private int gamemode;
|
||||
@Getter
|
||||
private int compressionThreshold = -1;
|
||||
/*========================================================================*/
|
||||
private final Collection<String> groups = new CaseInsensitiveSet();
|
||||
private final Collection<String> permissions = new CaseInsensitiveSet();
|
||||
@@ -121,14 +130,19 @@ public final class UserConnection implements ProxiedPlayer
|
||||
this.entityRewrite = EntityMap.getEntityMap( getPendingConnection().getVersion() );
|
||||
|
||||
this.displayName = name;
|
||||
try
|
||||
|
||||
switch ( getPendingConnection().getListener().getTabListType() )
|
||||
{
|
||||
this.tabList = getPendingConnection().getListener().getTabList().getDeclaredConstructor().newInstance();
|
||||
} catch ( ReflectiveOperationException ex )
|
||||
{
|
||||
throw new RuntimeException( ex );
|
||||
case "GLOBAL":
|
||||
tabListHandler = new Global( this );
|
||||
break;
|
||||
case "SERVER":
|
||||
tabListHandler = new ServerUnique( this );
|
||||
break;
|
||||
default:
|
||||
tabListHandler = new GlobalPing( this );
|
||||
break;
|
||||
}
|
||||
this.tabList.init( this );
|
||||
|
||||
Collection<String> g = bungee.getConfigurationAdapter().getGroups( name );
|
||||
for ( String s : g )
|
||||
@@ -137,13 +151,6 @@ public final class UserConnection implements ProxiedPlayer
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTabList(TabListHandler tabList)
|
||||
{
|
||||
tabList.init( this );
|
||||
this.tabList = tabList;
|
||||
}
|
||||
|
||||
public void sendPacket(PacketWrapper packet)
|
||||
{
|
||||
ch.write( packet );
|
||||
@@ -160,9 +167,7 @@ public final class UserConnection implements ProxiedPlayer
|
||||
{
|
||||
Preconditions.checkNotNull( name, "displayName" );
|
||||
Preconditions.checkArgument( name.length() <= 16, "Display name cannot be longer than 16 characters" );
|
||||
getTabList().onDisconnect();
|
||||
displayName = name;
|
||||
getTabList().onConnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -461,4 +466,14 @@ public final class UserConnection implements ProxiedPlayer
|
||||
{
|
||||
return ( locale == null && settings != null ) ? locale = Locale.forLanguageTag( settings.getLocale().replaceAll( "_", "-" ) ) : locale;
|
||||
}
|
||||
|
||||
public void setCompressionThreshold(int compressionThreshold)
|
||||
{
|
||||
if ( this.compressionThreshold == -1 )
|
||||
{
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
unsafe.sendPacket( new SetCompression( compressionThreshold ) );
|
||||
ch.setCompressionThreshold( compressionThreshold );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,10 +22,6 @@ import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ConfigurationAdapter;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.config.ServerInfo;
|
||||
import net.md_5.bungee.api.tab.TabListHandler;
|
||||
import net.md_5.bungee.tab.Global;
|
||||
import net.md_5.bungee.tab.GlobalPing;
|
||||
import net.md_5.bungee.tab.ServerUnique;
|
||||
import net.md_5.bungee.util.CaseInsensitiveMap;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
@@ -40,8 +36,7 @@ public class YamlConfig implements ConfigurationAdapter
|
||||
private enum DefaultTabList
|
||||
{
|
||||
|
||||
GLOBAL( Global.class ), GLOBAL_PING( GlobalPing.class ), SERVER( ServerUnique.class );
|
||||
private final Class<? extends TabListHandler> clazz;
|
||||
GLOBAL(), GLOBAL_PING(), SERVER();
|
||||
}
|
||||
private final Yaml yaml;
|
||||
private Map config;
|
||||
@@ -224,7 +219,7 @@ public class YamlConfig implements ConfigurationAdapter
|
||||
boolean query = get( "query_enabled", false, val );
|
||||
int queryPort = get( "query_port", 25577, val );
|
||||
|
||||
ListenerInfo info = new ListenerInfo( address, motd, maxPlayers, tabListSize, defaultServer, fallbackServer, forceDefault, forced, value.clazz, setLocalAddress, pingPassthrough, queryPort, query );
|
||||
ListenerInfo info = new ListenerInfo( address, motd, maxPlayers, tabListSize, defaultServer, fallbackServer, forceDefault, forced, value.toString(), setLocalAddress, pingPassthrough, queryPort, query );
|
||||
ret.add( info );
|
||||
}
|
||||
|
||||
|
@@ -31,6 +31,8 @@ import net.md_5.bungee.protocol.packet.ScoreboardScore;
|
||||
import net.md_5.bungee.protocol.packet.ScoreboardDisplay;
|
||||
import net.md_5.bungee.protocol.packet.PluginMessage;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.SetCompression;
|
||||
import net.md_5.bungee.tab.TabList;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class DownstreamBridge extends PacketHandler
|
||||
@@ -100,11 +102,8 @@ public class DownstreamBridge extends PacketHandler
|
||||
@Override
|
||||
public void handle(PlayerListItem playerList) throws Exception
|
||||
{
|
||||
|
||||
if ( !con.getTabList().onListUpdate( playerList.getUsername(), playerList.isOnline(), playerList.getPing() ) )
|
||||
{
|
||||
throw CancelSendSignal.INSTANCE;
|
||||
}
|
||||
con.getTabListHandler().onUpdate( TabList.rewrite( playerList ) );
|
||||
throw CancelSendSignal.INSTANCE; // Always throw because of profile rewriting
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -448,6 +447,13 @@ public class DownstreamBridge extends PacketHandler
|
||||
throw CancelSendSignal.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(SetCompression setCompression) throws Exception
|
||||
{
|
||||
con.setCompressionThreshold( setCompression.getThreshold() );
|
||||
server.getCh().setCompressionThreshold( setCompression.getThreshold() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
@@ -34,7 +34,7 @@ public class UpstreamBridge extends PacketHandler
|
||||
this.con = con;
|
||||
|
||||
BungeeCord.getInstance().addConnection( con );
|
||||
con.getTabList().onConnect();
|
||||
con.getTabListHandler().onConnect();
|
||||
con.unsafe().sendPacket( BungeeCord.getInstance().registerChannels() );
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class UpstreamBridge extends PacketHandler
|
||||
// We lost connection to the client
|
||||
PlayerDisconnectEvent event = new PlayerDisconnectEvent( con );
|
||||
bungee.getPluginManager().callEvent( event );
|
||||
con.getTabList().onDisconnect();
|
||||
con.getTabListHandler().onDisconnect();
|
||||
BungeeCord.getInstance().removeConnection( con );
|
||||
|
||||
if ( con.getServer() != null )
|
||||
@@ -62,10 +62,7 @@ public class UpstreamBridge extends PacketHandler
|
||||
@Override
|
||||
public void handle(PacketWrapper packet) throws Exception
|
||||
{
|
||||
if ( con.getPendingConnection().getVersion() <= ProtocolConstants.MINECRAFT_1_7_6 )
|
||||
{
|
||||
con.getEntityRewrite().rewriteServerbound( packet.buf, con.getClientEntityId(), con.getServerEntityId() );
|
||||
}
|
||||
con.getEntityRewrite().rewriteServerbound( packet.buf, con.getClientEntityId(), con.getServerEntityId() );
|
||||
if ( con.getServer() != null )
|
||||
{
|
||||
con.getServer().getCh().write( packet );
|
||||
@@ -78,7 +75,7 @@ public class UpstreamBridge extends PacketHandler
|
||||
if ( alive.getRandomId() == con.getSentPingId() )
|
||||
{
|
||||
int newPing = (int) ( System.currentTimeMillis() - con.getSentPingTime() );
|
||||
con.getTabList().onPingChange( newPing );
|
||||
con.getTabListHandler().onPingChange( newPing );
|
||||
con.setPing( newPing );
|
||||
}
|
||||
}
|
||||
|
@@ -30,8 +30,8 @@ public abstract class EntityMap
|
||||
return new EntityMap_1_7_2();
|
||||
case ProtocolConstants.MINECRAFT_1_7_6:
|
||||
return new EntityMap_1_7_6();
|
||||
case ProtocolConstants.MINECRAFT_14_11_a:
|
||||
return new EntityMap_14_11_a();
|
||||
case ProtocolConstants.MINECRAFT_SNAPSHOT:
|
||||
return new EntityMap_14_21_b();
|
||||
}
|
||||
throw new RuntimeException( "Version " + version + " has no entity map" );
|
||||
}
|
||||
|
@@ -4,14 +4,15 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.UserConnection;
|
||||
import net.md_5.bungee.connection.LoginResult;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.protocol.DefinedPacket;
|
||||
import net.md_5.bungee.protocol.ProtocolConstants;
|
||||
import java.util.UUID;
|
||||
|
||||
class EntityMap_14_11_a extends EntityMap
|
||||
class EntityMap_14_21_b extends EntityMap
|
||||
{
|
||||
|
||||
EntityMap_14_11_a()
|
||||
EntityMap_14_21_b()
|
||||
{
|
||||
addRewrite( 0x04, ProtocolConstants.Direction.TO_CLIENT, true ); // Entity Equipment
|
||||
addRewrite( 0x0A, ProtocolConstants.Direction.TO_CLIENT, true ); // Use bed
|
||||
@@ -38,6 +39,7 @@ class EntityMap_14_11_a extends EntityMap
|
||||
addRewrite( 0x25, ProtocolConstants.Direction.TO_CLIENT, true ); // Block Break Animation
|
||||
addRewrite( 0x2C, ProtocolConstants.Direction.TO_CLIENT, true ); // Spawn Global Entity
|
||||
addRewrite( 0x43, ProtocolConstants.Direction.TO_CLIENT, true ); // Camera
|
||||
addRewrite( 0x49, ProtocolConstants.Direction.TO_CLIENT, true ); // Update Entity NBT
|
||||
|
||||
addRewrite( 0x02, ProtocolConstants.Direction.TO_SERVER, true ); // Use Entity
|
||||
addRewrite( 0x0B, ProtocolConstants.Direction.TO_SERVER, true ); // Entity Action
|
||||
@@ -84,23 +86,24 @@ class EntityMap_14_11_a extends EntityMap
|
||||
}
|
||||
} else if ( packetId == 0x0E /* Spawn Object */ )
|
||||
{
|
||||
DefinedPacket.readVarInt( packet );
|
||||
int idLength = packet.readerIndex() - readerIndex - packetIdLength;
|
||||
|
||||
int type = packet.getByte( readerIndex + packetIdLength + idLength );
|
||||
DefinedPacket.readVarInt( packet );
|
||||
int type = packet.readUnsignedByte();
|
||||
|
||||
if ( type == 60 || type == 90 )
|
||||
{
|
||||
int readId = packet.getInt( packetIdLength + idLength + 15 );
|
||||
packet.skipBytes( 14 );
|
||||
int position = packet.readerIndex();
|
||||
int readId = packet.readInt();
|
||||
int changedId = -1;
|
||||
if ( readId == oldId )
|
||||
{
|
||||
packet.setInt( packetIdLength + idLength + 15, newId );
|
||||
packet.setInt( position, newId );
|
||||
changedId = newId;
|
||||
} else if ( readId == newId )
|
||||
{
|
||||
packet.setInt( packetIdLength + idLength + 15, oldId );
|
||||
changedId = newId;
|
||||
packet.setInt( position, oldId );
|
||||
changedId = oldId;
|
||||
}
|
||||
if ( changedId != -1 )
|
||||
{
|
||||
@@ -118,36 +121,17 @@ class EntityMap_14_11_a extends EntityMap
|
||||
}
|
||||
} else if ( packetId == 0x0C /* Spawn Player */ )
|
||||
{
|
||||
DefinedPacket.readVarInt( packet );
|
||||
DefinedPacket.readVarInt( packet ); // Entity ID
|
||||
int idLength = packet.readerIndex() - readerIndex - packetIdLength;
|
||||
String uuid = DefinedPacket.readString( packet );
|
||||
String username = DefinedPacket.readString( packet );
|
||||
int props = DefinedPacket.readVarInt( packet );
|
||||
if ( props == 0 )
|
||||
UUID uuid = DefinedPacket.readUUID( packet );
|
||||
ProxiedPlayer player;
|
||||
if ( ( player = BungeeCord.getInstance().getPlayerByOfflineUUID( uuid ) ) != null )
|
||||
{
|
||||
UserConnection player = (UserConnection) BungeeCord.getInstance().getPlayer( username );
|
||||
if ( player != null )
|
||||
{
|
||||
LoginResult profile = player.getPendingConnection().getLoginProfile();
|
||||
if ( profile != null && profile.getProperties() != null
|
||||
&& profile.getProperties().length >= 1 )
|
||||
{
|
||||
ByteBuf rest = packet.slice().copy();
|
||||
packet.readerIndex( readerIndex );
|
||||
packet.writerIndex( readerIndex + packetIdLength + idLength );
|
||||
DefinedPacket.writeString( player.getUniqueId().toString(), packet );
|
||||
DefinedPacket.writeString( username, packet );
|
||||
DefinedPacket.writeVarInt( profile.getProperties().length, packet );
|
||||
for ( LoginResult.Property property : profile.getProperties() )
|
||||
{
|
||||
DefinedPacket.writeString( property.getName(), packet );
|
||||
DefinedPacket.writeString( property.getValue(), packet );
|
||||
DefinedPacket.writeString( property.getSignature(), packet );
|
||||
}
|
||||
packet.writeBytes( rest );
|
||||
rest.release();
|
||||
}
|
||||
}
|
||||
int previous = packet.writerIndex();
|
||||
packet.readerIndex( readerIndex );
|
||||
packet.writerIndex( readerIndex + packetIdLength + idLength );
|
||||
DefinedPacket.writeUUID( player.getUniqueId(), packet );
|
||||
packet.writerIndex( previous );
|
||||
}
|
||||
} else if ( packetId == 0x42 /* Combat Event */ )
|
||||
{
|
||||
@@ -167,4 +151,29 @@ class EntityMap_14_11_a extends EntityMap
|
||||
}
|
||||
packet.readerIndex( readerIndex );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rewriteServerbound(ByteBuf packet, int oldId, int newId)
|
||||
{
|
||||
super.rewriteServerbound( packet, oldId, newId );
|
||||
//Special cases
|
||||
int readerIndex = packet.readerIndex();
|
||||
int packetId = DefinedPacket.readVarInt( packet );
|
||||
int packetIdLength = packet.readerIndex() - readerIndex;
|
||||
|
||||
if ( packetId == 0x18 /* Spectate */ )
|
||||
{
|
||||
UUID uuid = DefinedPacket.readUUID( packet );
|
||||
ProxiedPlayer player;
|
||||
if ( ( player = BungeeCord.getInstance().getPlayer( uuid ) ) != null )
|
||||
{
|
||||
int previous = packet.writerIndex();
|
||||
packet.readerIndex( readerIndex );
|
||||
packet.writerIndex( readerIndex + packetIdLength );
|
||||
DefinedPacket.writeUUID( ( (UserConnection) player ).getPendingConnection().getOfflineId(), packet );
|
||||
packet.writerIndex( previous );
|
||||
}
|
||||
}
|
||||
packet.readerIndex( readerIndex );
|
||||
}
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
package net.md_5.bungee.netty;
|
||||
|
||||
import net.md_5.bungee.protocol.PacketCompressor;
|
||||
import net.md_5.bungee.protocol.PacketDecompressor;
|
||||
import net.md_5.bungee.protocol.PacketWrapper;
|
||||
import com.google.common.base.Preconditions;
|
||||
import io.netty.channel.Channel;
|
||||
@@ -71,4 +73,28 @@ public class ChannelWrapper
|
||||
{
|
||||
return ch;
|
||||
}
|
||||
|
||||
public void setCompressionThreshold(int compressionThreshold)
|
||||
{
|
||||
if ( ch.pipeline().get( PacketCompressor.class ) == null && compressionThreshold != -1 )
|
||||
{
|
||||
addBefore( PipelineUtils.PACKET_ENCODER, "compress", new PacketCompressor() );
|
||||
}
|
||||
if ( compressionThreshold != -1 )
|
||||
{
|
||||
ch.pipeline().get( PacketCompressor.class ).setThreshold( compressionThreshold );
|
||||
} else
|
||||
{
|
||||
ch.pipeline().remove( "compress" );
|
||||
}
|
||||
|
||||
if ( ch.pipeline().get( PacketDecompressor.class ) == null && compressionThreshold != -1 )
|
||||
{
|
||||
addBefore( PipelineUtils.PACKET_DECODER, "decompress", new PacketDecompressor() );
|
||||
}
|
||||
if ( compressionThreshold == -1 )
|
||||
{
|
||||
ch.pipeline().remove( "decompress" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,158 +0,0 @@
|
||||
package net.md_5.bungee.tab;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.tab.CustomTabList;
|
||||
import net.md_5.bungee.api.tab.TabListAdapter;
|
||||
import net.md_5.bungee.protocol.packet.PlayerListItem;
|
||||
|
||||
public class Custom extends TabListAdapter implements CustomTabList
|
||||
{
|
||||
|
||||
private static final int ROWS = 20;
|
||||
private static final int COLUMNS = 3;
|
||||
private static final char[] FILLER = new char[]
|
||||
{
|
||||
'0', '1', '2', '2', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
|
||||
};
|
||||
private static final int MAX_LEN = 16;
|
||||
/*========================================================================*/
|
||||
private final Collection<String> sentStuff = new HashSet<>();
|
||||
/*========================================================================*/
|
||||
private final String[][] sent = new String[ ROWS ][ COLUMNS ];
|
||||
private final String[][] slots = new String[ ROWS ][ COLUMNS ];
|
||||
private int rowLim;
|
||||
private int colLim;
|
||||
|
||||
public Custom(ProxiedPlayer player)
|
||||
{
|
||||
this.init( player );
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String setSlot(int row, int column, String text)
|
||||
{
|
||||
return setSlot( row, column, text, true );
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String setSlot(int row, int column, String text, boolean update)
|
||||
{
|
||||
Preconditions.checkArgument( row > 0 && row <= ROWS, "row out of range" );
|
||||
Preconditions.checkArgument( column > 0 && column <= COLUMNS, "column out of range" );
|
||||
|
||||
if ( text != null )
|
||||
{
|
||||
Preconditions.checkArgument( text.length() <= MAX_LEN - 2, "text must be <= %s chars", MAX_LEN - 2 );
|
||||
Preconditions.checkArgument( !ChatColor.stripColor( text ).isEmpty(), "Text cannot consist entirely of colour codes" );
|
||||
text = attempt( text );
|
||||
sentStuff.add( text );
|
||||
|
||||
if ( rowLim < row || colLim < column )
|
||||
{
|
||||
rowLim = row;
|
||||
colLim = column;
|
||||
}
|
||||
}
|
||||
|
||||
slots[--row][--column] = text;
|
||||
if ( update )
|
||||
{
|
||||
update();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private String attempt(String s)
|
||||
{
|
||||
for ( char c : FILLER )
|
||||
{
|
||||
String attempt = s + Character.toString( ChatColor.COLOR_CHAR ) + c;
|
||||
if ( !sentStuff.contains( attempt ) )
|
||||
{
|
||||
return attempt;
|
||||
}
|
||||
}
|
||||
if ( s.length() <= MAX_LEN - 4 )
|
||||
{
|
||||
return attempt( s + Character.toString( ChatColor.COLOR_CHAR ) + FILLER[0] );
|
||||
}
|
||||
throw new IllegalArgumentException( "List already contains all variants of string" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void update()
|
||||
{
|
||||
clear();
|
||||
for ( int i = 0; i < rowLim; i++ )
|
||||
{
|
||||
for ( int j = 0; j < colLim; j++ )
|
||||
{
|
||||
String text = ( slots[i][j] != null ) ? slots[i][j] : new StringBuilder().append( base( i ) ).append( base( j ) ).toString();
|
||||
sent[i][j] = text;
|
||||
getPlayer().unsafe().sendPacket( new PlayerListItem( text, true, (short) 0 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear()
|
||||
{
|
||||
for ( int i = 0; i < rowLim; i++ )
|
||||
{
|
||||
for ( int j = 0; j < colLim; j++ )
|
||||
{
|
||||
if ( sent[i][j] != null )
|
||||
{
|
||||
String text = sent[i][j];
|
||||
sent[i][j] = null;
|
||||
getPlayer().unsafe().sendPacket( new PlayerListItem( text, false, (short) 9999 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getRows()
|
||||
{
|
||||
return ROWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getColumns()
|
||||
{
|
||||
return COLUMNS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getSize()
|
||||
{
|
||||
return ROWS * COLUMNS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListUpdate(String name, boolean online, int ping)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private static char[] base(int n)
|
||||
{
|
||||
String hex = Integer.toHexString( n + 1 );
|
||||
char[] alloc = new char[ hex.length() * 2 ];
|
||||
for ( int i = 0; i < alloc.length; i++ )
|
||||
{
|
||||
if ( i % 2 == 0 )
|
||||
{
|
||||
alloc[i] = ChatColor.COLOR_CHAR;
|
||||
} else
|
||||
{
|
||||
alloc[i] = hex.charAt( i / 2 );
|
||||
}
|
||||
}
|
||||
return alloc;
|
||||
}
|
||||
}
|
@@ -1,24 +1,30 @@
|
||||
package net.md_5.bungee.tab;
|
||||
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.UserConnection;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.tab.TabListAdapter;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.LoginResult;
|
||||
import net.md_5.bungee.protocol.ProtocolConstants;
|
||||
import net.md_5.bungee.protocol.packet.PlayerListItem;
|
||||
|
||||
public class Global extends TabListAdapter
|
||||
import java.util.Collection;
|
||||
|
||||
public class Global extends TabList
|
||||
{
|
||||
|
||||
private boolean sentPing;
|
||||
|
||||
@Override
|
||||
public void onConnect()
|
||||
public Global(ProxiedPlayer player)
|
||||
{
|
||||
for ( ProxiedPlayer p : ProxyServer.getInstance().getPlayers() )
|
||||
{
|
||||
getPlayer().unsafe().sendPacket( new PlayerListItem( p.getDisplayName(), true, (short) p.getPing() ) );
|
||||
}
|
||||
BungeeCord.getInstance().broadcast( new PlayerListItem( getPlayer().getDisplayName(), true, (short) getPlayer().getPing() ) );
|
||||
super( player );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(PlayerListItem playerListItem)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -27,19 +33,112 @@ public class Global extends TabListAdapter
|
||||
if ( !sentPing )
|
||||
{
|
||||
sentPing = true;
|
||||
BungeeCord.getInstance().broadcast( new PlayerListItem( getPlayer().getDisplayName(), true, (short) getPlayer().getPing() ) );
|
||||
PlayerListItem packet = new PlayerListItem();
|
||||
packet.setAction( PlayerListItem.Action.UPDATE_LATENCY );
|
||||
PlayerListItem.Item item = new PlayerListItem.Item();
|
||||
item.setUuid( player.getUniqueId() );
|
||||
item.setUsername( player.getName() );
|
||||
item.setDisplayName( ComponentSerializer.toString( TextComponent.fromLegacyText( player.getDisplayName() ) ) );
|
||||
item.setPing( player.getPing() );
|
||||
packet.setItems( new PlayerListItem.Item[]
|
||||
{
|
||||
item
|
||||
} );
|
||||
BungeeCord.getInstance().broadcast( packet );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerChange()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect()
|
||||
{
|
||||
PlayerListItem playerListItem = new PlayerListItem();
|
||||
playerListItem.setAction( PlayerListItem.Action.ADD_PLAYER );
|
||||
Collection<ProxiedPlayer> players = BungeeCord.getInstance().getPlayers();
|
||||
PlayerListItem.Item[] items = new PlayerListItem.Item[ players.size() ];
|
||||
playerListItem.setItems( items );
|
||||
int i = 0;
|
||||
for ( ProxiedPlayer p : players )
|
||||
{
|
||||
PlayerListItem.Item item = items[i++] = new PlayerListItem.Item();
|
||||
item.setUuid( p.getUniqueId() );
|
||||
item.setUsername( p.getName() );
|
||||
item.setDisplayName( ComponentSerializer.toString( TextComponent.fromLegacyText( p.getDisplayName() ) ) );
|
||||
LoginResult loginResult = ( (UserConnection) p ).getPendingConnection().getLoginProfile();
|
||||
String[][] props = new String[ loginResult.getProperties().length ][];
|
||||
for ( int j = 0; j < props.length; j++ )
|
||||
{
|
||||
props[ j] = new String[]
|
||||
{
|
||||
loginResult.getProperties()[j].getName(),
|
||||
loginResult.getProperties()[j].getValue(),
|
||||
loginResult.getProperties()[j].getSignature()
|
||||
};
|
||||
}
|
||||
item.setProperties( props );
|
||||
item.setGamemode( ( (UserConnection) p ).getGamemode() );
|
||||
item.setPing( p.getPing() );
|
||||
}
|
||||
if ( player.getPendingConnection().getVersion() >= ProtocolConstants.MINECRAFT_SNAPSHOT )
|
||||
{
|
||||
player.unsafe().sendPacket( playerListItem );
|
||||
} else
|
||||
{
|
||||
// Split up the packet
|
||||
for ( PlayerListItem.Item item : playerListItem.getItems() )
|
||||
{
|
||||
PlayerListItem packet = new PlayerListItem();
|
||||
packet.setAction( playerListItem.getAction() );
|
||||
PlayerListItem.Item[] it = new PlayerListItem.Item[ 1 ];
|
||||
it[0] = item;
|
||||
packet.setItems( it );
|
||||
player.unsafe().sendPacket( packet );
|
||||
}
|
||||
}
|
||||
PlayerListItem packet = new PlayerListItem();
|
||||
packet.setAction( PlayerListItem.Action.ADD_PLAYER );
|
||||
PlayerListItem.Item item = new PlayerListItem.Item();
|
||||
item.setUuid( player.getUniqueId() );
|
||||
item.setUsername( player.getName() );
|
||||
item.setDisplayName( ComponentSerializer.toString( TextComponent.fromLegacyText( player.getDisplayName() ) ) );
|
||||
LoginResult loginResult = ( (UserConnection) player ).getPendingConnection().getLoginProfile();
|
||||
String[][] props = new String[ loginResult.getProperties().length ][];
|
||||
for ( int j = 0; j < props.length; j++ )
|
||||
{
|
||||
props[ j] = new String[]
|
||||
{
|
||||
loginResult.getProperties()[j].getName(),
|
||||
loginResult.getProperties()[j].getValue(),
|
||||
loginResult.getProperties()[j].getSignature()
|
||||
};
|
||||
}
|
||||
item.setProperties( props );
|
||||
item.setGamemode( ( (UserConnection) player ).getGamemode() );
|
||||
item.setPing( player.getPing() );
|
||||
packet.setItems( new PlayerListItem.Item[]
|
||||
{
|
||||
item
|
||||
} );
|
||||
BungeeCord.getInstance().broadcast( packet );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect()
|
||||
{
|
||||
BungeeCord.getInstance().broadcast( new PlayerListItem( getPlayer().getDisplayName(), false, (short) 9999 ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListUpdate(String name, boolean online, int ping)
|
||||
{
|
||||
return false;
|
||||
PlayerListItem packet = new PlayerListItem();
|
||||
packet.setAction( PlayerListItem.Action.REMOVE_PLAYER );
|
||||
PlayerListItem.Item item = new PlayerListItem.Item();
|
||||
item.setUuid( player.getUniqueId() );
|
||||
item.setUsername( player.getName() );
|
||||
packet.setItems( new PlayerListItem.Item[]
|
||||
{
|
||||
item
|
||||
} );
|
||||
BungeeCord.getInstance().broadcast( packet );
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package net.md_5.bungee.tab;
|
||||
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.protocol.packet.PlayerListItem;
|
||||
|
||||
public class GlobalPing extends Global
|
||||
@@ -10,13 +13,29 @@ public class GlobalPing extends Global
|
||||
/*========================================================================*/
|
||||
private int lastPing;
|
||||
|
||||
public GlobalPing(ProxiedPlayer player)
|
||||
{
|
||||
super( player );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPingChange(int ping)
|
||||
{
|
||||
if ( ping - PING_THRESHOLD > lastPing && ping + PING_THRESHOLD < lastPing )
|
||||
{
|
||||
lastPing = ping;
|
||||
BungeeCord.getInstance().broadcast( new PlayerListItem( getPlayer().getDisplayName(), true, (short) ping ) );
|
||||
PlayerListItem packet = new PlayerListItem();
|
||||
packet.setAction( PlayerListItem.Action.UPDATE_LATENCY );
|
||||
PlayerListItem.Item item = new PlayerListItem.Item();
|
||||
item.setUuid( player.getUniqueId() );
|
||||
item.setUsername( player.getName() );
|
||||
item.setDisplayName( ComponentSerializer.toString( TextComponent.fromLegacyText( player.getDisplayName() ) ) );
|
||||
item.setPing( player.getPing() );
|
||||
packet.setItems( new PlayerListItem.Item[]
|
||||
{
|
||||
item
|
||||
} );
|
||||
BungeeCord.getInstance().broadcast( packet );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,38 +2,105 @@ package net.md_5.bungee.tab;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import net.md_5.bungee.api.tab.TabListAdapter;
|
||||
import java.util.UUID;
|
||||
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.protocol.ProtocolConstants;
|
||||
import net.md_5.bungee.protocol.packet.PlayerListItem;
|
||||
|
||||
public class ServerUnique extends TabListAdapter
|
||||
public class ServerUnique extends TabList
|
||||
{
|
||||
|
||||
private final Collection<String> usernames = new HashSet<>();
|
||||
private final Collection<UUID> uuids = new HashSet<>();
|
||||
private final Collection<String> usernames = new HashSet<>(); // Support for <=1.7.9
|
||||
|
||||
public ServerUnique(ProxiedPlayer player)
|
||||
{
|
||||
super( player );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(PlayerListItem playerListItem)
|
||||
{
|
||||
for ( PlayerListItem.Item item : playerListItem.getItems() )
|
||||
{
|
||||
if ( playerListItem.getAction() == PlayerListItem.Action.ADD_PLAYER )
|
||||
{
|
||||
if ( item.getUuid() != null )
|
||||
{
|
||||
uuids.add( item.getUuid() );
|
||||
} else
|
||||
{
|
||||
usernames.add( item.getUsername() );
|
||||
}
|
||||
} else if ( playerListItem.getAction() == PlayerListItem.Action.REMOVE_PLAYER )
|
||||
{
|
||||
if ( item.getUuid() != null )
|
||||
{
|
||||
uuids.remove( item.getUuid() );
|
||||
} else
|
||||
{
|
||||
usernames.remove( item.getUsername() );
|
||||
}
|
||||
}
|
||||
}
|
||||
player.unsafe().sendPacket( playerListItem );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPingChange(int ping)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerChange()
|
||||
{
|
||||
synchronized ( usernames )
|
||||
PlayerListItem packet = new PlayerListItem();
|
||||
packet.setAction( PlayerListItem.Action.REMOVE_PLAYER );
|
||||
PlayerListItem.Item[] items = new PlayerListItem.Item[ uuids.size() + usernames.size() ];
|
||||
int i = 0;
|
||||
for ( UUID uuid : uuids )
|
||||
{
|
||||
for ( String username : usernames )
|
||||
{
|
||||
getPlayer().unsafe().sendPacket( new PlayerListItem( username, false, (short) 9999 ) );
|
||||
}
|
||||
usernames.clear();
|
||||
PlayerListItem.Item item = items[i++] = new PlayerListItem.Item();
|
||||
item.setUuid( uuid );
|
||||
}
|
||||
for ( String username : usernames )
|
||||
{
|
||||
PlayerListItem.Item item = items[i++] = new PlayerListItem.Item();
|
||||
item.setUsername( username );
|
||||
item.setDisplayName( username );
|
||||
}
|
||||
packet.setItems( items );
|
||||
if ( player.getPendingConnection().getVersion() >= ProtocolConstants.MINECRAFT_SNAPSHOT )
|
||||
{
|
||||
player.unsafe().sendPacket( packet );
|
||||
} else
|
||||
{
|
||||
// Split up the packet
|
||||
for ( PlayerListItem.Item item : packet.getItems() )
|
||||
{
|
||||
PlayerListItem p2 = new PlayerListItem();
|
||||
p2.setAction( packet.getAction() );
|
||||
PlayerListItem.Item[] it = new PlayerListItem.Item[ 1 ];
|
||||
it[0] = item;
|
||||
p2.setItems( it );
|
||||
player.unsafe().sendPacket( p2 );
|
||||
}
|
||||
}
|
||||
uuids.clear();
|
||||
usernames.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListUpdate(String name, boolean online, int ping)
|
||||
public void onConnect()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect()
|
||||
{
|
||||
if ( online )
|
||||
{
|
||||
usernames.add( name );
|
||||
} else
|
||||
{
|
||||
usernames.remove( name );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
59
proxy/src/main/java/net/md_5/bungee/tab/TabList.java
Normal file
59
proxy/src/main/java/net/md_5/bungee/tab/TabList.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package net.md_5.bungee.tab;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.UserConnection;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.connection.LoginResult;
|
||||
import net.md_5.bungee.protocol.packet.PlayerListItem;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class TabList
|
||||
{
|
||||
|
||||
protected final ProxiedPlayer player;
|
||||
|
||||
public abstract void onUpdate(PlayerListItem playerListItem);
|
||||
|
||||
public abstract void onPingChange(int ping);
|
||||
|
||||
public abstract void onServerChange();
|
||||
|
||||
public abstract void onConnect();
|
||||
|
||||
public abstract void onDisconnect();
|
||||
|
||||
public static PlayerListItem rewrite(PlayerListItem playerListItem)
|
||||
{
|
||||
for ( PlayerListItem.Item item : playerListItem.getItems() )
|
||||
{
|
||||
if ( item.getUuid() == null ) // Old style ping
|
||||
{
|
||||
continue;
|
||||
}
|
||||
UserConnection player = BungeeCord.getInstance().getPlayerByOfflineUUID( item.getUuid() );
|
||||
if ( player != null )
|
||||
{
|
||||
item.setUuid( player.getUniqueId() );
|
||||
LoginResult loginResult = player.getPendingConnection().getLoginProfile();
|
||||
String[][] props = new String[ loginResult.getProperties().length ][];
|
||||
for ( int i = 0; i < props.length; i++ )
|
||||
{
|
||||
props[ i] = new String[]
|
||||
{
|
||||
loginResult.getProperties()[i].getName(),
|
||||
loginResult.getProperties()[i].getValue(),
|
||||
loginResult.getProperties()[i].getSignature()
|
||||
};
|
||||
}
|
||||
item.setProperties( props );
|
||||
if ( playerListItem.getAction() == PlayerListItem.Action.ADD_PLAYER || playerListItem.getAction() == PlayerListItem.Action.UPDATE_GAMEMODE )
|
||||
{
|
||||
player.setGamemode( item.getGamemode() );
|
||||
}
|
||||
player.setPing( player.getPing() );
|
||||
}
|
||||
}
|
||||
return playerListItem;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user