Implement Support for MinecraftForge / FML 1.7.10

Additional implementation help provided by @jk-5 and @bloodmc.
This commit is contained in:
Daniel Naylor 2014-09-26 10:20:19 +10:00 committed by md_5
parent 8715c5fd82
commit cfad2c65d4
24 changed files with 1111 additions and 24 deletions

View File

@ -1,5 +1,7 @@
package net.md_5.bungee.api; package net.md_5.bungee.api;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@ -74,6 +76,27 @@ public class ServerPing
private String description; private String description;
private Favicon favicon; private Favicon favicon;
@Data
public static class ModInfo
{
private String type = "FML";
private List<ModItem> modList = new ArrayList<>();
}
@Data
@AllArgsConstructor
public static class ModItem
{
private String modid;
private String version;
}
// Right now, we don't get the mods from the user, so we just use a stock ModInfo object to
// create the server ping. Vanilla clients will ignore this.
private final ModInfo modinfo = new ModInfo();
@Deprecated @Deprecated
public ServerPing(Protocol version, Players players, String description, String favicon) public ServerPing(Protocol version, Players players, String description, String favicon)
{ {

View File

@ -1,6 +1,7 @@
package net.md_5.bungee.api.connection; package net.md_5.bungee.api.connection;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import net.md_5.bungee.api.Callback; import net.md_5.bungee.api.Callback;
import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.CommandSender;
@ -153,4 +154,28 @@ public interface ProxiedPlayer extends Connection, CommandSender
* @see Title * @see Title
*/ */
void sendTitle(Title title); void sendTitle(Title title);
/**
* Gets this player's Forge Mod List, if the player has sent this
* information during the lifetime of their connection to Bungee. There is
* no guarantee that information is available at any time, as it is only
* sent during a FML handshake. Therefore, this will only contain
* information for a user that has attempted joined a Forge server.
* <p>
* Consumers of this API should be aware that an empty mod list does
* <em>not</em> indicate that a user is not a Forge user, and so should not
* use this API to check for this - there is no way to tell this reliably.
* </p>
* <p>
* Calling this when handling a
* {@link net.md_5.bungee.api.event.ServerConnectedEvent} may be the best
* place to do so as this event occurs after a FML handshake has completed,
* if any has occurred.
* </p>
*
* @return A {@link Map} of mods, where the key is the name of the mod, and
* the value is the version. Returns an empty list if the FML handshake has
* not occurred for this {@link ProxiedPlayer} yet.
*/
Map<String, String> getModList();
} }

View File

