Implement Forge FML2 protocol (MC 1.13+) (#1184)

Forge uses a different handshake scheme in FML2 protocol.
This handshake scheme uses LoginPluginRequest/Response packets.
This commit is contained in:
ORelio 2020-08-11 12:52:38 +02:00
parent aeac56890b
commit a28409043c
5 changed files with 346 additions and 91 deletions

View file

@ -143,6 +143,7 @@
<Compile Include="Mapping\MaterialExtensions.cs" />
<Compile Include="Protocol\EntityActionType.cs" />
<Compile Include="Protocol\Handlers\DataTypes.cs" />
<Compile Include="Protocol\Handlers\Forge\FMLVersion.cs" />
<Compile Include="Protocol\Handlers\PacketIncomingType.cs" />
<Compile Include="Protocol\Handlers\PacketOutgoingType.cs" />
<Compile Include="Protocol\Handlers\Protocol18Forge.cs" />

View file

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MinecraftClient.Protocol.Handlers.Forge
{
/// <summary>
/// Version of the FML protocol
/// </summary>
/// <seealso href="https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLNetworkConstants.java"/>
enum FMLVersion
{
FML,
FML2
}
}

View file

@ -31,92 +31,94 @@ namespace MinecraftClient.Protocol.Handlers.Forge
}
public List<ForgeMod> Mods;
internal FMLVersion Version;
/// <summary>
/// Create a new ForgeInfo from the given data.
/// </summary>
/// <param name="data">The modinfo JSON tag.</param>
/// <exception cref="System.ArgumentException">Thrown on missing mod list in JSON data</exception>
internal ForgeInfo(Json.JSONData data)
/// <param name="fmlVersion">Forge protocol version</param>
internal ForgeInfo(Json.JSONData data, FMLVersion fmlVersion)
{
this.Mods = new List<ForgeMod>();
bool listFound = false;
this.Version = fmlVersion;
// Example ModInfo for Minecraft 1.12 and lower (FML)
// "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"
// }]
// }
if (data.Properties.ContainsKey("modList") && data.Properties["modList"].Type == Json.JSONData.DataType.Array)
switch (fmlVersion)
{
listFound = true;
case FMLVersion.FML:
foreach (Json.JSONData mod in data.Properties["modList"].DataArray)
{
String modid = mod.Properties["modid"].StringValue;
String version = mod.Properties["version"].StringValue;
// Example ModInfo for Minecraft 1.12 and lower (FML)
this.Mods.Add(new ForgeMod(modid, version));
}
// "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"
// }]
// }
foreach (Json.JSONData mod in data.Properties["modList"].DataArray)
{
String modid = mod.Properties["modid"].StringValue;
String modversion = mod.Properties["version"].StringValue;
this.Mods.Add(new ForgeMod(modid, modversion));
}
break;
case FMLVersion.FML2:
// Example ModInfo for Minecraft 1.13 and greater (FML2)
// "forgeData": {
// "channels": [{
// "res": "minecraft:unregister",
// "version": "FML2",
// "required": true
// }, {
// "res": "minecraft:register",
// "version": "FML2",
// "required": true
// }],
// "mods": [{
// "modId": "minecraft",
// "modmarker": "1.15.2"
// }, {
// "modId": "forge",
// "modmarker": "ANY"
// }, {
// "modId": "rats",
// "modmarker": "5.3.2"
// }, {
// "modId": "citadel",
// "modmarker": "1.1.11"
// }],
// "fmlNetworkVersion": 2
// }
foreach (Json.JSONData mod in data.Properties["mods"].DataArray)
{
String modid = mod.Properties["modId"].StringValue;
String modmarker = mod.Properties["modmarker"].StringValue;
this.Mods.Add(new ForgeMod(modid, modmarker));
}
break;
default:
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
}
// Example ModInfo for Minecraft 1.13 and greater (FML2)
// "forgeData": {
// "channels": [{
// "res": "minecraft:unregister",
// "version": "FML2",
// "required": true
// }, {
// "res": "minecraft:register",
// "version": "FML2",
// "required": true
// }],
// "mods": [{
// "modId": "minecraft",
// "modmarker": "1.15.2"
// }, {
// "modId": "forge",
// "modmarker": "ANY"
// }, {
// "modId": "rats",
// "modmarker": "5.3.2"
// }, {
// "modId": "citadel",
// "modmarker": "1.1.11"
// }],
// "fmlNetworkVersion": 2
// }
if (data.Properties.ContainsKey("mods") && data.Properties["mods"].Type == Json.JSONData.DataType.Array)
{
listFound = true;
foreach (Json.JSONData mod in data.Properties["mods"].DataArray)
{
String modid = mod.Properties["modId"].StringValue;
String version = mod.Properties["modmarker"].StringValue;
this.Mods.Add(new ForgeMod(modid, version));
}
}
if (!listFound)
throw new ArgumentException("Missing mod list", "data");
}
}
}

