Allows connecting to newer Forge servers, 1.13.2 to 1.16.5 at least, which use the FML2 handshake protocol: https://wiki.vg/Minecraft_Forge_Handshake#FML2_protocol_.281.13_-_Current.29 Tested with a modded 1.16.5 server Extends #88 #134 Forge FML (v1, for 1.7.10 - 1.12.2) * protocol: update Cargo.lock * protocol: send FML2 on fmlNetworkVersion: 2 * protocol: factor out read_raw_packet_from() * protocol: move plugin message writing from Server to Conn; add write_fml2_handshake_plugin_message * protocol: CommandNode: add forge:modid, forge:enum * forge: add fml2 handshake packets * server: handle fml:loginwrapper fml::handshake packets
This commit is contained in:
parent
1a257e2e90
commit
6b961622aa
|
@ -55,7 +55,7 @@ development is not in lock-step with the server version. The level of
|
|||
support varies, but the goal is to support major versions from 1.7.10
|
||||
up to the current latest major version. Occasionally, snapshots are also supported.
|
||||
|
||||
Forge servers are currently supported on 1.7.10 - 1.12.2.
|
||||
Forge servers are supported on 1.7.10 - 1.12.2 (FML) and 1.13.2 - 1.16.5 (FML2).
|
||||
|
||||
Support for older protocols will _not_ be dropped as newer protocols are added.
|
||||
|
||||
|
|
|
@ -840,9 +840,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.119"
|
||||
version = "1.0.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bdd36f49e35b61d49efd8aa7fc068fd295961fd2286d0b2ee9a4c7a14e99cc3"
|
||||
checksum = "166b2349061381baf54a58e4b13c89369feb0ef2eaa57198899e2312aac30aab"
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
|
|
|
@ -191,3 +191,121 @@ impl Serializable for FmlHs {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod fml2 {
|
||||
// https://wiki.vg/Minecraft_Forge_Handshake#FML2_protocol_.281.13_-_Current.29
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Channel {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
impl Serializable for Channel {
|
||||
fn read_from<R: io::Read>(buf: &mut R) -> Result<Self, Error> {
|
||||
Ok(Channel {
|
||||
name: Serializable::read_from(buf)?,
|
||||
version: Serializable::read_from(buf)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_to<W: io::Write>(&self, buf: &mut W) -> Result<(), Error> {
|
||||
self.name.write_to(buf)?;
|
||||
self.version.write_to(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Registry {
|
||||
pub name: String,
|
||||
pub marker: String,
|
||||
}
|
||||
|
||||
impl Serializable for Registry {
|
||||
fn read_from<R: io::Read>(buf: &mut R) -> Result<Self, Error> {
|
||||
Ok(Registry {
|
||||
name: Serializable::read_from(buf)?,
|
||||
marker: "".to_string(), // not in ModList
|
||||
})
|
||||
}
|
||||
|
||||
fn write_to<W: io::Write>(&self, buf: &mut W) -> Result<(), Error> {
|
||||
self.name.write_to(buf)?;
|
||||
self.marker.write_to(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FmlHandshake {
|
||||
ModList {
|
||||
mod_names: LenPrefixed<VarInt, String>,
|
||||
channels: LenPrefixed<VarInt, Channel>,
|
||||
registries: LenPrefixed<VarInt, Registry>,
|
||||
},
|
||||
|
||||
ModListReply {
|
||||
mod_names: LenPrefixed<VarInt, String>,
|
||||
channels: LenPrefixed<VarInt, Channel>,
|
||||
registries: LenPrefixed<VarInt, Registry>,
|
||||
},
|
||||
|
||||
ServerRegistry {
|
||||
name: String,
|
||||
snapshot_present: bool,
|
||||
snapshot: Vec<u8>,
|
||||
},
|
||||
|
||||
ConfigurationData {
|
||||
filename: String,
|
||||
contents: Vec<u8>,
|
||||
},
|
||||
|
||||
Acknowledgement,
|
||||
}
|
||||
|
||||
impl FmlHandshake {
|
||||
pub fn packet_by_id<R: io::Read>(id: i32, buf: &mut R) -> Result<Self, Error> {
|
||||
Ok(match id {
|
||||
1 => FmlHandshake::ModList {
|
||||
mod_names: Serializable::read_from(buf)?,
|
||||
channels: Serializable::read_from(buf)?,
|
||||
registries: Serializable::read_from(buf)?,
|
||||
},
|
||||
3 => FmlHandshake::ServerRegistry {
|
||||
name: Serializable::read_from(buf)?,
|
||||
snapshot_present: Serializable::read_from(buf)?,
|
||||
snapshot: Serializable::read_from(buf)?,
|
||||
},
|
||||
4 => FmlHandshake::ConfigurationData {
|
||||
filename: Serializable::read_from(buf)?,
|
||||
contents: Serializable::read_from(buf)?,
|
||||
},
|
||||
_ => unimplemented!(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serializable for FmlHandshake {
|
||||
fn read_from<R: io::Read>(_buf: &mut R) -> Result<Self, Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn write_to<W: io::Write>(&self, buf: &mut W) -> Result<(), Error> {
|
||||
match self {
|
||||
FmlHandshake::ModListReply {
|
||||
mod_names,
|
||||
channels,
|
||||
registries,
|
||||
} => {
|
||||
VarInt(2).write_to(buf)?;
|
||||
mod_names.write_to(buf)?;
|
||||
channels.write_to(buf)?;
|
||||
registries.write_to(buf)
|
||||
}
|
||||
FmlHandshake::Acknowledgement => VarInt(99).write_to(buf),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -971,7 +971,7 @@ pub enum Direction {
|
|||
|
||||
/// The protocol has multiple 'sub-protocols' or states which control which
|
||||
/// packet an id points to.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum State {
|
||||
Handshaking,
|
||||
Play,
|
||||
|
@ -1036,7 +1036,7 @@ pub struct Conn {
|
|||
|
||||
cipher: Option<Aes128Cfb>,
|
||||
|
||||
compression_threshold: i32,
|
||||
pub compression_threshold: i32,
|
||||
}
|
||||
|
||||
impl Conn {
|
||||
|
@ -1099,17 +1099,91 @@ impl Conn {
|
|||
}
|
||||
self.write_all(&buf)?;
|
||||
|
||||
Result::Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_packet(&mut self) -> Result<packet::Packet, Error> {
|
||||
let len = VarInt::read_from(self)?.0 as usize;
|
||||
pub fn write_plugin_message(&mut self, channel: &str, data: &[u8]) -> Result<(), Error> {
|
||||
if is_network_debug() {
|
||||
debug!(
|
||||
"Sending plugin message: channel={}, data={:?}",
|
||||
channel, data
|
||||
);
|
||||
}
|
||||
debug_assert!(self.state == State::Play);
|
||||
if self.protocol_version >= 47 {
|
||||
self.write_packet(packet::play::serverbound::PluginMessageServerbound {
|
||||
channel: channel.to_string(),
|
||||
data: data.to_vec(),
|
||||
})?;
|
||||
} else {
|
||||
self.write_packet(packet::play::serverbound::PluginMessageServerbound_i16 {
|
||||
channel: channel.to_string(),
|
||||
data: LenPrefixedBytes::<VarShort>::new(data.to_vec()),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_fmlhs_plugin_message(&mut self, msg: &forge::FmlHs) -> Result<(), Error> {
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
msg.write_to(&mut buf)?;
|
||||
|
||||
self.write_plugin_message("FML|HS", &buf)
|
||||
}
|
||||
|
||||
pub fn write_login_plugin_response(
|
||||
&mut self,
|
||||
message_id: VarInt,
|
||||
successful: bool,
|
||||
data: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
if is_network_debug() {
|
||||
debug!(
|
||||
"Sending login plugin message: message_id={:?}, successful={:?}, data={:?}",
|
||||
message_id, successful, data,
|
||||
);
|
||||
}
|
||||
debug_assert!(self.state == State::Login);
|
||||
self.write_packet(packet::login::serverbound::LoginPluginResponse {
|
||||
message_id,
|
||||
successful,
|
||||
data: data.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_fml2_handshake_plugin_message(
|
||||
&mut self,
|
||||
message_id: VarInt,
|
||||
msg: Option<&forge::fml2::FmlHandshake>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(msg) = msg {
|
||||
let mut inner_buf: Vec<u8> = vec![];
|
||||
msg.write_to(&mut inner_buf)?;
|
||||
|
||||
let mut outer_buf: Vec<u8> = vec![];
|
||||
"fml:handshake".to_string().write_to(&mut outer_buf)?;
|
||||
VarInt(inner_buf.len() as i32).write_to(&mut outer_buf)?;
|
||||
inner_buf.write_to(&mut outer_buf)?;
|
||||
|
||||
self.write_login_plugin_response(message_id, true, &outer_buf)
|
||||
} else {
|
||||
unimplemented!() // successful: false, no payload
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn read_raw_packet_from<R: io::Read>(
|
||||
buf: &mut R,
|
||||
compression_threshold: i32,
|
||||
) -> Result<(i32, Box<io::Cursor<Vec<u8>>>), Error> {
|
||||
let len = VarInt::read_from(buf)?.0 as usize;
|
||||
let mut ibuf = vec![0; len];
|
||||
self.read_exact(&mut ibuf)?;
|
||||
buf.read_exact(&mut ibuf)?;
|
||||
|
||||
let mut buf = io::Cursor::new(ibuf);
|
||||
|
||||
if self.compression_threshold >= 0 {
|
||||
if compression_threshold >= 0 {
|
||||
let uncompressed_size = VarInt::read_from(&mut buf)?.0;
|
||||
if uncompressed_size != 0 {
|
||||
let mut new = Vec::with_capacity(uncompressed_size as usize);
|
||||
|
@ -1120,7 +1194,7 @@ impl Conn {
|
|||
if is_network_debug() {
|
||||
debug!(
|
||||
"Decompressed threshold={} len={} uncompressed_size={} to {} bytes",
|
||||
self.compression_threshold,
|
||||
compression_threshold,
|
||||
len,
|
||||
uncompressed_size,
|
||||
new.len()
|
||||
|
@ -1131,6 +1205,13 @@ impl Conn {
|
|||
}
|
||||
let id = VarInt::read_from(&mut buf)?.0;
|
||||
|
||||
Ok((id, Box::new(buf)))
|
||||
}
|
||||
|
||||
pub fn read_packet(&mut self) -> Result<packet::Packet, Error> {
|
||||
let compression_threshold = self.compression_threshold;
|
||||
let (id, mut buf) = Conn::read_raw_packet_from(self, compression_threshold)?;
|
||||
|
||||
let dir = match self.direction {
|
||||
Direction::Clientbound => Direction::Serverbound,
|
||||
Direction::Serverbound => Direction::Clientbound,
|
||||
|
@ -1224,6 +1305,7 @@ impl Conn {
|
|||
|
||||
// For modded servers, get the list of Forge mods installed
|
||||
let mut forge_mods: std::vec::Vec<crate::protocol::forge::ForgeMod> = vec![];
|
||||
let mut fml_network_version: Option<i64> = None;
|
||||
if let Some(modinfo) = val.get("modinfo") {
|
||||
if let Some(modinfo_type) = modinfo.get("type") {
|
||||
if modinfo_type == "FML" {
|
||||
|
@ -1240,6 +1322,7 @@ impl Conn {
|
|||
.push(crate::protocol::forge::ForgeMod { modid, version });
|
||||
}
|
||||
}
|
||||
fml_network_version = Some(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -1267,6 +1350,13 @@ impl Conn {
|
|||
}
|
||||
}
|
||||
}
|
||||
fml_network_version = Some(
|
||||
forge_data
|
||||
.get("fmlNetworkVersion")
|
||||
.unwrap()
|
||||
.as_i64()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok((
|
||||
|
@ -1301,6 +1391,7 @@ impl Conn {
|
|||
.and_then(Value::as_str)
|
||||
.map(|v| v.to_owned()),
|
||||
forge_mods,
|
||||
fml_network_version,
|
||||
},
|
||||
ping,
|
||||
))
|
||||
|
@ -1352,6 +1443,7 @@ pub struct Status {
|
|||
pub description: format::Component,
|
||||
pub favicon: Option<String>,
|
||||
pub forge_mods: Vec<crate::protocol::forge::ForgeMod>,
|
||||
pub fml_network_version: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -3135,6 +3135,10 @@ pub enum CommandProperty {
|
|||
EntitySummon,
|
||||
Dimension,
|
||||
UUID,
|
||||
ForgeModId,
|
||||
ForgeEnum {
|
||||
cls: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Serializable for CommandNode {
|
||||
|
@ -3264,6 +3268,10 @@ impl Serializable for CommandNode {
|
|||
"minecraft:entity_summon" => CommandProperty::EntitySummon,
|
||||
"minecraft:dimension" => CommandProperty::Dimension,
|
||||
"minecraft:uuid" => CommandProperty::UUID,
|
||||
"forge:modid" => CommandProperty::ForgeModId,
|
||||
"forge:enum" => CommandProperty::ForgeEnum {
|
||||
cls: Serializable::read_from(buf)?,
|
||||
},
|
||||
_ => panic!("unsupported command node parser {}", parse),
|
||||
})
|
||||
} else {
|
||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -91,7 +91,7 @@ pub struct Game {
|
|||
|
||||
impl Game {
|
||||
pub fn connect_to(&mut self, address: &str) {
|
||||
let (protocol_version, forge_mods) =
|
||||
let (protocol_version, forge_mods, fml_network_version) =
|
||||
match protocol::Conn::new(&address, self.default_protocol_version)
|
||||
.and_then(|conn| conn.do_status())
|
||||
{
|
||||
|
@ -100,14 +100,18 @@ impl Game {
|
|||
"Detected server protocol version {}",
|
||||
res.0.version.protocol
|
||||
);
|
||||
(res.0.version.protocol, res.0.forge_mods)
|
||||
(
|
||||
res.0.version.protocol,
|
||||
res.0.forge_mods,
|
||||
res.0.fml_network_version,
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Error pinging server {} to get protocol version: {:?}, defaulting to {}",
|
||||
address, err, self.default_protocol_version
|
||||
);
|
||||
(self.default_protocol_version, vec![])
|
||||
(self.default_protocol_version, vec![], None)
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -127,6 +131,7 @@ impl Game {
|
|||
&address,
|
||||
protocol_version,
|
||||
forge_mods,
|
||||
fml_network_version,
|
||||
))
|
||||
.unwrap();
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ use crate::types::Gamemode;
|
|||
use crate::world;
|
||||
use crate::world::block;
|
||||
use cgmath::prelude::*;
|
||||
use log::{debug, error, warn};
|
||||
use log::{debug, error, info, warn};
|
||||
use rand::{self, Rng};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::BuildHasherDefault;
|
||||
|
@ -110,14 +110,17 @@ impl Server {
|
|||
address: &str,
|
||||
protocol_version: i32,
|
||||
forge_mods: Vec<forge::ForgeMod>,
|
||||
fml_network_version: Option<i64>,
|
||||
) -> Result<Server, protocol::Error> {
|
||||
let mut conn = protocol::Conn::new(address, protocol_version)?;
|
||||
|
||||
let tag = if !forge_mods.is_empty() {
|
||||
"\0FML\0"
|
||||
} else {
|
||||
""
|
||||
let tag = match fml_network_version {
|
||||
Some(1) => "\0FML\0",
|
||||
Some(2) => "\0FML2\0",
|
||||
None => "",
|
||||
_ => panic!("unsupported FML network version: {:?}", fml_network_version),
|
||||
};
|
||||
|
||||
let host = conn.host.clone() + tag;
|
||||
let port = conn.port;
|
||||
conn.write_packet(protocol::packet::handshake::serverbound::Handshake {
|
||||
|
@ -167,7 +170,6 @@ impl Server {
|
|||
Some(rx),
|
||||
));
|
||||
}
|
||||
// TODO: avoid duplication
|
||||
protocol::packet::Packet::LoginSuccess_UUID(val) => {
|
||||
warn!("Server is running in offline mode");
|
||||
debug!("Login: {} {:?}", val.username, val.uuid);
|
||||
|
@ -188,7 +190,7 @@ impl Server {
|
|||
protocol::packet::Packet::LoginDisconnect(val) => {
|
||||
return Err(protocol::Error::Disconnect(val.reason))
|
||||
}
|
||||
val => return Err(protocol::Error::Err(format!("Wrong packet: {:?}", val))),
|
||||
val => return Err(protocol::Error::Err(format!("Wrong packet 1: {:?}", val))),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -225,6 +227,7 @@ impl Server {
|
|||
write.enable_encyption(&shared, false);
|
||||
|
||||
let uuid;
|
||||
let compression_threshold = read.compression_threshold;
|
||||
loop {
|
||||
match read.read_packet()? {
|
||||
protocol::packet::Packet::SetInitialCompression(val) => {
|
||||
|
@ -248,7 +251,73 @@ impl Server {
|
|||
protocol::packet::Packet::LoginDisconnect(val) => {
|
||||
return Err(protocol::Error::Disconnect(val.reason))
|
||||
}
|
||||
val => return Err(protocol::Error::Err(format!("Wrong packet: {:?}", val))),
|
||||
protocol::packet::Packet::LoginPluginRequest(req) => {
|
||||
match req.channel.as_ref() {
|
||||
"fml:loginwrapper" => {
|
||||
let mut cursor = std::io::Cursor::new(req.data);
|
||||
let channel: String = protocol::Serializable::read_from(&mut cursor)?;
|
||||
|
||||
let (id, mut data) = protocol::Conn::read_raw_packet_from(
|
||||
&mut cursor,
|
||||
compression_threshold,
|
||||
)?;
|
||||
|
||||
match channel.as_ref() {
|
||||
"fml:handshake" => {
|
||||
let packet =
|
||||
forge::fml2::FmlHandshake::packet_by_id(id, &mut data)?;
|
||||
use forge::fml2::FmlHandshake::*;
|
||||
match packet {
|
||||
ModList {
|
||||
mod_names,
|
||||
channels,
|
||||
registries,
|
||||
} => {
|
||||
info!("ModList mod_names={:?} channels={:?} registries={:?}", mod_names, channels, registries);
|
||||
write.write_fml2_handshake_plugin_message(
|
||||
req.message_id,
|
||||
Some(&ModListReply {
|
||||
mod_names,
|
||||
channels,
|
||||
registries,
|
||||
}),
|
||||
)?;
|
||||
}
|
||||
ServerRegistry {
|
||||
name,
|
||||
snapshot_present: _,
|
||||
snapshot: _,
|
||||
} => {
|
||||
info!("ServerRegistry {:?}", name);
|
||||
write.write_fml2_handshake_plugin_message(
|
||||
req.message_id,
|
||||
Some(&Acknowledgement),
|
||||
)?;
|
||||
}
|
||||
ConfigurationData { filename, contents } => {
|
||||
info!(
|
||||
"ConfigurationData filename={:?} contents={}",
|
||||
filename,
|
||||
String::from_utf8_lossy(&contents)
|
||||
);
|
||||
write.write_fml2_handshake_plugin_message(
|
||||
req.message_id,
|
||||
Some(&Acknowledgement),
|
||||
)?;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
_ => panic!(
|
||||
"unknown LoginPluginRequest fml:loginwrapper channel: {:?}",
|
||||
channel
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => panic!("unsupported LoginPluginRequest channel: {:?}", req.channel),
|
||||
}
|
||||
}
|
||||
val => return Err(protocol::Error::Err(format!("Wrong packet 2: {:?}", val))),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -946,33 +1015,17 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: remove wrappers and directly call on Conn
|
||||
fn write_fmlhs_plugin_message(&mut self, msg: &forge::FmlHs) {
|
||||
use crate::protocol::Serializable;
|
||||
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
msg.write_to(&mut buf).unwrap();
|
||||
|
||||
self.write_plugin_message("FML|HS", &buf);
|
||||
let _ = self.conn.as_mut().unwrap().write_fmlhs_plugin_message(msg); // TODO handle errors
|
||||
}
|
||||
|
||||
fn write_plugin_message(&mut self, channel: &str, data: &[u8]) {
|
||||
if protocol::is_network_debug() {
|
||||
debug!(
|
||||
"Sending plugin message: channel={}, data={:?}",
|
||||
channel, data
|
||||
);
|
||||
}
|
||||
if self.protocol_version >= 47 {
|
||||
self.write_packet(packet::play::serverbound::PluginMessageServerbound {
|
||||
channel: channel.to_string(),
|
||||
data: data.to_vec(),
|
||||
});
|
||||
} else {
|
||||
self.write_packet(packet::play::serverbound::PluginMessageServerbound_i16 {
|
||||
channel: channel.to_string(),
|
||||
data: crate::protocol::LenPrefixedBytes::<protocol::VarShort>::new(data.to_vec()),
|
||||
});
|
||||
}
|
||||
let _ = self
|
||||
.conn
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_plugin_message(channel, data); // TODO handle errors
|
||||
}
|
||||
|
||||
fn on_game_join_worldnames_ishard(
|
||||
|
|
Loading…
Reference in New Issue