@ -31,18 +31,30 @@ public abstract class DefinedPacket
return new String( b, Charsets.UTF_8 ); return new String( b, Charsets.UTF_8 );
} }
public static void writeArrayLegacy(byte[] b, ByteBuf buf) public static void writeArrayLegacy(byte[] b, ByteBuf buf, boolean allowExtended)
{
// (Integer.MAX_VALUE & 0x1FFF9A ) = 2097050 - Forge's current upper limit
if ( allowExtended )
{
Preconditions.checkArgument( b.length <= ( Integer.MAX_VALUE & 0x1FFF9A ), "Cannot send array longer than 2097050 (got %s bytes)", b.length );
} else
{ {
Preconditions.checkArgument( b.length <= Short.MAX_VALUE, "Cannot send array longer than Short.MAX_VALUE (got %s bytes)", b.length ); Preconditions.checkArgument( b.length <= Short.MAX_VALUE, "Cannot send array longer than Short.MAX_VALUE (got %s bytes)", b.length );
}
buf.writeShort( b.length ); // Write a 2 or 3 byte number that represents the length of the packet. (3 byte "shorts" for Forge only)
// No vanilla packet should give a 3 byte packet, this method will still retain vanilla behaviour.
writeVarShort( buf, b.length );
buf.writeBytes( b ); buf.writeBytes( b );
} }
public static byte[] readArrayLegacy(ByteBuf buf) public static byte[] readArrayLegacy(ByteBuf buf)
{ {
short len = buf.readShort(); // Read in a 2 or 3 byte number that represents the length of the packet. (3 byte "shorts" for Forge only)
Preconditions.checkArgument( len <= Short.MAX_VALUE, "Cannot receive array longer than Short.MAX_VALUE (got %s bytes)", len ); // No vanilla packet should give a 3 byte packet, this method will still retain vanilla behaviour.
int len = readVarShort( buf );
// (Integer.MAX_VALUE & 0x1FFF9A ) = 2097050 - Forge's current upper limit
Preconditions.checkArgument( len <= ( Integer.MAX_VALUE & 0x1FFF9A ), "Cannot receive array longer than 2097050 (got %s bytes)", len );
byte[] ret = new byte[ len ]; byte[] ret = new byte[ len ];
buf.readBytes( ret ); buf.readBytes( ret );
@ -83,6 +95,11 @@ public abstract class DefinedPacket
} }
public static int readVarInt(ByteBuf input) public static int readVarInt(ByteBuf input)
{
return readVarInt( input, 5 );
}
public static int readVarInt(ByteBuf input, int maxBytes)
{ {
int out = 0; int out = 0;
int bytes = 0; int bytes = 0;
@ -93,7 +110,7 @@ public abstract class DefinedPacket
out |= ( in & 0x7F ) << ( bytes++ * 7 ); out |= ( in & 0x7F ) << ( bytes++ * 7 );
if ( bytes > 5 ) if ( bytes > maxBytes )
{ {
throw new RuntimeException( "VarInt too big" ); throw new RuntimeException( "VarInt too big" );
} }
@ -129,6 +146,33 @@ public abstract class DefinedPacket
} }
} }
public static int readVarShort(ByteBuf buf)
{
int low = buf.readUnsignedShort();
int high = 0;
if ( ( low & 0x8000 ) != 0 )
{
low = low & 0x7FFF;
high = buf.readUnsignedByte();
}
return ( ( high & 0xFF ) << 15 ) | low;
}
public static void writeVarShort(ByteBuf buf, int toWrite)
{
int low = toWrite & 0x7FFF;
int high = ( toWrite & 0x7F8000 ) >> 15;
if ( high != 0 )
{
low = low | 0x8000;
}
buf.writeShort( low );
if ( high != 0 )
{
buf.writeByte( high );
}
}
public static void writeUUID(UUID value, ByteBuf output) public static void writeUUID(UUID value, ByteBuf output)
{ {
output.writeLong( value.getMostSignificantBits() ); output.writeLong( value.getMostSignificantBits() );

View File

@ -41,8 +41,8 @@ public class EncryptionRequest extends DefinedPacket
writeString( serverId, buf ); writeString( serverId, buf );
if ( protocolVersion < ProtocolConstants.MINECRAFT_SNAPSHOT ) if ( protocolVersion < ProtocolConstants.MINECRAFT_SNAPSHOT )
{ {
writeArrayLegacy( publicKey, buf ); writeArrayLegacy( publicKey, buf, false );
writeArrayLegacy( verifyToken, buf ); writeArrayLegacy( verifyToken, buf, false );
} else } else
{ {
writeArray( publicKey, buf ); writeArray( publicKey, buf );

View File

@ -38,8 +38,8 @@ public class EncryptionResponse extends DefinedPacket
{ {
if ( protocolVersion < ProtocolConstants.MINECRAFT_SNAPSHOT ) if ( protocolVersion < ProtocolConstants.MINECRAFT_SNAPSHOT )
{ {
writeArrayLegacy( sharedSecret, buf ); writeArrayLegacy( sharedSecret, buf, false );
writeArrayLegacy( verifyToken, buf ); writeArrayLegacy( verifyToken, buf, false );
} else } else
{ {
writeArray( sharedSecret, buf ); writeArray( sharedSecret, buf );

View File

@ -24,6 +24,11 @@ public class PluginMessage extends DefinedPacket
private String tag; private String tag;
private byte[] data; private byte[] data;
/**
* Allow this packet to be sent as an "extended" packet.
*/
private boolean allowExtendedPacket = false;
@Override @Override
public void read(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) public void read(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion)
{ {
@ -44,7 +49,7 @@ public class PluginMessage extends DefinedPacket
writeString( tag, buf ); writeString( tag, buf );
if ( protocolVersion < ProtocolConstants.MINECRAFT_SNAPSHOT ) if ( protocolVersion < ProtocolConstants.MINECRAFT_SNAPSHOT )
{ {
writeArrayLegacy( data, buf ); writeArrayLegacy( data, buf, allowExtendedPacket );
} else } else
{ {
buf.writeBytes( data ); buf.writeBytes( data );

View File

@ -69,6 +69,7 @@ import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.plugin.PluginManager; import net.md_5.bungee.api.plugin.PluginManager;
import net.md_5.bungee.command.*; import net.md_5.bungee.command.*;
import net.md_5.bungee.conf.YamlConfig; import net.md_5.bungee.conf.YamlConfig;
import net.md_5.bungee.forge.ForgeConstants;
import net.md_5.bungee.log.LoggingOutputStream; import net.md_5.bungee.log.LoggingOutputStream;
import net.md_5.bungee.netty.PipelineUtils; import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.DefinedPacket; import net.md_5.bungee.protocol.DefinedPacket;
@ -229,6 +230,10 @@ public class BungeeCord extends ProxyServer
pluginManager.loadPlugins(); pluginManager.loadPlugins();
config.load(); config.load();
registerChannel( ForgeConstants.FML_TAG );
registerChannel( ForgeConstants.FML_HANDSHAKE_TAG );
registerChannel( ForgeConstants.FORGE_REGISTER );
isRunning = true; isRunning = true;
pluginManager.enablePlugins(); pluginManager.enablePlugins();
@ -538,7 +543,7 @@ public class BungeeCord extends ProxyServer
public PluginMessage registerChannels() public PluginMessage registerChannels()
{ {
return new PluginMessage( "REGISTER", Util.format( pluginChannels, "\00" ).getBytes( Charsets.UTF_8 ) ); return new PluginMessage( "REGISTER", Util.format( pluginChannels, "\00" ).getBytes( Charsets.UTF_8 ), false );
} }
@Override @Override

View File

@ -110,7 +110,7 @@ public class BungeeServerInfo implements ServerInfo
return true; return true;
} else if ( queue ) } else if ( queue )
{ {
packetQueue.add( new PluginMessage( channel, data ) ); packetQueue.add( new PluginMessage( channel, data, false ) );
} }
return false; return false;
} }

View File

@ -1,8 +1,8 @@
package net.md_5.bungee; package net.md_5.bungee;
import net.md_5.bungee.protocol.packet.Respawn;
import net.md_5.bungee.protocol.packet.ClientStatus; import net.md_5.bungee.protocol.packet.ClientStatus;
import net.md_5.bungee.protocol.packet.PluginMessage; import net.md_5.bungee.protocol.packet.PluginMessage;
import net.md_5.bungee.protocol.packet.Respawn;
public class PacketConstants public class PacketConstants
{ {
@ -13,6 +13,6 @@ public class PacketConstants
public static final PluginMessage FORGE_MOD_REQUEST = new PluginMessage( "FML", new byte[] public static final PluginMessage FORGE_MOD_REQUEST = new PluginMessage( "FML", new byte[]
{ {
0, 0, 0, 0, 0, 2 0, 0, 0, 0, 0, 2
} ); }, false );
public static final PluginMessage I_AM_BUNGEE = new PluginMessage( "BungeeCord", new byte[ 0 ] ); public static final PluginMessage I_AM_BUNGEE = new PluginMessage( "BungeeCord", new byte[ 0 ], false );
} }

View File

@ -25,6 +25,9 @@ public class ServerConnection implements Server
@Getter @Getter
@Setter @Setter
private boolean isObsolete; private boolean isObsolete;
@Getter
private final boolean forgeServer = false;
private final Unsafe unsafe = new Unsafe() private final Unsafe unsafe = new Unsafe()
{ {
@Override @Override
@ -37,7 +40,7 @@ public class ServerConnection implements Server
@Override @Override
public void sendData(String channel, byte[] data) public void sendData(String channel, byte[] data)
{ {
unsafe().sendPacket( new PluginMessage( channel, data ) ); unsafe().sendPacket( new PluginMessage( channel, data, forgeServer ) );
} }
@Override @Override

View File

@ -5,6 +5,8 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import java.util.Objects; import java.util.Objects;
import java.util.Queue; import java.util.Queue;
import java.util.Set;
import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ProxyServer;
@ -19,6 +21,9 @@ import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.CancelSendSignal; import net.md_5.bungee.connection.CancelSendSignal;
import net.md_5.bungee.connection.DownstreamBridge; import net.md_5.bungee.connection.DownstreamBridge;
import net.md_5.bungee.connection.LoginResult; import net.md_5.bungee.connection.LoginResult;
import net.md_5.bungee.forge.ForgeConstants;
import net.md_5.bungee.forge.ForgeServerHandler;
import net.md_5.bungee.forge.ForgeUtils;
import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss; import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketHandler; import net.md_5.bungee.netty.PacketHandler;
@ -45,6 +50,8 @@ public class ServerConnector extends PacketHandler
private final UserConnection user; private final UserConnection user;
private final BungeeServerInfo target; private final BungeeServerInfo target;
private State thisState = State.LOGIN_SUCCESS; private State thisState = State.LOGIN_SUCCESS;
@Getter
private ForgeServerHandler handshakeHandler;
private enum State private enum State
{ {
@ -70,6 +77,7 @@ public class ServerConnector extends PacketHandler
{ {
this.ch = channel; this.ch = channel;
this.handshakeHandler = new ForgeServerHandler( user, ch, target );
Handshake originalHandshake = user.getPendingConnection().getHandshake(); Handshake originalHandshake = user.getPendingConnection().getHandshake();
Handshake copiedHandshake = new Handshake( originalHandshake.getProtocolVersion(), originalHandshake.getHost(), originalHandshake.getPort(), 2 ); Handshake copiedHandshake = new Handshake( originalHandshake.getProtocolVersion(), originalHandshake.getHost(), originalHandshake.getPort(), 2 );
@ -102,6 +110,10 @@ public class ServerConnector extends PacketHandler
Preconditions.checkState( thisState == State.LOGIN_SUCCESS, "Not expecting LOGIN_SUCCESS" ); Preconditions.checkState( thisState == State.LOGIN_SUCCESS, "Not expecting LOGIN_SUCCESS" );
ch.setProtocol( Protocol.GAME ); ch.setProtocol( Protocol.GAME );
thisState = State.LOGIN; thisState = State.LOGIN;
if ( user.getServer() != null && user.getForgeClientHandler().isHandshakeComplete() )
{
user.getForgeClientHandler().resetHandshake();
}
throw CancelSendSignal.INSTANCE; throw CancelSendSignal.INSTANCE;
} }
@ -142,6 +154,11 @@ public class ServerConnector extends PacketHandler
ch.write( user.getSettings() ); ch.write( user.getSettings() );
} }
if ( user.getForgeClientHandler().getClientModList() == null && !user.getForgeClientHandler().isHandshakeComplete() ) // Vanilla
{
user.getForgeClientHandler().setHandshakeComplete();
}
if ( user.getServer() == null ) if ( user.getServer() == null )
{ {
// Once again, first connection // Once again, first connection
@ -158,12 +175,12 @@ public class ServerConnector extends PacketHandler
{ {
MinecraftOutput out = new MinecraftOutput(); MinecraftOutput out = new MinecraftOutput();
out.writeStringUTF8WithoutLengthHeaderBecauseDinnerboneStuffedUpTheMCBrandPacket( ProxyServer.getInstance().getName() + " (" + ProxyServer.getInstance().getVersion() + ")" ); out.writeStringUTF8WithoutLengthHeaderBecauseDinnerboneStuffedUpTheMCBrandPacket( ProxyServer.getInstance().getName() + " (" + ProxyServer.getInstance().getVersion() + ")" );
user.unsafe().sendPacket( new PluginMessage( "MC|Brand", out.toArray() ) ); user.unsafe().sendPacket( new PluginMessage( "MC|Brand", out.toArray(), handshakeHandler.isServerForge() ) );
} else } else
{ {
ByteBuf brand = ByteBufAllocator.DEFAULT.heapBuffer(); ByteBuf brand = ByteBufAllocator.DEFAULT.heapBuffer();
DefinedPacket.writeString( bungee.getName() + " (" + bungee.getVersion() + ")", brand ); DefinedPacket.writeString( bungee.getName() + " (" + bungee.getVersion() + ")", brand );
user.unsafe().sendPacket( new PluginMessage( "MC|Brand", brand.array().clone() ) ); user.unsafe().sendPacket( new PluginMessage( "MC|Brand", brand.array().clone(), handshakeHandler.isServerForge() ) );
brand.release(); brand.release();
} }
} else } else
@ -249,6 +266,49 @@ public class ServerConnector extends PacketHandler
throw CancelSendSignal.INSTANCE; throw CancelSendSignal.INSTANCE;
} }
@Override
public void handle(PluginMessage pluginMessage) throws Exception
{
if ( pluginMessage.getTag().equals( ForgeConstants.FML_REGISTER ) )
{
Set<String> channels = ForgeUtils.readRegisteredChannels( pluginMessage );
boolean isForgeServer = false;
for ( String channel : channels )
{
if ( channel.equals( ForgeConstants.FML_HANDSHAKE_TAG ) )
{
isForgeServer = true;
break;
}
}
if ( isForgeServer && !this.handshakeHandler.isServerForge() )
{
// We now set the server-side handshake handler for the client to this.
handshakeHandler.setServerAsForgeServer();
user.setForgeServerHandler( handshakeHandler );
}
}
if ( pluginMessage.getTag().equals( ForgeConstants.FML_HANDSHAKE_TAG ) || pluginMessage.getTag().equals( ForgeConstants.FORGE_REGISTER ) )
{
this.handshakeHandler.handle( pluginMessage );
if ( user.getForgeClientHandler().checkUserOutdated() )
{
ch.close();
user.getPendingConnects().remove( target );
}
// We send the message as part of the handler, so don't send it here.
throw CancelSendSignal.INSTANCE;
} else
{
// We have to forward these to the user, especially with Forge as stuff might break
// This includes any REGISTER messages we intercepted earlier.
user.unsafe().sendPacket( pluginMessage );
}
}
@Override @Override
public String toString() public String toString()
{ {

View File

@ -1,6 +1,7 @@
package net.md_5.bungee; package net.md_5.bungee;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFuture;
@ -13,6 +14,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -34,6 +36,8 @@ import net.md_5.bungee.api.score.Scoreboard;
import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.entitymap.EntityMap; import net.md_5.bungee.entitymap.EntityMap;
import net.md_5.bungee.forge.ForgeClientHandler;
import net.md_5.bungee.forge.ForgeServerHandler;
import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss; import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils; import net.md_5.bungee.netty.PipelineUtils;
@ -117,6 +121,13 @@ public final class UserConnection implements ProxiedPlayer
private EntityMap entityRewrite; private EntityMap entityRewrite;
private Locale locale; private Locale locale;
/*========================================================================*/ /*========================================================================*/
@Getter
@Setter
private ForgeClientHandler forgeClientHandler;
@Getter
@Setter
private ForgeServerHandler forgeServerHandler;
/*========================================================================*/
private final Unsafe unsafe = new Unsafe() private final Unsafe unsafe = new Unsafe()
{ {
@Override @Override
@ -152,6 +163,8 @@ public final class UserConnection implements ProxiedPlayer
{ {
addGroups( s ); addGroups( s );
} }
forgeClientHandler = new ForgeClientHandler( this );
} }
public void sendPacket(PacketWrapper packet) public void sendPacket(PacketWrapper packet)
@ -369,7 +382,7 @@ public final class UserConnection implements ProxiedPlayer
@Override @Override
public void sendData(String channel, byte[] data) public void sendData(String channel, byte[] data)
{ {
unsafe().sendPacket( new PluginMessage( channel, data ) ); unsafe().sendPacket( new PluginMessage( channel, data, forgeClientHandler.isForgeUser() ) );
} }
@Override @Override
@ -470,6 +483,19 @@ public final class UserConnection implements ProxiedPlayer
return ( locale == null && settings != null ) ? locale = Locale.forLanguageTag( settings.getLocale().replaceAll( "_", "-" ) ) : locale; return ( locale == null && settings != null ) ? locale = Locale.forLanguageTag( settings.getLocale().replaceAll( "_", "-" ) ) : locale;
} }
@Override
public Map<String, String> getModList()
{
if ( forgeClientHandler.getClientModList() == null )
{
// Return an empty map, rather than a null, if the client hasn't got any mods,
// or is yet to complete a handshake.
return ImmutableMap.of();
}
return ImmutableMap.copyOf( forgeClientHandler.getClientModList() );
}
private static final String EMPTY_TEXT = ComponentSerializer.toString( new TextComponent( "" ) ); private static final String EMPTY_TEXT = ComponentSerializer.toString( new TextComponent( "" ) );
@Override @Override

View File

@ -288,9 +288,6 @@ public class InitialHandler extends PacketHandler implements PendingConnection
return; return;
} }
// TODO: Nuuuu Mojang why u do this
// unsafe().sendPacket( PacketConstants.I_AM_BUNGEE );
// unsafe().sendPacket( PacketConstants.FORGE_MOD_REQUEST );
Callback<PreLoginEvent> callback = new Callback<PreLoginEvent>() Callback<PreLoginEvent> callback = new Callback<PreLoginEvent>()
{ {

View File

@ -19,6 +19,7 @@ import net.md_5.bungee.protocol.packet.ClientSettings;
import net.md_5.bungee.protocol.packet.PluginMessage; import net.md_5.bungee.protocol.packet.PluginMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import net.md_5.bungee.forge.ForgeConstants;
import net.md_5.bungee.protocol.ProtocolConstants; import net.md_5.bungee.protocol.ProtocolConstants;
import net.md_5.bungee.protocol.packet.TabCompleteResponse; import net.md_5.bungee.protocol.packet.TabCompleteResponse;
@ -142,6 +143,21 @@ public class UpstreamBridge extends PacketHandler
throw CancelSendSignal.INSTANCE; throw CancelSendSignal.INSTANCE;
} }
// We handle forge handshake messages if forge support is enabled.
if ( pluginMessage.getTag().equals( ForgeConstants.FML_HANDSHAKE_TAG ) )
{
// Let our forge client handler deal with this packet.
con.getForgeClientHandler().handle( pluginMessage );
throw CancelSendSignal.INSTANCE;
}
if ( con.getServer() != null && !con.getServer().isForgeServer() && pluginMessage.getData().length > Short.MAX_VALUE )
{
// Drop the packet if the server is not a Forge server and the message was > 32kiB (as suggested by @jk-5)
// Do this AFTER the mod list, so we get that even if the intial server isn't modded.
throw CancelSendSignal.INSTANCE;
}
PluginMessageEvent event = new PluginMessageEvent( con, con.getServer(), pluginMessage.getTag(), pluginMessage.getData().clone() ); PluginMessageEvent event = new PluginMessageEvent( con, con.getServer(), pluginMessage.getTag(), pluginMessage.getData().clone() );
if ( bungee.getPluginManager().callEvent( event ).isCancelled() ) if ( bungee.getPluginManager().callEvent( event ).isCancelled() )
{ {

View File

@ -0,0 +1,177 @@
package net.md_5.bungee.forge;
import java.util.ArrayDeque;
import java.util.Map;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.protocol.packet.PluginMessage;
/**
* Handles the Forge Client data and handshake procedure.
*/
@RequiredArgsConstructor
public class ForgeClientHandler
{
@NonNull
private final UserConnection con;
@Getter
@Setter(AccessLevel.PACKAGE)
private boolean forgeOutdated = false;
/**
* The users' mod list.
*/
@Getter
@Setter(AccessLevel.PACKAGE)
private Map<String, String> clientModList = null;
private final ArrayDeque<PluginMessage> packetQueue = new ArrayDeque<PluginMessage>();
@NonNull
@Setter(AccessLevel.PACKAGE)
private ForgeClientHandshakeState state = ForgeClientHandshakeState.HELLO;
private PluginMessage serverModList = null;
private PluginMessage serverIdList = null;
/**
* Handles the Forge packet.
*
* @param message The Forge Handshake packet to handle.
*/
public void handle(PluginMessage message) throws IllegalArgumentException
{
if ( !message.getTag().equalsIgnoreCase( ForgeConstants.FML_HANDSHAKE_TAG ) )
{
throw new IllegalArgumentException( "Expecting a Forge Handshake packet." );
}
message.setAllowExtendedPacket( true ); // FML allows extended packets so this must be enabled
ForgeClientHandshakeState prevState = state;
packetQueue.add( message );
state = state.send( message, con );
if ( state != prevState ) // state finished, send packets
{
synchronized ( packetQueue )
{
while ( !packetQueue.isEmpty() )
{
ForgeLogger.logClient( ForgeLogger.LogDirection.SENDING, prevState.name(), packetQueue.getFirst() );
con.getForgeServerHandler().receive( packetQueue.removeFirst() );
}
}
}
}
/**
* Receives a {@link PluginMessage} from ForgeServer to pass to Client.
*
* @param message The message to being received.
*/
public void receive(PluginMessage message) throws IllegalArgumentException
{
state = state.handle( message, con );
}
/**
* Resets the client handshake state to HELLO, and, if we know the handshake
* has been completed before, send the reset packet.
*/
public void resetHandshake()
{
state = ForgeClientHandshakeState.HELLO;
con.unsafe().sendPacket( ForgeConstants.FML_RESET_HANDSHAKE );
}
/**
* Sends the server mod list to the client, or stores it for sending later.
*
* @param modList The {@link PluginMessage} to send to the client containing
* the mod list.
* @throws IllegalArgumentException Thrown if the {@link PluginMessage} was
* not as expected.
*/
public void setServerModList(PluginMessage modList) throws IllegalArgumentException
{
if ( !modList.getTag().equalsIgnoreCase( ForgeConstants.FML_HANDSHAKE_TAG ) || modList.getData()[0] != 2 )
{
throw new IllegalArgumentException( "modList" );
}
this.serverModList = modList;
}
/**
* Sends the server ID list to the client, or stores it for sending later.
*
* @param idList The {@link PluginMessage} to send to the client containing
* the ID list.
* @throws IllegalArgumentException Thrown if the {@link PluginMessage} was
* not as expected.
*/
public void setServerIdList(PluginMessage idList) throws IllegalArgumentException
{
if ( !idList.getTag().equalsIgnoreCase( ForgeConstants.FML_HANDSHAKE_TAG ) || idList.getData()[0] != 3 )
{
throw new IllegalArgumentException( "idList" );
}
this.serverIdList = idList;
}
/**
* Returns whether the handshake is complete.
*
* @return <code>true</code> if the handshake has been completed.
*/
public boolean isHandshakeComplete()
{
return this.state == ForgeClientHandshakeState.DONE;
}
public void setHandshakeComplete()
{
this.state = ForgeClientHandshakeState.DONE;
}
/**
* Returns whether we know if the user is a forge user.
*
* @return <code>true</code> if the user is a forge user.
*/
public boolean isForgeUser()
{
return clientModList != null;
}
/**
* Checks to see if a user is using an outdated FML build, and takes
* appropriate action on the User side. This should only be called during a
* server connection, by the ServerConnector
*
* @return <code>true</code> if the user's FML build is outdated, otherwise
* <code>false</code>
*/
public boolean checkUserOutdated()
{
if ( forgeOutdated )
{
if ( con.isDimensionChange() )
{
con.disconnect( BungeeCord.getInstance().getTranslation( "connect_kick_outdated_forge" ) );
} else
{
con.sendMessage( BungeeCord.getInstance().getTranslation( "connect_kick_outdated_forge" ) );
}
}
return forgeOutdated;
}
}

View File

@ -0,0 +1,221 @@
package net.md_5.bungee.forge;
import java.util.Map;
import net.md_5.bungee.ServerConnector;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.forge.ForgeLogger.LogDirection;
import net.md_5.bungee.protocol.packet.PluginMessage;
/**
* Handshake sequence manager for the Bungee - Forge Client (Upstream) link.
* Modelled after the Forge implementation. See
* https://github.com/MinecraftForge/FML/blob/master/src/main/java/cpw/mods/fml/common/network/handshake/FMLHandshakeClientState.java
*/
enum ForgeClientHandshakeState implements IForgeClientPacketHandler<ForgeClientHandshakeState>
{
/**
* Initiated at the start of a client handshake. This is a special case
* where we don't want to use a {@link PluginMessage}, we're just sending
* stuff out here.
*
* Transitions into the HELLO state upon completion.
*
* Requires: {@link UserConnection}.
*/
START
{
@Override
public ForgeClientHandshakeState handle(PluginMessage message, UserConnection con)
{
ForgeLogger.logClient( LogDirection.RECEIVED, this.name(), message );
con.unsafe().sendPacket( message );
con.getForgeClientHandler().setState( HELLO );
return HELLO;
}
@Override
public ForgeClientHandshakeState send(PluginMessage message, UserConnection con)
{
return HELLO;
}
},
/**
* Waiting to receive a client HELLO and the mod list. Upon receiving the
* mod list, return the mod list of the server.
*
* We will be stuck in this state if we don't have a forge client. This is
* OK.
*
* Transitions to the WAITINGCACK state upon completion.
*
* Requires:
* {@link PluginMessage}, {@link UserConnection}, {@link ServerConnector}
*/
HELLO
{
@Override
public ForgeClientHandshakeState handle(PluginMessage message, UserConnection con)
{
ForgeLogger.logClient( LogDirection.RECEIVED, this.name(), message );
// Server Hello.
if ( message.getData()[0] == 0 )
{
con.unsafe().sendPacket( message );
}
return this;
}
@Override
public ForgeClientHandshakeState send(PluginMessage message, UserConnection con)
{
// Client Hello.
if ( message.getData()[0] == 1 )
{
return this;
}
// Mod list.
if ( message.getData()[0] == 2 )
{
if ( con.getForgeClientHandler().getClientModList() == null )
{
// This is the first Forge connection - so get the mods now.
// Once we've done it, no point doing it again.
Map<String, String> clientModList = ForgeUtils.readModList( message );
con.getForgeClientHandler().setClientModList( clientModList );
// Get the version from the mod list.
// TODO: Remove this once Bungee becomes 1.8 only.
int buildNumber = ForgeUtils.getFmlBuildNumber( clientModList );
// If we get 0, we're probably using a testing build, so let it though. Otherwise, check the build number.
if ( buildNumber < ForgeConstants.FML_MIN_BUILD_VERSION && buildNumber != 0 )
{
// Mark the user as an old Forge user. This will then cause any Forge ServerConnectors to cancel any
// connections to it.
con.getForgeClientHandler().setForgeOutdated( true );
}
}
return WAITINGSERVERDATA;
}
return this;
}
},
WAITINGSERVERDATA
{
@Override
public ForgeClientHandshakeState handle(PluginMessage message, UserConnection con)
{
ForgeLogger.logClient( ForgeLogger.LogDirection.RECEIVED, this.name(), message );
// Mod list.
if ( message.getData()[0] == 2 )
{
con.unsafe().sendPacket( message );
}
return this;
}
@Override
public ForgeClientHandshakeState send(PluginMessage message, UserConnection con)
{
// ACK
return WAITINGSERVERCOMPLETE;
}
},
WAITINGSERVERCOMPLETE
{
@Override
public ForgeClientHandshakeState handle(PluginMessage message, UserConnection con)
{
ForgeLogger.logClient( ForgeLogger.LogDirection.RECEIVED, this.name(), message );
// Mod ID's.
if ( message.getData()[0] == 3 )
{
con.unsafe().sendPacket( message );
return this;
}
con.unsafe().sendPacket( message ); // pass everything else
return this;
}
@Override
public ForgeClientHandshakeState send(PluginMessage message, UserConnection con)
{
// Send ACK.
return PENDINGCOMPLETE;
}
},
PENDINGCOMPLETE
{
@Override
public ForgeClientHandshakeState handle(PluginMessage message, UserConnection con)
{
// Ack.
if ( message.getData()[0] == -1 )
{
ForgeLogger.logClient( ForgeLogger.LogDirection.RECEIVED, this.name(), message );
con.unsafe().sendPacket( message );
}
return this;
}
@Override
public ForgeClientHandshakeState send(PluginMessage message, UserConnection con)
{
// Send an ACK
return COMPLETE;
}
},
COMPLETE
{
@Override
public ForgeClientHandshakeState handle(PluginMessage message, UserConnection con)
{
// Ack.
if ( message.getData()[0] == -1 )
{
ForgeLogger.logClient( ForgeLogger.LogDirection.RECEIVED, this.name(), message );
con.unsafe().sendPacket( message );
}
return this;
}
@Override
public ForgeClientHandshakeState send(PluginMessage message, UserConnection con)
{
return DONE;
}
},
/**
* Handshake has been completed. Ignores any future handshake packets.
*/
DONE
{
@Override
public ForgeClientHandshakeState handle(PluginMessage message, UserConnection con)
{
ForgeLogger.logClient( ForgeLogger.LogDirection.RECEIVED, this.name(), message );
return this;
}
@Override
public ForgeClientHandshakeState send(PluginMessage message, UserConnection con)
{
return this;
}
}
}

View File

@ -0,0 +1,49 @@
package net.md_5.bungee.forge;
import java.util.regex.Pattern;
import net.md_5.bungee.protocol.packet.PluginMessage;
public class ForgeConstants
{
// Forge
public static final String FORGE_REGISTER = "FORGE";
// FML
public static final String FML_TAG = "FML";
public static final String FML_HANDSHAKE_TAG = "FML|HS";
public static final String FML_REGISTER = "REGISTER";
public static final PluginMessage FML_RESET_HANDSHAKE = new PluginMessage( FML_HANDSHAKE_TAG, new byte[]
{
-2, 0
}, false );
public static final PluginMessage FML_ACK = new PluginMessage( FML_HANDSHAKE_TAG, new byte[]
{
-1, 0
}, false );
public static final PluginMessage FML_START_CLIENT_HANDSHAKE = new PluginMessage( FML_HANDSHAKE_TAG, new byte[]
{
0, 1
}, false );
public static final PluginMessage FML_START_SERVER_HANDSHAKE = new PluginMessage( FML_HANDSHAKE_TAG, new byte[]
{
1, 1
}, false );
public static final PluginMessage FML_EMPTY_MOD_LIST = new PluginMessage( FML_HANDSHAKE_TAG, new byte[]
{
2, 0
}, false );
/**
* The minimum Forge version required to use Forge features. TODO: When the
* FML branch gets pulled, update this number to be the build that includes
* the change.
*/
public static final int FML_MIN_BUILD_VERSION = 1209;
/**
* Regex to use to scrape the version information from a FML handshake.
*/
public static final Pattern FML_HANDSHAKE_VERSION_REGEX = Pattern.compile( "(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)" );
}

View File

@ -0,0 +1,71 @@
package net.md_5.bungee.forge;
import java.util.logging.Level;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.protocol.packet.PluginMessage;
class ForgeLogger
{
static void logServer(LogDirection direction, String stateName, PluginMessage message)
{
String dir = direction == LogDirection.SENDING ? "Server -> Bungee" : "Server <- Bungee";
String log = "[" + stateName + " " + dir + "][" + direction.name() + ": " + getNameFromDiscriminator( message.getTag(), message ) + "]";
BungeeCord.getInstance().getLogger().log( Level.FINE, log );
}
static void logClient(LogDirection direction, String stateName, PluginMessage message)
{
String dir = direction == LogDirection.SENDING ? "Client -> Bungee" : "Client <- Bungee";
String log = "[" + stateName + " " + dir + "][" + direction.name() + ": " + getNameFromDiscriminator( message.getTag(), message ) + "]";
BungeeCord.getInstance().getLogger().log( Level.FINE, log );
}
private static String getNameFromDiscriminator(String channel, PluginMessage message)
{
byte discrim = message.getData()[0];
if ( channel.equals( ForgeConstants.FML_HANDSHAKE_TAG ) )
{
switch ( discrim )
{
case -2:
return "Reset";
case -1:
return "HandshakeAck";
case 0:
return "ServerHello";
case 1:
return "ClientHello";
case 2:
return "ModList";
case 3:
return "ModIdData";
default:
return "Unknown";
}
} else if ( channel.equals( ForgeConstants.FORGE_REGISTER ) )
{
switch ( discrim )
{
case 1:
return "DimensionRegister";
case 2:
return "FluidIdMap";
default:
return "Unknown";
}
}
return "UnknownChannel";
}
public enum LogDirection
{
SENDING,
RECEIVED
}
private ForgeLogger()
{ // Don't allow instantiations
}
}

View File

@ -0,0 +1,85 @@
package net.md_5.bungee.forge;
import java.util.ArrayDeque;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.forge.ForgeLogger.LogDirection;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.PluginMessage;
/**
* Contains data about the Forge server, and handles the handshake.
*/
@RequiredArgsConstructor
public class ForgeServerHandler
{
private final UserConnection con;
@Getter
private final ChannelWrapper ch;
@Getter(AccessLevel.PACKAGE)
private final ServerInfo serverInfo;
@Getter
private ForgeServerHandshakeState state = ForgeServerHandshakeState.START;
@Getter
private boolean serverForge = false;
private final ArrayDeque<PluginMessage> packetQueue = new ArrayDeque<PluginMessage>();
/**
* Handles any {@link PluginMessage} that contains a FML Handshake or Forge
* Register.
*
* @param message The message to handle.
* @throws IllegalArgumentException If the wrong packet is sent down.
*/
public void handle(PluginMessage message) throws IllegalArgumentException
{
if ( !message.getTag().equalsIgnoreCase( ForgeConstants.FML_HANDSHAKE_TAG ) && !message.getTag().equalsIgnoreCase( ForgeConstants.FORGE_REGISTER ) )
{
throw new IllegalArgumentException( "Expecting a Forge REGISTER or FML Handshake packet." );
}
message.setAllowExtendedPacket( true ); // FML allows extended packets so this must be enabled
ForgeServerHandshakeState prevState = state;
packetQueue.add( message );
state = state.send( message, con );
if ( state != prevState ) // send packets
{
synchronized ( packetQueue )
{
while ( !packetQueue.isEmpty() )
{
ForgeLogger.logServer( LogDirection.SENDING, prevState.name(), packetQueue.getFirst() );
con.getForgeClientHandler().receive( packetQueue.removeFirst() );
}
}
}
}
/**
* Receives a {@link PluginMessage} from ForgeClientData to pass to Server.
*
* @param message The message to being received.
*/
public void receive(PluginMessage message) throws IllegalArgumentException
{
state = state.handle( message, ch );
}
/**
* Flags the server as a Forge server. Cannot be used to set a server back
* to vanilla (there would be no need)
*/
public void setServerAsForgeServer()
{
serverForge = true;
}
}

View File

@ -0,0 +1,138 @@
package net.md_5.bungee.forge;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.forge.ForgeLogger.LogDirection;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.PluginMessage;
/**
* Handshake sequence manager for the Bungee - Forge Server (Downstream/Server
* Connector) link. Modelled after the Forge implementation.
*/
public enum ForgeServerHandshakeState implements IForgeServerPacketHandler<ForgeServerHandshakeState>
{
/**
* Start the handshake.
*
*/
START
{
@Override
public ForgeServerHandshakeState handle(PluginMessage message, ChannelWrapper ch)
{
ForgeLogger.logServer( LogDirection.RECEIVED, this.name(), message );
ch.write( message );
return this;
}
@Override
public ForgeServerHandshakeState send(PluginMessage message, UserConnection con)
{
// Send custom channel registration. Send Hello.
return HELLO;
}
},
HELLO
{
@Override
public ForgeServerHandshakeState handle(PluginMessage message, ChannelWrapper ch)
{
ForgeLogger.logServer( LogDirection.RECEIVED, this.name(), message );
if ( message.getData()[0] == 1 ) // Client Hello
{
ch.write( message );
}
if ( message.getData()[0] == 2 ) // Client ModList
{
ch.write( message );
}
return this;
}
@Override
public ForgeServerHandshakeState send(PluginMessage message, UserConnection con)
{
// Send Server Mod List.
return WAITINGCACK;
}
},
WAITINGCACK
{
@Override
public ForgeServerHandshakeState handle(PluginMessage message, ChannelWrapper ch)
{
ForgeLogger.logServer( LogDirection.RECEIVED, this.name(), message );
ch.write( message );
return this;
}
@Override
public ForgeServerHandshakeState send(PluginMessage message, UserConnection con)
{
if ( message.getData()[0] == 3 && message.getTag().equals( ForgeConstants.FML_HANDSHAKE_TAG ) )
{
con.getForgeClientHandler().setServerIdList( message );
return this;
}
if ( message.getData()[0] == -1 && message.getTag().equals( ForgeConstants.FML_HANDSHAKE_TAG ) ) // transition to COMPLETE after sending ACK
{
return this;
}
if ( message.getTag().equals( ForgeConstants.FORGE_REGISTER ) ) // wait for Forge channel registration
{
return COMPLETE;
}
return this;
}
},
COMPLETE
{
@Override
public ForgeServerHandshakeState handle(PluginMessage message, ChannelWrapper ch)
{
// Wait for ACK
ForgeLogger.logServer( LogDirection.RECEIVED, this.name(), message );
ch.write( message );
return this;
}
@Override
public ForgeServerHandshakeState send(PluginMessage message, UserConnection con)
{
// Send ACK
return DONE;
}
},
/**
* Handshake has been completed. Do not respond to any more handshake
* packets.
*/
DONE
{
@Override
public ForgeServerHandshakeState handle(PluginMessage message, ChannelWrapper ch)
{
// RECEIVE 2 ACKS
ForgeLogger.logServer( LogDirection.RECEIVED, this.name(), message );
ch.write( message );
return this;
}
@Override
public ForgeServerHandshakeState send(PluginMessage message, UserConnection con)
{
return this;
}
}
}

View File

@ -0,0 +1,74 @@
package net.md_5.bungee.forge;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.packet.PluginMessage;
public class ForgeUtils
{
/**
* Gets the registered FML channels from the packet.
*
* @param pluginMessage The packet representing FMLProxyPacket.
* @return The registered channels.
*/
public static Set<String> readRegisteredChannels(PluginMessage pluginMessage)
{
String channels = new String( pluginMessage.getData(), Charsets.UTF_8 );
String[] split = channels.split( "\0" );
Set<String> channelSet = ImmutableSet.copyOf( split );
return channelSet;
}
/**
* Gets the modlist from the packet.
*
* @param pluginMessage The packet representing FMLProxyPacket.
* @return The modlist.
*/
public static Map<String, String> readModList(PluginMessage pluginMessage)
{
Map<String, String> modTags = Maps.newHashMap();
ByteBuf payload = Unpooled.wrappedBuffer( pluginMessage.getData() );
byte discriminator = payload.readByte();
if ( discriminator == 2 ) // ModList
{
ByteBuf buffer = payload.slice();
int modCount = DefinedPacket.readVarInt( buffer, 2 );
for ( int i = 0; i < modCount; i++ )
{
modTags.put( DefinedPacket.readString( buffer ), DefinedPacket.readString( buffer ) );
}
}
return modTags;
}
/**
* Get the build number of FML from the packet.
*
* @param modList The modlist, as a Map.
* @return The build number, or 0 if it failed.
*/
public static int getFmlBuildNumber(Map<String, String> modList)
{
if ( modList.containsKey( "FML" ) )
{
Matcher matcher = ForgeConstants.FML_HANDSHAKE_VERSION_REGEX.matcher( modList.get( "FML" ) );
if ( matcher.find() )
{
// We know from the regex that we have an int.
return Integer.parseInt( matcher.group( 4 ) );
}
}
return 0;
}
}

View File

@ -0,0 +1,31 @@
package net.md_5.bungee.forge;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.protocol.packet.PluginMessage;
/**
* An interface that defines a Forge Handshake Client packet.
*
* @param <S> The State to transition to.
*/
public interface IForgeClientPacketHandler<S>
{
/**
* Handles any {@link PluginMessage} packets.
*
* @param message The {@link PluginMessage} to handle.
* @param con The {@link UserConnection} to send packets to.
* @return The state to transition to.
*/
public S handle(PluginMessage message, UserConnection con);
/**
* Sends any {@link PluginMessage} packets.
*
* @param message The {@link PluginMessage} to send.
* @param con The {@link UserConnection} to set data.
* @return The state to transition to.
*/
public S send(PluginMessage message, UserConnection con);
}

View File

@ -0,0 +1,36 @@
package net.md_5.bungee.forge;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.PluginMessage;
/**
* An interface that defines a Forge Handshake Server packet.
*
* @param <S> The State to transition to.
*/
public interface IForgeServerPacketHandler<S>
{
/**
* Handles any {@link net.md_5.bungee.protocol.packet.PluginMessage}
* packets.
*
* @param message The {@link net.md_5.bungee.protocol.packet.PluginMessage}
* to handle.
* @param ch The {@link ChannelWrapper} to send packets to.
* @return The state to transition to.
*/
public S handle(PluginMessage message, ChannelWrapper ch);
/**
* Sends any {@link net.md_5.bungee.protocol.packet.PluginMessage} packets.
*
* @param message The {@link net.md_5.bungee.protocol.packet.PluginMessage}
* to send.
* @param con The {@link net.md_5.bungee.UserConnection} to send packets to
* or read from.
* @return The state to transition to.
*/
public S send(PluginMessage message, UserConnection con);
}

View File

@ -3,6 +3,7 @@ already_connected=\u00a7cYou are already connected to this server!
already_connecting=\u00a7cAlready connecting to this server! already_connecting=\u00a7cAlready connecting to this server!
command_list=\u00a7a[{0}] \u00a7e({1}): \u00a7r{2} command_list=\u00a7a[{0}] \u00a7e({1}): \u00a7r{2}
connect_kick=\u00a7cKicked whilst connecting to {0}: {1} connect_kick=\u00a7cKicked whilst connecting to {0}: {1}
connect_kick_outdated_forge=\u00a7cYour version of Forge is outdated. Please update Forge and try again.
current_server=\u00a76You are currently connected to {0}. current_server=\u00a76You are currently connected to {0}.
fallback_kick=\u00a7cCould not connect to default or fallback server, please try again later: {0} fallback_kick=\u00a7cCould not connect to default or fallback server, please try again later: {0}
fallback_lobby=\u00a7cCould not connect to target server, you have been moved to the fallback server. fallback_lobby=\u00a7cCould not connect to target server, you have been moved to the fallback server.