View file

@ -169,7 +169,7 @@ namespace MinecraftClient.Protocol.Handlers
{
packetData.Clear();
int size = dataTypes.ReadNextVarIntRAW(socketWrapper); //Packet size
byte[] rawpacket = socketWrapper.ReadDataRAW(size);//Packet contents
byte[] rawpacket = socketWrapper.ReadDataRAW(size); //Packet contents
for (int i = 0; i < rawpacket.Length; i++)
packetData.Enqueue(rawpacket[i]);
@ -209,6 +209,13 @@ namespace MinecraftClient.Protocol.Handlers
if (protocolversion >= MC18Version)
compression_treshold = dataTypes.ReadNextVarInt(packetData);
break;
case 0x04:
int messageId = dataTypes.ReadNextVarInt(packetData);
string channel = dataTypes.ReadNextString(packetData);
List<byte> responseData = new List<byte>();
bool understood = pForge.HandleLoginPluginRequest(channel, packetData, ref responseData);
SendLoginPluginResponse(messageId, understood, responseData.ToArray());
return understood;
default:
return false; //Ignored packet
}
@ -1012,7 +1019,7 @@ namespace MinecraftClient.Protocol.Handlers
{
byte[] protocol_version = dataTypes.GetVarInt(protocolversion);
string server_address = pForge.GetServerAddress(handler.GetServerHost());
byte[] server_port = BitConverter.GetBytes((ushort)handler.GetServerPort()); Array.Reverse(server_port);
byte[] server_port = dataTypes.GetUShort((ushort)handler.GetServerPort());
byte[] next_state = dataTypes.GetVarInt(2);
byte[] handshake_packet = dataTypes.ConcatBytes(protocol_version, dataTypes.GetString(server_address), server_port, next_state);
@ -1445,6 +1452,25 @@ namespace MinecraftClient.Protocol.Handlers
catch (ObjectDisposedException) { return false; }
}
/// <summary>
/// Send a Login Plugin Response packet (0x02)
/// </summary>
/// <param name="messageId">Login Plugin Request message Id </param>
/// <param name="understood">TRUE if the request was understood</param>
/// <param name="data">Response to the request</param>
/// <returns>TRUE if successfully sent</returns>
public bool SendLoginPluginResponse(int messageId, bool understood, byte[] data)
{
try
{
SendPacket(0x02, dataTypes.ConcatBytes(dataTypes.GetVarInt(messageId), dataTypes.GetBool(understood), data));
return true;
}
catch (SocketException) { return false; }
catch (System.IO.IOException) { return false; }
catch (ObjectDisposedException) { return false; }
}
/// <summary>
/// Send an Interact Entity Packet to server
/// </summary>

View file

