Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add forge (1.8 and 1.7.10) support #100

Merged
merged 10 commits into from Oct 25, 2015
Merged

Add forge (1.8 and 1.7.10) support #100

merged 10 commits into from Oct 25, 2015

Conversation

@Pokechu22
Copy link
Contributor

Pokechu22 commented Oct 25, 2015

Fixes #88 and fixes #9 (although I can't seem to view #9 right now).

Connection to most modded servers running forge 1.8 and forge 1.7.10 should now be possible!

Tested with a few simple servers (a 1.8 server running rpcraft and a 1.7.10 cauldron server running a few random mods I had when I was troubleshooting a WDL issue). Also tested with a Feed The Beast infinity server, and it works, so it probably should work with most sets of mods.

The one issue is that mod i18n isn't complete, so you get things like this:

>/givesample
[gendustry.givesample.usage]

But that doesn't seem too important or fixable at this time.


Here's a bunch of technical details and how I figured it out:

First off, to detect whether the server is forge:

For the server pinger, the JSON will change.

{"description":"A Minecraft Server","players":{"max":20,"online":0},"version":{"name":"1.8","protocol":47},"modinfo":{"type":"FML","modList":[{"modid":"mcp","version":"9.05"},{"modid":"FML","version":"8.0.99.99"},{"modid":"Forge","version":"11.14.3.1512"},{"modid":"rpcraft","version":"Beta 1.3 - 1.8.0"}]}}

This is injected in ServerStatusResponse by calling FMLNetworkHandler.enhanceStatusQuery().

The presence of the modinfo tag (and type being FML) indicates that it's a forge server. In addition, you've got the mod list and their versions right there, so a response to the kick message won't be needed. Easy enough.

Now, to connect to a forge server:

A search for the "This server requires FML/Forge to be installed. Contact your server admin for more details." message allows us to find how FML kicks the player. It occurs in a changed 0x00 Handshake packet, updaged here.

It looks like all that needs to be done is changing the server's IP – \0FML\0 to the end. Suprisingly, that seems to work for joining in. The client can now send chat messages.

But, unfortunately, it can't receive them.

Also, the server is kind enough to have extreme packet debug logging turned on, and every second or so this appears in the log:

[22:28:09] [Netty Server IO #5/DEBUG]: OUT: [PLAY:0] net.minecraft.network.play.server.S00PacketKeepAlive
[22:28:09] [Netty Server IO #5/DEBUG]:  IN: [PLAY:0] net.minecraft.network.play.client.C00PacketKeepAlive
[22:28:09] [Netty Server IO #5/INFO]: Unexpected packet during modded negotiation - assuming vanilla or keepalives : net.minecraft.network.play.client.C00PacketKeepAlive

Then again, it does say "assuming vanilla or keepalives". And it is the latter, but it still seems like a bad sign, as why should we still be "during modded negotiation"?

That error message occurs in FML's NetworkDispatcher. Guess what's right at the top of that? A define that can be used to enable more debuging for the handshake (if passed in via -Dfml.debugNetworkHandshake=true when starting the server).

With that enabled:

[22:43:02] [Netty Server IO #1/DEBUG]: Enabled auto read
[22:43:02] [Netty Server IO #1/DEBUG]: Ping: (1.4-1.5.x) from /0:0:0:0:0:0:0:1:49688
[22:43:03] [Netty Server IO #2/DEBUG]: Set listener of net.minecraft.network.NetworkManager@3cd9b106 to net.minecraft.server.network.NetHandlerHandshakeTCP@38e63245
[22:43:03] [Netty Server IO #2/DEBUG]: Enabled auto read
[22:43:04] [Netty Server IO #2/DEBUG]:  IN: [HANDSHAKING:0] net.minecraft.network.handshake.client.C00Handshake
[22:43:04] [Netty Server IO #2/DEBUG]: Enabled auto read
[22:43:04] [Netty Server IO #2/DEBUG]: Set listener of net.minecraft.network.NetworkManager@3cd9b106 to net.minecraft.server.network.NetHandlerStatusServer@1dcb4472
[22:43:04] [Netty Server IO #2/DEBUG]:  IN: [STATUS:0] net.minecraft.network.status.client.C00PacketServerQuery
[22:43:04] [Netty Server IO #2/DEBUG]: OUT: [STATUS:0] net.minecraft.network.status.server.S00PacketServerInfo
[22:43:04] [Netty Server IO #3/DEBUG]: Set listener of net.minecraft.network.NetworkManager@783dcd07 to net.minecraft.server.network.NetHandlerHandshakeTCP@7c0246d8
[22:43:04] [Netty Server IO #2/DEBUG]: Disconnecting /0:0:0:0:0:0:0:1:49691
java.io.IOException: An existing connection was forcibly closed by the remote host
        at sun.nio.ch.SocketDispatcher.read0(Native Method) ~[?:1.8.0_51]
        at sun.nio.ch.SocketDispatcher.read(Unknown Source) ~[?:1.8.0_51]
        at sun.nio.ch.IOUtil.readIntoNativeBuffer(Unknown Source) ~[?:1.8.0_51]
        at sun.nio.ch.IOUtil.read(Unknown Source) ~[?:1.8.0_51]
        at sun.nio.ch.SocketChannelImpl.read(Unknown Source) ~[?:1.8.0_51]
        at io.netty.buffer.UnpooledUnsafeDirectByteBuf.setBytes(UnpooledUnsafeDirectByteBuf.java:446) ~[UnpooledUnsafeDirectByteBuf.class:4.0.15.Final]
        at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:871) ~[AbstractByteBuf.class:4.0.15.Final]
        at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:208) ~[NioSocketChannel.class:4.0.15.Final]
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:124) [AbstractNioByteChannel$NioByteUnsafe.class:4.0.15.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:485) [NioEventLoop.class:4.0.15.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:452) [NioEventLoop.class:4.0.15.Final]
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:346) [NioEventLoop.class:4.0.15.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:101) [SingleThreadEventExecutor$2.class:4.0.15.Final]
        at java.lang.Thread.run(Unknown Source) [?:1.8.0_51]
[22:43:04] [Netty Server IO #3/DEBUG]: Enabled auto read
[22:43:04] [Netty Server IO #3/DEBUG]:  IN: [HANDSHAKING:0] net.minecraft.network.handshake.client.C00Handshake
[22:43:04] [Netty Server IO #3/DEBUG]: Enabled auto read
[22:43:04] [Netty Server IO #3/DEBUG]: Set listener of net.minecraft.network.NetworkManager@783dcd07 to net.minecraft.server.network.NetHandlerLoginServer@66293aad
[22:43:04] [Netty Server IO #3/DEBUG]:  IN: [LOGIN:0] net.minecraft.network.login.client.C00PacketLoginStart
[22:43:04] [Netty Server IO #3/DEBUG]: OUT: [LOGIN:1] net.minecraft.network.login.server.S01PacketEncryptionRequest
[22:43:05] [Netty Server IO #3/DEBUG]:  IN: [LOGIN:1] net.minecraft.network.login.client.C01PacketEncryptionResponse
[22:43:08] [User Authenticator #1/DEBUG]: Opening connection to https://sessionserver.mojang.com/session/minecraft/hasJoined?serverId=-2cb7703b830b85beb0a18023e22892e3dba4421&username=the_creeper56
[22:43:09] [User Authenticator #1/DEBUG]: Reading data from https://sessionserver.mojang.com/session/minecraft/hasJoined?serverId=-2cb7703b830b85beb0a18023e22892e3dba4421&username=the_creeper56
[22:43:10] [User Authenticator #1/DEBUG]: Successful read, server response was 200
[22:43:10] [User Authenticator #1/DEBUG]: Response: {"id":"0adf5ab4533746e193eed588be43d794","name":"the_creeper56","properties":[{"name":"textures","value":"eyJ0aW1lc3RhbXAiOjE0NDU0OTI1OTEyNTUsInByb2ZpbGVJZCI6IjBhZGY1YWI0NTMzNzQ2ZTE5M2VlZDU4OGJlNDNkNzk0IiwicHJvZmlsZU5hbWUiOiJ0aGVfY3JlZXBlcjU2IiwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzY5ZDJiYWNkZWQ0N2RhY2U3YmJiZDk4ZGY1M2Y3NGM2ZTViNWU3YzQ1M2UzNzY3N2ZhMjlmOGEzZjdhNjhmIn19fQ==","signature":"fXiS6BZ1ZxhqCSGc02j0qgAhChG4hWtCJPKZmHgLRvw46alhgapSmOyGO/Twn1bXjSwC3YETqFxT84Yu5T7q0pvb1vwMlAcmD/MltUuzQlUq6xwO5AUhEuFFTlHeni8+XVjDaNBR1BGkQ/G6WrjursKSRrx0+dOpTv2NaTDNN4f/zP3ZYy2uLtVq7Lt0bq3KBPeOi9rMDAOQTcuuzcXCT0maVFc6fvVbyc7G9e3RDMTGzIqPve11nEPoaPpAdIC6BYzxQdOalPkf73qftH7du6UgN0qQSa/KMAZSOES3ZLcxX1b236tKuGo90cj38unVUH6kKTnhA/ifJYosMRmKXfSYQxsUigqM5UWI2SoKvNF/JXQeJ15qBCA4aP+irRMkogVX8heCYoUJsR1s0JznM1dyCSup/2j4TznSslbaftMoAkszzDJconbESOEnyNf5q0uJzQAIE22ivdzGraZuwFFn9ccgG/5fWF1zZPQQJoMXHQVAHwUt3QS7MsLeUD+UOMIi480Ae5dNsLuX2f+zxV2DR2u4/sJs4cV3dUtqNdTqMoY7BsG/zZ8E7BmxJmnq/zcbQyniMCT27Ln3Uaxc1MR2WEwQi4l3szgaJeJ36LVU9kvCBbynOHM6nM8tSHpK1ja1hzbQKtAupkUFp0TFcpeiWF9RRrG+3rnZjVbcfEA="}],"legacy":true}
[22:43:10] [User Authenticator #1/INFO]: UUID of player the_creeper56 is 0adf5ab4-5337-46e1-93ee-d588be43d794
[22:43:10] [Netty Server IO #3/DEBUG]: OUT: [LOGIN:3] net.minecraft.network.login.server.S03PacketEnableCompression
[22:43:10] [Netty Server IO #3/DEBUG]: OUT: [LOGIN:2] net.minecraft.network.login.server.S02PacketLoginSuccess
[22:43:11] [Netty Server IO #3/DEBUG]: FMLHandshakeServerState: null->FMLHandshakeServerState$1:START
[22:43:11] [Netty Server IO #3/DEBUG]: Set listener of net.minecraft.network.NetworkManager@783dcd07 to net.minecraftforge.fml.common.network.handshake.NetworkDispatcher$1@443a11f4
[22:43:11] [Netty Server IO #3/DEBUG]: Enabled auto read
[22:43:11] [Netty Server IO #3/DEBUG]: OUT: [PLAY:63] net.minecraft.network.play.server.S3FPacketCustomPayload
[22:43:11] [Netty Server IO #3/DEBUG]: OUT: [PLAY:63] net.minecraft.network.play.server.S3FPacketCustomPayload
[22:43:11] [Netty Server IO #3/DEBUG]:   Next: HELLO
[22:43:12] [Netty Server IO #3/DEBUG]:  IN: [PLAY:22] net.minecraft.network.play.client.C16PacketClientStatus
[22:43:12] [Netty Server IO #3/INFO]: Unexpected packet during modded negotiation - assuming vanilla or keepalives : net.minecraft.network.play.client.C16PacketClientStatus
[22:43:13] [Netty Server IO #3/DEBUG]: OUT: [PLAY:0] net.minecraft.network.play.server.S00PacketKeepAlive
[22:43:14] [Netty Server IO #3/DEBUG]:  IN: [PLAY:0] net.minecraft.network.play.client.C00PacketKeepAlive
[22:43:14] [Netty Server IO #3/INFO]: Unexpected packet during modded negotiation - assuming vanilla or keepalives : net.minecraft.network.play.client.C00PacketKeepAlive
...

Note the "FMLHandshakeServerState: null->FMLHandshakeServerState$1:START" and the "Next: HELLO". Well, FMLHandshakeServerState looks useful. But there's also the client version: FMLHandshakeServerState. And that has a big comment on the top:

/**
 * Packet handshake sequence manager- client side (responding to remote server)
 *
 * Flow:
 * 1. Wait for server hello. (START). Move to HELLO state.
 * 2. Receive Server Hello. Send customchannel registration. Send Client Hello. Send our modlist. Move to WAITINGFORSERVERDATA state.
 * 3. Receive server modlist. Send ack if acceptable, else send nack and exit error. Receive server IDs. Move to COMPLETE state. Send ack.
 *
 * @author cpw
 *
 */
enum FMLHandshakeClientState implements IHandshakeState<FMLHandshakeClientState>
{
...

Additionally, FMLHandshakeMessage and FMLHandshakeCodec provide packet format and discriminator byte information, respectively.

What's a discriminator byte? It seems to be something used so that different 0x3f Plugin Message packets can be sent on the same channel — a single byte at the start allows the code to chose which of several packets is registered. I use something similar in WDL except with ints (and it being manually switched rather than having an automatic registration system). So, here's all of the packets (for documentation's sake):

FML|HS (ForgeModLoader - Handshake):

0 - ServerHello:

  • 1 byte for the FML protocol (described in NetworkRegistry). We can probably ignore this.
  • 1 int (4 bytes) for the custom dimension, if protocol > 1. We can ignore this in MCC. This is a 4-byte value, not a varint.

1 - ClientHello:

  • 1 byte, for the client protocol version. This is labeled as serverProtocolVersion, but seems to be the value on the client. For MCC, we'll just resend what the server sent.

2 - ModList

  • 1 varint, for the number of mods installed on the client.
  • Array of 2 UTF-8 strings (length prefixed), with an array length of the number of mods.
    • The first string is the mod name.
    • The second string is the mod version.

3 - RegistryData

  • 1 boolean, for whether there will be another RegistryData after this one.
  • 1 string, the name of this registry
  • 1 varint, which is the length of the following data.
  • A series of string / varint pairs, which is the registry string-id mapping.
  • Another varint, which is the length of the next data.
  • A series of strings, which are substitutions, which doesn't really matter to MCC.

-1 - HandshakeAck

  • 1 byte, for the phase. This seems to be the index in the FMLHandshakeClientState / FMLHandshakeServerState enum.

-2 - HandshakeReset

  • No payload whatsoever. When received by the client, the handshake must be redone. Does not appear to ever be sent. MCC will still handle it.

Here's the connection process:

  1. Complete the normal client login process, up to and including receiving 0x02 Login Success, but no further.
  2. Server registers the plugin channels registration it uses (FML|HS, FML, FML|MP, FML, FORGE).
  3. Server sends a FML|HS ServerHello packet, including its version.
  4. Server registers the plugin channels registration it uses (FML|HS, FML, FML|MP, FML, FORGE).
  5. Client sends a FML|HS ClientHello packet, including its version.
  6. Client sends a FML|HS ModList packet containing its mod list.
  7. Server checks compatibility of the client's mods against its own mod list.
    • If it's invalid, the player will be disconnected with a reason explaining why (and listing needed mods).
    • If the client's modlist is valid, the server sends a FML|HS ModList packet containing its mod list.
  8. Client checks compatibility of the server's mods against its own mod list.
    • If it's invalid, it will disconnect.
    • If the server's modlist is valid, the client sends a FML|HS HandshakeAck packet with the phase being 2 (WAITINGSERVERDATA).
  9. If the connection is not local (I assume this means the integrated server):
    • The server sends all of its registries individually in a series of FML|HS RegistryData packets.
      • If the hasNext parameter is true, continue storing the registry values.
      • If the hasNext parameter is false, validate that the client registries are not missing any values. If things are missing, disconnect. Else, send a FML|HS HandshakeAck packet with the phase being 3.
  10. The server sends a FML|HS HandshakeAck with the phase being 2.
  11. The client sends a FML|HS HandshakeAck with the phase being 4.
  12. The server sends a FML|HS HandshakeAck with the phase being 3.
  13. The server sends a FML CompleteHandshake packet to itself with the side being 1 (SERVER). (I do not know why, and this doens't go to the client).
  14. The client sends a FML|HS HandshakeAck with the phase being 5.
  15. The client sends a FML CompleteHandshake packet to itself with the side being 0 (CLIENT). (I do not know why, and this doens't go to the client).
  16. The normal client login process continues from 0x01 Join Game.

Now for 1.7.10. Here's the latest code for it: https://github.com/MinecraftForge/MinecraftForge/tree/605457deecba2144d69113d3c9ce6589021f542b

Sadly, the existing code doesn't work:

[16:16:54] [Netty IO #1/DEBUG]: Set listener of net.minecraft.network.NetworkManager@765acd4b to net.minecraft.server.network.NetHandlerHandshakeTCP@19dc1618
[16:16:54] [Netty IO #1/DEBUG]: Enabled auto read
[16:16:54] [Netty IO #1/DEBUG]: Ping: (1.4-1.5.x) from /0:0:0:0:0:0:0:1:56919
[16:16:54] [Netty IO #2/DEBUG]: Set listener of net.minecraft.network.NetworkManager@5f3181bb to net.minecraft.server.network.NetHandlerHandshakeTCP@5b0e1e85
[16:16:54] [Netty IO #2/DEBUG]: Enabled auto read
[16:16:54] [Netty IO #2/DEBUG]:  IN: [HANDSHAKING:0] net.minecraft.network.handshake.client.C00Handshake[]
[16:16:54] [Netty IO #2/DEBUG]: Enabled auto read
[16:16:54] [Netty IO #2/DEBUG]: Set listener of net.minecraft.network.NetworkManager@5f3181bb to net.minecraft.server.network.NetHandlerStatusServer@7e2374cf
[16:16:54] [Netty IO #2/DEBUG]:  IN: [STATUS:0] net.minecraft.network.status.client.C00PacketServerQuery[]
[16:16:54] [Netty IO #2/DEBUG]: OUT: [STATUS:0] net.minecraft.network.status.server.S00PacketServerInfo[]
[16:16:54] [Netty IO #3/DEBUG]: Set listener of net.minecraft.network.NetworkManager@59ba61b to net.minecraft.server.network.NetHandlerHandshakeTCP@1a42270
[16:16:54] [Netty IO #3/DEBUG]: Enabled auto read
[16:16:54] [Netty IO #3/DEBUG]:  IN: [HANDSHAKING:0] net.minecraft.network.handshake.client.C00Handshake[]
[16:16:54] [Netty IO #3/DEBUG]: Enabled auto read
[16:16:54] [Netty IO #3/DEBUG]: Set listener of net.minecraft.network.NetworkManager@59ba61b to net.minecraft.server.network.NetHandlerLoginServer@553f2244
[16:16:54] [Netty IO #3/DEBUG]:  IN: [LOGIN:0] net.minecraft.network.login.client.C00PacketLoginStart[]
[16:16:54] [Netty IO #3/DEBUG]: OUT: [LOGIN:2] net.minecraft.network.login.server.S02PacketLoginSuccess[]
[16:16:55] [Netty IO #3/DEBUG]: Set listener of net.minecraft.network.NetworkManager@59ba61b to net.minecraft.network.NetHandlerPlayServer@7e147936
[16:16:55] [Netty IO #3/DEBUG]: Enabled auto read
[16:16:55] [Netty IO #3/DEBUG]: OUT: [PLAY:63] net.minecraft.network.play.server.S3FPacketCustomPayload[]
[16:16:55] [Netty IO #3/DEBUG]: OUT: [PLAY:63] net.minecraft.network.play.server.S3FPacketCustomPayload[]
[16:16:55] [Netty IO #3/ERROR]: NetworkDispatcher exception
io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(12) + length(17997) exceeds writerIndex(37): UnpooledHeapByteBuf(ridx: 12, widx: 37, cap: 37)
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:263) ~[ByteToMessageDecoder.class:?]
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:131) ~[ByteToMessageDecoder.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.invokeChannelRead(DefaultChannelHandlerContext.java:337) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:323) [DefaultChannelHandlerContext.class:?]
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:173) [ByteToMessageDecoder.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.invokeChannelRead(DefaultChannelHandlerContext.java:337) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:323) [DefaultChannelHandlerContext.class:?]
        at io.netty.handler.timeout.ReadTimeoutHandler.channelRead(ReadTimeoutHandler.java:149) [ReadTimeoutHandler.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.invokeChannelRead(DefaultChannelHandlerContext.java:337) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:323) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) [ChannelInboundHandlerAdapter.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.invokeChannelRead(DefaultChannelHandlerContext.java:337) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:323) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:785) [DefaultChannelPipeline.class:?]
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:100) [AbstractNioByteChannel$NioByteUnsafe.class:?]
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:480) [NioEventLoop.class:?]
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:447) [NioEventLoop.class:?]
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:341) [NioEventLoop.class:?]
        at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:101) [SingleThreadEventExecutor$2.class:?]
        at java.lang.Thread.run(Unknown Source) [?:1.8.0_51]
Caused by: java.lang.IndexOutOfBoundsException: readerIndex(12) + length(17997) exceeds writerIndex(37): UnpooledHeapByteBuf(ridx: 12, widx: 37, cap: 37)
        at io.netty.buffer.AbstractByteBuf.checkReadableBytes(AbstractByteBuf.java:1160) ~[AbstractByteBuf.class:?]
        at io.netty.buffer.AbstractByteBuf.readBytes(AbstractByteBuf.java:668) ~[AbstractByteBuf.class:?]
        at io.netty.buffer.AbstractByteBuf.readBytes(AbstractByteBuf.java:676) ~[AbstractByteBuf.class:?]
        at net.minecraft.network.PacketBuffer.readBytes(SourceFile:581) ~[et.class:?]
        at net.minecraft.network.play.client.C17PacketCustomPayload.func_148837_a(SourceFile:50) ~[iz.class:?]
        at net.minecraft.util.MessageDeserializer.decode(SourceFile:40) ~[ez.class:?]
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:232) ~[ByteToMessageDecoder.class:?]
        ... 19 more
[16:16:55] [Netty IO #3/ERROR]: NetworkDispatcher exception
io.netty.handler.codec.DecoderException: java.io.IOException: Bad packet id 76
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:263) ~[ByteToMessageDecoder.class:?]
        at io.netty.handler.codec.ByteToMessageDecoder.channelInactive(ByteToMessageDecoder.java:196) ~[ByteToMessageDecoder.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.invokeChannelInactive(DefaultChannelHandlerContext.java:237) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.fireChannelInactive(DefaultChannelHandlerContext.java:223) [DefaultChannelHandlerContext.class:?]
        at io.netty.handler.codec.ByteToMessageDecoder.channelInactive(ByteToMessageDecoder.java:214) [ByteToMessageDecoder.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.invokeChannelInactive(DefaultChannelHandlerContext.java:237) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.fireChannelInactive(DefaultChannelHandlerContext.java:223) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.ChannelInboundHandlerAdapter.channelInactive(ChannelInboundHandlerAdapter.java:75) [ChannelInboundHandlerAdapter.class:?]
        at io.netty.handler.timeout.ReadTimeoutHandler.channelInactive(ReadTimeoutHandler.java:143) [ReadTimeoutHandler.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.invokeChannelInactive(DefaultChannelHandlerContext.java:237) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.fireChannelInactive(DefaultChannelHandlerContext.java:223) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.ChannelInboundHandlerAdapter.channelInactive(ChannelInboundHandlerAdapter.java:75) [ChannelInboundHandlerAdapter.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.invokeChannelInactive(DefaultChannelHandlerContext.java:237) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelHandlerContext.fireChannelInactive(DefaultChannelHandlerContext.java:223) [DefaultChannelHandlerContext.class:?]
        at io.netty.channel.DefaultChannelPipeline.fireChannelInactive(DefaultChannelPipeline.java:767) [DefaultChannelPipeline.class:?]
        at io.netty.channel.AbstractChannel$AbstractUnsafe$5.run(AbstractChannel.java:558) [AbstractChannel$AbstractUnsafe$5.class:?]
        at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:354) [SingleThreadEventExecutor.class:?]
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:348) [NioEventLoop.class:?]
        at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:101) [SingleThreadEventExecutor$2.class:?]
        at java.lang.Thread.run(Unknown Source) [?:1.8.0_51]
Caused by: java.io.IOException: Bad packet id 76
        at net.minecraft.util.MessageDeserializer.decode(SourceFile:37) ~[ez.class:?]
        at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:232) ~[ByteToMessageDecoder.class:?]
        ... 19 more
[16:16:55] [Server thread/INFO]: the_creeper56 lost connection: TranslatableComponent{key='disconnect.genericReason', args=[Internal Exception: io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(12) + length(17997) exceeds writerIndex(37): UnpooledHeapByteBuf(ridx: 12, widx: 37, cap: 37)], siblings=[], style=Style{hasParent=false, color=null, bold=null, italic=null, underlined=null, obfuscated=null, clickEvent=null, hoverEvent=null}}
[16:16:55] [Server thread/INFO]: the_creeper56 left the game

Sadly, there is no handshake debugging in 1.7.10, but it looks like the handshake is mostly the same from the code. There's one difference – only one packet that is the equivilant of "registries" is sent (ModIdData). This is sent once, and the client immediately ack's it, rather than waiting for hasNext to be false.

3 - ModIdData

  • 1 varint, which is the length of the following data.
  • A series of string / varint pairs, which is the string-id mapping.
  • Another varint, which is the length of the next data.
  • A series of strings, which is blockSubstitutions
    • Another varint, which is the length of the next data.
  • A series of strings, which is itemSubstitutions

However, there was another reason this didn't work. 1.7.10's plugin message packet is prefixed with the length, while 1.8's plugin message is not. Once both of those were fixed, it worked fine.

... it still doesn't work with feed the beast though. And that's a bazillion mods.

Wait... they changed the packet again in forge. Evidently, in forge, a "varshort" is used. Here's how they load it (from ByteBufUtils):

    /**
     * An extended length short. Used by custom payload packets to extend size.
     *
     * @param buf
     * @return
     */
    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;
    }

Looks like a short, that when the top bit is set, is actually a 3-byte integer. Weird, but good to know. If I use that method, the client connects properly to FTB servers.

It seems that it didn't find the right discriminator byte and thus wasn't finishing connection, because the length was 1 byte longer than expected. Once that's fixed, everything works.


Pings to the forge devs, @cpw and @LexManos. Does this seem like the right way of connecting a custom client to a forge server?

Pokechu22 added 10 commits Oct 22, 2015
This includes making sure plugin channels have their packet.
Also, it fixes a mistake in #92, where brand info doesn't send
length in 1.7.10 (same channel issue).  Finally, there's only 1
registry sent to the client in 1.7.10.
More percisely, use varshorts for the length of the 3F packet,
as forge makes it longer.  Only really matters if a bazillion
mods are installed, which they are with FTB.
@ORelio

This comment has been minimized.

Copy link
Owner

ORelio commented Oct 25, 2015

Congratulations, after reading your code and your explanations that's truely impressive!
Never thought about getting mod list from server ping response, and great overall RE work :)
That's a very nice way of celebrating the #100th issue/pull request 😄

ORelio added a commit that referenced this pull request Oct 25, 2015
Add forge (1.8 and 1.7.10) support by Pokechu22!
@ORelio ORelio merged commit 29a9fe8 into ORelio:Indev Oct 25, 2015
1 check passed
1 check passed
continuous-integration/appveyor AppVeyor build succeeded
Details
@Aragas

This comment has been minimized.

Copy link
Contributor

Aragas commented Oct 26, 2015

Wow, this is really impressive! Thanks!

@Aragas Aragas mentioned this pull request Oct 26, 2015
@Pokechu22

This comment has been minimized.

Copy link
Contributor Author

Pokechu22 commented Oct 26, 2015

I'm currently working on getting my protocol documentation onto wiki.vg. Here's where I've got it for now. I'll move it to the article space once it's more complete.

@ORelio

This comment has been minimized.

Copy link
Owner

ORelio commented Oct 26, 2015

Excellent initiative, wiki.vg is missing a good Forge documentation and this would help a lot of people :)
Also you might add an entry for MCC in the client li ... wait... you just added it... nevermind :D

ORelio added a commit that referenced this pull request Oct 29, 2015
When no mods are installed, FML client/server will skip mod negociation
phase and act as a vanilla client/server. MCC should do the same else
login will not work properly. See #100 : Forge Support
@ORelio

This comment has been minimized.

Copy link
Owner

ORelio commented Oct 29, 2015

After a few day of testing, received a bug report by a user having a problem on a server running forge but not actually having any mods. In that case, the modinfo node is present in ping response but there is not mods as seen in debug line after ping.

During my tests on #9 I could notice that in this case, a Forge server/client will act as a vanilla server/client and totally skip the FML mod negociation step. This case does not seems to be handled as MCC will wait for FML packets from the server so chat prompt never appears since login phase is not considered as finished. This should be fixed now in the above commit :)

@Pokechu22

This comment has been minimized.

Copy link
Contributor Author

Pokechu22 commented Oct 29, 2015

Good catch! I never tested an empty forge server (instead assuming that it would behave the same as the client, where it always lists MCP, Forge, and FML as installed).

It looks like the way that the mods are added is via a call to Loader.instance().getActiveModList()):

public List<ModContainer> getActiveModList()
{
    return modController != null ? modController.getActiveModList() : ImmutableList.<ModContainer>of();
}

modController is a LoadController... which, when loading mods, calls loader's getModList method... I can't actually seem to figure out where it skips MCP and such, actually. I'll look.

@ORelio

This comment has been minimized.

Copy link
Owner

ORelio commented Oct 29, 2015

Well not sure either, my reverse engineering work was far from being as much thorough as yours.
However when trying to connect without any change to see what was happening, it... worked!
So I installed a mod and then studied handshake and mod negociation. And saw the discriminator byte without really understanding what it was and thinking it was affecting every packet and not just plugin messages. That's why it's cool to see your forge article growing slowly with new details :)
... Is the discriminator byte used only for FML|HS or for all plugin messages?

@Pokechu22

This comment has been minimized.

Copy link
Contributor Author

Pokechu22 commented Oct 30, 2015

... Is the discriminator byte used only for FML|HS or for all plugin messages?

I think that forge uses the concept of discriminator bytes for most of its plugin channels (or at least it's the default), and I think most forge mods also use them (I'm not too familiar with forge's API). But the specific bytes vary between the channel. Anything that uses FMLIndexedMessageToMessageCodec uses them. I think some other forge mods use them, but am not sure.

The main plugin channel specification doesn't use them, although vanilla minecraft (appears to) use something like it in one case — MC|AdvCdm uses a value of 0x00 or 0x01 to change between (what I think are) block coordinates and entity ids – physical command blocks or minecart command blocks.

@ORelio

This comment has been minimized.

Copy link
Owner

ORelio commented Oct 30, 2015

Okay so i'ts not incompatible with vanilla plugin messages if discriminator byte is only used for specific plugin channels. Thanks.

@ORelio

This comment has been minimized.

Copy link
Owner

ORelio commented Nov 5, 2015

Great additions to your wiki article, so in fact discriminator byte is like a packet ID (so you might want to put then on the left of each packet), and forge protocol packets are simply wrapped in the plugin message channel... well explained and pretty easy to understand. Good job :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.