@ -44,17 +44,17 @@ namespace MinecraftClient.Protocol.Handlers
public string GetServerAddress(string serverAddress)
{
if (ForgeEnabled())
return serverAddress + "\0FML\0";
return serverAddress + "\0" + forgeInfo.Version + "\0";
return serverAddress;
}
/// <summary>
/// Completes the Minecraft Forge handshake.
/// Completes the Minecraft Forge handshake (Forge Protocol version 1: FML)
/// </summary>
/// <returns>Whether the handshake was successful.</returns>
public bool CompleteForgeHandshake()
{
if (ForgeEnabled())
if (ForgeEnabled() && forgeInfo.Version == FMLVersion.FML)
{
int packetID = -1;
Queue<byte> packetData = new Queue<byte>();
@ -98,7 +98,7 @@ namespace MinecraftClient.Protocol.Handlers
}
/// <summary>
/// Handle Forge plugin messages
/// Handle Forge plugin messages (Forge Protocol version 1: FML)
/// </summary>
/// <param name="channel">Plugin message channel</param>
/// <param name="packetData">Plugin message data</param>
@ -106,7 +106,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <returns>TRUE if the plugin message was recognized and handled</returns>
public bool HandlePluginMessage(string channel, Queue<byte> packetData, ref int currentDimension)
{
if (ForgeEnabled() && fmlHandshakeState != FMLHandshakeClientState.DONE)
if (ForgeEnabled() && forgeInfo.Version == FMLVersion.FML && fmlHandshakeState != FMLHandshakeClientState.DONE)
{
if (channel == "FML|HS")
{
@ -234,7 +234,198 @@ namespace MinecraftClient.Protocol.Handlers
}
/// <summary>
/// Send a forge plugin channel packet ("FML|HS"). Compression and encryption will be handled automatically.
/// Handle Forge plugin messages during login phase (Forge Protocol version 2: FML2)
/// </summary>
/// <param name="channel">Plugin message channel</param>
/// <param name="packetData">Plugin message data</param>
/// <param name="responseData">Response data to return to server</param>
/// <returns>TRUE/FALSE depending on whether the packet was understood or not</returns>
public bool HandleLoginPluginRequest(string channel, Queue<byte> packetData, ref List<byte> responseData)
{
if (ForgeEnabled() && forgeInfo.Version == FMLVersion.FML2 && channel == "fml:loginwrapper")
{
// Forge Handshake handler source code used to implement the FML2 packets:
// https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLNetworkConstants.java
// https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLHandshakeHandler.java
// https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/NetworkInitialization.java
// https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLLoginWrapper.java
// https://github.com/MinecraftForge/MinecraftForge/blob/master/src/main/java/net/minecraftforge/fml/network/FMLHandshakeMessages.java
//
// During Login, Forge will send a set of LoginPluginRequest packets and we need to respond accordingly.
// Each login plugin message contains in its payload field an inner packet created by FMLLoginWrapper.java:
//
// [ ResourceLocation ][ String ] // FML Channel name
// [ Inner Packet ][ Packet ] // Minecraft-Like packet
//
// The channel name allows identifying which handler in Forge will process the packet
// For instance, the handshake channel is fml:handshake as per FMLNetworkConstants.java
//
// The inner packet has the same layout as a Minecraft packet.
// Forge uses Minecraft's PacketBuffer class to decode the packet.
// We assume no network compression is active at this point.
//
// [ Length ][ VarInt ]
// [ PacketID ][ VarInt ]
// [ Data ][ .... ]
//
// Once decoded, the packet ID for fml:handshake is mapped in NetworkInitialization.java:
//
// 99 = Client to Server - Client ACK
// 1 = Server to Client - Mod List
// 2 = Client to Server - Mod List
// 3 = Server to Client - Registry
// 4 = Server to Client - Config
//
// The content of each message is mapped into a class inside FMLHandshakeMessages.java
// FMLHandshakeHandler will then process the packet, e.g. handleServerModListOnClient() for Server Mod List.
string fmlChannel = dataTypes.ReadNextString(packetData);
dataTypes.ReadNextVarInt(packetData); // Packet length
int packetID = dataTypes.ReadNextVarInt(packetData);
if (fmlChannel == "fml:handshake")
{
bool fmlResponseReady = false;
List<byte> fmlResponsePacket = new List<byte>();
switch (packetID)
{
case 1:
// Server Mod List: FMLHandshakeMessages.java > S2CModList > decode()
//
// [ Mod Count ][ VarInt ]
// [ Mod Name ][ String ] // Amount of entries according to Mod Count
// [ Channel Count ][ VarInt ]
// [ Chan Name ][ String ] // Amount of entries according to Channel Count
// [ Version ][ String ] // Each entry is a pair of Channel Name + Version (1)
// [ Registry Count ][ VarInt ]
// [ Registry ][ String ] // Amount of entries according to Registry Count
//
// [1]: Version is usually set to "FML2" for FML stuff and "1" for mods
if (Settings.DebugMessages)
ConsoleIO.WriteLineFormatted("§8Received FML2 Server Mod List");
List<string> mods = new List<string>();
int modCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < modCount; i++)
mods.Add(dataTypes.ReadNextString(packetData));
Dictionary<string, string> channels = new Dictionary<string, string>();
int channelCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < channelCount; i++)
channels.Add(dataTypes.ReadNextString(packetData), dataTypes.ReadNextString(packetData));
List<string> registries = new List<string>();
int registryCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < registryCount; i++)
registries.Add(dataTypes.ReadNextString(packetData));
// Server Mod List Reply: FMLHandshakeMessages.java > C2SModListReply > encode()
//
// [ Mod Count ][ VarInt ]
// [ Mod Name ][ String ] // Amount of entries according to Mod Count
// [ Channel Count ][ VarInt ]
// [ Chan Name ][ String ] // Amount of entries according to Channel Count
// [ Version ][ String ] // Each entry is a pair of Channel Name + Version
// [ Registry Count ][ VarInt ]
// [ Registry ][ String ] // Amount of entries according to Registry Count
// [ Version ][ String ] // Each entry is a pair of Registry Name + Version
//
// We are supposed to validate server info against our set of installed mods, then reply with our list
// In MCC, we just want to send a valid response so we'll reply back with data collected from the server.
if (Settings.DebugMessages)
ConsoleIO.WriteLineFormatted("§8Sending back FML2 Client Mod List");
// Packet ID 2: Client to Server Mod List
fmlResponsePacket.AddRange(dataTypes.GetVarInt(2));
fmlResponsePacket.AddRange(dataTypes.GetVarInt(mods.Count));
foreach (string mod in mods)
fmlResponsePacket.AddRange(dataTypes.GetString(mod));
fmlResponsePacket.AddRange(dataTypes.GetVarInt(channels.Count));
foreach (KeyValuePair<string, string> item in channels)
{
fmlResponsePacket.AddRange(dataTypes.GetString(item.Key));
fmlResponsePacket.AddRange(dataTypes.GetString(item.Value));
}
fmlResponsePacket.AddRange(dataTypes.GetVarInt(registries.Count));
foreach (string registry in registries)
{
fmlResponsePacket.AddRange(dataTypes.GetString(registry));
// We don't have Registry mapping from server, leave it empty
fmlResponsePacket.AddRange(dataTypes.GetString(""));
}
fmlResponseReady = true;
break;
case 3:
// Server Registry: FMLHandshakeMessages.java > S2CRegistry > decode()
//
// [ Registry Name ][ String ]
// [ Snapshot Present ][ Bool ]
// [ Snapshot data ][ .... ] // Only if "Snapshot Present" is True
//
// Registry Snapshot: ForgeRegistry.java > Snapshot > read(PacketBuffer)
// Not documented yet. We're ignoring this packet in MCC
if (Settings.DebugMessages)
{
string registryName = dataTypes.ReadNextString(packetData);
ConsoleIO.WriteLineFormatted("§8Acknowledging FML2 Server Registry: " + registryName);
}
fmlResponsePacket.AddRange(dataTypes.GetVarInt(99));
fmlResponseReady = true;
break;
case 4:
// Server Config: FMLHandshakeMessages.java > S2CConfigData > decode()
//
// [ Config Name ][ String ]
// [ Config Data ][ .... ] // Remaining packet data (1)
//
// [1] Config data may containt a standard Minecraft string readable with dataTypes.readNextString()
// We're ignoring this packet in MCC
if (Settings.DebugMessages)
{
string configName = dataTypes.ReadNextString(packetData);
ConsoleIO.WriteLineFormatted("§8Acknowledging FML2 Server Config: " + configName);
}
fmlResponsePacket.AddRange(dataTypes.GetVarInt(99));
fmlResponseReady = true;
break;
default:
if (Settings.DebugMessages)
ConsoleIO.WriteLineFormatted("§8Got Unknown FML2 Handshake message no. " + packetID);
break;
}
if (fmlResponseReady)
{
// Wrap our FML packet into a LoginPluginResponse payload
responseData.Clear();
responseData.AddRange(dataTypes.GetString(fmlChannel));
responseData.AddRange(dataTypes.GetVarInt(fmlResponsePacket.Count));
responseData.AddRange(fmlResponsePacket);
return true;
}
}
else if (Settings.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8Ignoring Unknown FML2 LoginMessage channel: " + fmlChannel);
}
}
return false;
}
/// <summary>
/// Send a forge plugin channel packet ("FML|HS"). Compression and encryption will be handled automatically. (Forge Protocol version 1: FML)
/// </summary>
/// <param name="discriminator">Discriminator to use.</param>
/// <param name="data">packet Data</param>
@ -244,34 +435,52 @@ namespace MinecraftClient.Protocol.Handlers
}
/// <summary>
/// Server Info: Check for For Forge on a Minecraft server Ping result
/// Server Info: Check for For Forge versions 1 and 2 on a Minecraft server Ping result
/// </summary>
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <returns>True if the server is running Forge</returns>
public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo forgeInfo)
{
return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, "modinfo", "type", "FML") // MC 1.12 and lower
|| ServerInfoCheckForgeSub(jsonData, ref forgeInfo, "forgeData", "fmlNetworkVersion", "2"); // MC 1.13 and greater
return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower
|| ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2); // MC 1.13 and greater
}
/// <summary>
/// Server Info: Check for For Forge on a Minecraft server Ping result
/// Server Info: Check for For Forge on a Minecraft server Ping result (Handles FML and FML2
/// </summary>
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <param name="forgeDataTag">ForgeData JSON field, e.g. "modinfo"</param>
/// <param name="versionField">ForgeData version field, e.g. "type"</param>
/// <param name="versionString">ForgeData version value, e.g. "FML"</param>
/// <param name="fmlVersion">Forge protocol version</param>
/// <returns>True if the server is running Forge</returns>
private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo forgeInfo, string forgeDataTag, string versionField, string versionString)
private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo forgeInfo, FMLVersion fmlVersion)
{
string forgeDataTag;
string versionField;
string versionString;
switch (fmlVersion)
{
case FMLVersion.FML:
forgeDataTag = "modinfo";
versionField = "type";
versionString = "FML";
break;
case FMLVersion.FML2:
forgeDataTag = "forgeData";
versionField = "fmlNetworkVersion";
versionString = "2";
break;
default:
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
}
if (jsonData.Properties.ContainsKey(forgeDataTag) && jsonData.Properties[forgeDataTag].Type == Json.JSONData.DataType.Object)
{
Json.JSONData modData = jsonData.Properties[forgeDataTag];
if (modData.Properties.ContainsKey(versionField) && modData.Properties[versionField].StringValue == versionString)
{
forgeInfo = new ForgeInfo(modData);
forgeInfo = new ForgeInfo(modData, fmlVersion);
if (forgeInfo.Mods.Any())
{
ConsoleIO.WriteLineFormatted(String.Format("§8Server is running Forge with {0} mods.", forgeInfo.Mods.Count));