Skip to content

Commit 5eb7e04

Browse files
committed
Add forge info support
1 parent 5a088e8 commit 5eb7e04

8 files changed

Lines changed: 207 additions & 16 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
3535
restore-keys: ${{ runner.os }}-cargo-
3636
- name: Build (Debug)
37-
run: cargo build --verbose
37+
run: cargo build --all-features --verbose
3838
- name: Upload Artifact
3939
uses: actions/upload-artifact@v4
4040
with:

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
key: ${{ runner.os }}-rel-cargo-${{ hashFiles('**/Cargo.lock') }}
3232
restore-keys: ${{ runner.os }}-rel-cargo-
3333
- name: Build (Release)
34-
run: cargo build --release --verbose
34+
run: cargo build --all-features --release --verbose
3535
- name: Upload Artifact
3636
uses: actions/upload-artifact@v4
3737
with:

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ name = "mcping"
33
version = "2.0.0"
44
edition = "2024"
55

6+
[features]
7+
analyze-forge-info = []
8+
ping-legacy = []
9+
610
[dependencies]
711
clap = { version = "4.5.52", features = ["derive"] }
812
serde_json = "1.0.145"

src/analyze/forge_info.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
use crate::analyze::{Analyzer, StatusPayload};
2+
use crate::network::schema::{read_string, read_var_int_buf};
3+
use async_trait::async_trait;
4+
use bytes::{Buf, BufMut, BytesMut};
5+
use clap::Args;
6+
use std::io::ErrorKind;
7+
8+
#[derive(Args, Debug)]
9+
pub struct ForgeInfoArgs {
10+
/// Display forge channels
11+
#[arg(long)]
12+
display_channels: bool,
13+
}
14+
15+
pub struct ForgeInfo<'a> {
16+
args: &'a ForgeInfoArgs,
17+
}
18+
19+
async fn try_analyze_encoded(data: &str, display_channels: bool) -> std::io::Result<()> {
20+
let chars = data.encode_utf16().collect::<Vec<u16>>();
21+
if chars.len() < 2 {
22+
return Err(std::io::Error::new(
23+
ErrorKind::InvalidData,
24+
"ForgeData too short",
25+
));
26+
}
27+
28+
let buffer_len = chars[0] as u32 | (chars[1] as u32) << 15;
29+
let mut buffer = BytesMut::with_capacity(buffer_len as usize);
30+
let mut bits_in_buf = 0;
31+
let mut buf = 0;
32+
for char in chars[2..].iter() {
33+
while bits_in_buf >= 8 {
34+
buffer.put_u8((buf & 0xFF) as u8);
35+
buf >>= 8;
36+
bits_in_buf -= 8;
37+
}
38+
buf |= ((*char as u32) & 32767) << bits_in_buf;
39+
bits_in_buf += 15;
40+
}
41+
while bits_in_buf > 0 {
42+
buffer.put_u8((buf & 0xFF) as u8);
43+
buf >>= 8;
44+
bits_in_buf -= 8;
45+
}
46+
log::trace!("ForgeData: {:?}", String::from_utf8(buffer.to_vec()));
47+
48+
if buffer.try_get_u8()? != 0 {
49+
log::info!("Server truncated mod information");
50+
}
51+
52+
let size = buffer.try_get_u16()?;
53+
for _ in 0..size {
54+
let flag = read_var_int_buf(&mut buffer)?;
55+
let ch_size = flag >> 1 & (!(1 << 31));
56+
let ignore_server_only = flag & 1 != 0;
57+
let name = read_string(&mut buffer)?;
58+
let version = if ignore_server_only {
59+
"<UNCHECKED>".to_string()
60+
} else {
61+
read_string(&mut buffer)?
62+
};
63+
if version == "" {
64+
log::info!("Mod: {}", name);
65+
} else {
66+
log::info!("Mod: {} ({})", name, version);
67+
}
68+
69+
for _ in 0..ch_size {
70+
let path = read_string(&mut buffer)?;
71+
let ver = read_var_int_buf(&mut buffer)?;
72+
let required = buffer.try_get_u8()? != 0;
73+
if !display_channels {
74+
continue;
75+
}
76+
if required {
77+
log::info!(" Channel* {} ({})", path, ver);
78+
} else {
79+
log::info!(" Channel {} ({})", path, ver);
80+
}
81+
}
82+
}
83+
84+
if !display_channels {
85+
return Ok(());
86+
}
87+
88+
let non_mod_channels = read_var_int_buf(&mut buffer)?;
89+
if non_mod_channels == 0 {
90+
return Ok(());
91+
}
92+
log::info!("Non-mod channels:");
93+
for _ in 0..non_mod_channels {
94+
let path = read_string(&mut buffer)?;
95+
let ver = read_var_int_buf(&mut buffer)?;
96+
if buffer.try_get_u8()? != 0 {
97+
log::info!(" Channel* {} ({})", path, ver);
98+
} else {
99+
log::info!(" Channel {} ({})", path, ver);
100+
}
101+
}
102+
103+
Ok(())
104+
}
105+
106+
#[async_trait]
107+
impl Analyzer for ForgeInfo<'_> {
108+
fn enabled(&self, payload: &StatusPayload) -> bool {
109+
payload
110+
.full_extra
111+
.as_ref()
112+
.map(|m| m["forgeData"].as_object())
113+
.flatten()
114+
.is_some()
115+
}
116+
117+
async fn analyze(&self, payload: &StatusPayload) {
118+
let forge_data = &payload.full_extra.as_ref().expect("Forge data")["forgeData"];
119+
log::info!(
120+
"Forge Mod Loader (Network Version {})",
121+
forge_data["fmlNetworkVersion"]
122+
.as_i64()
123+
.map(|i| i.to_string())
124+
.unwrap_or("<unknown version>".to_string())
125+
);
126+
if let Some(data) = forge_data["d"].as_str() {
127+
try_analyze_encoded(data, self.args.display_channels)
128+
.await
129+
.err()
130+
.map(|e| log::error!("{}", e));
131+
} else {
132+
if forge_data["truncated"].as_bool().unwrap_or(false) {
133+
log::info!("Server truncated mod information");
134+
}
135+
136+
let default_vec = vec![];
137+
let mod_list = forge_data["mods"].as_array().unwrap_or(&default_vec);
138+
let ch_list = forge_data["channels"].as_array().unwrap_or(&default_vec);
139+
140+
for mod_data in mod_list {
141+
let name = mod_data["modId"].as_str().unwrap_or("<unknown name>");
142+
let version = mod_data["modmarker"].as_str().unwrap_or("");
143+
if version == "" {
144+
log::info!("Mod: {}", name);
145+
} else {
146+
log::info!("Mod: {} ({})", name, version);
147+
}
148+
}
149+
150+
if self.args.display_channels {
151+
for ch in ch_list {
152+
let path = ch["res"].as_str().unwrap_or("<unknown path>");
153+
let ver = ch["version"].as_i64().unwrap_or(0);
154+
if ch["required"].as_bool().unwrap_or(false) {
155+
log::info!(" Channel* {} ({})", path, ver);
156+
} else {
157+
log::info!(" Channel {} ({})", path, ver);
158+
}
159+
}
160+
}
161+
}
162+
}
163+
}
164+
165+
impl ForgeInfo<'_> {
166+
pub fn new(args: &'_ ForgeInfoArgs) -> ForgeInfo<'_> {
167+
ForgeInfo { args }
168+
}
169+
}

src/analyze/mod.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
mod favicon;
2+
#[cfg(feature = "analyze-forge-info")]
3+
mod forge_info;
24
mod motd;
35
mod ping;
46
mod player;
57
mod version;
68

79
use crate::analyze::favicon::FaviconArgs;
10+
#[cfg(feature = "analyze-forge-info")]
11+
use crate::analyze::forge_info::ForgeInfoArgs;
812
use crate::analyze::motd::{MotdArgs, sanitize_motd_args};
913
use crate::analyze::player::PlayerArgs;
1014
use crate::mode::QueryMode;
@@ -70,11 +74,13 @@ impl AnalyzerTools<'_> {
7074

7175
#[derive(Debug, Clone, Eq, PartialEq, ValueEnum)]
7276
pub enum AvailableAnalyzers {
73-
PING,
74-
VERSION,
75-
MOTD,
76-
PLAYER,
77-
FAVICON,
77+
Ping,
78+
Version,
79+
Motd,
80+
Player,
81+
Favicon,
82+
#[cfg(feature = "analyze-forge-info")]
83+
ForgeInfo,
7884
}
7985

8086
#[derive(Args, Debug)]
@@ -89,37 +95,45 @@ pub struct AnalyzerArgs {
8995
player_args: PlayerArgs,
9096
#[command(flatten)]
9197
favicon_args: FaviconArgs,
98+
#[cfg(feature = "analyze-forge-info")]
99+
#[command(flatten)]
100+
forge_info_args: ForgeInfoArgs,
92101
}
93102

94103
pub fn sanitize_analyzer_args(args: &mut crate::BaseArgs) {
95104
let analyzers = &args.analyzer_args.analyzers;
96-
if analyzers.contains(&AvailableAnalyzers::MOTD) {
105+
if analyzers.contains(&AvailableAnalyzers::Motd) {
97106
sanitize_motd_args(args);
98107
}
99108
}
100109

101110
pub fn init_analyzer_tools(args: &'_ AnalyzerArgs) -> AnalyzerTools<'_> {
102111
let mut analyzers: Vec<Box<dyn Analyzer>> = Vec::new();
103112

104-
if args.analyzers.contains(&AvailableAnalyzers::PING) {
113+
if args.analyzers.contains(&AvailableAnalyzers::Ping) {
105114
analyzers.push(Box::new(ping::Ping {}));
106115
}
107116

108-
if args.analyzers.contains(&AvailableAnalyzers::VERSION) {
117+
if args.analyzers.contains(&AvailableAnalyzers::Version) {
109118
analyzers.push(Box::new(version::Version {}));
110119
}
111120

112-
if args.analyzers.contains(&AvailableAnalyzers::MOTD) {
121+
if args.analyzers.contains(&AvailableAnalyzers::Motd) {
113122
analyzers.push(Box::new(motd::Motd::new(&args.motd_args)));
114123
}
115124

116-
if args.analyzers.contains(&AvailableAnalyzers::PLAYER) {
125+
if args.analyzers.contains(&AvailableAnalyzers::Player) {
117126
analyzers.push(Box::new(player::Player::new(&args.player_args)));
118127
}
119128

120-
if args.analyzers.contains(&AvailableAnalyzers::FAVICON) {
129+
if args.analyzers.contains(&AvailableAnalyzers::Favicon) {
121130
analyzers.push(Box::new(favicon::Favicon::new(&args.favicon_args)));
122131
}
123132

133+
#[cfg(feature = "analyze-forge-info")]
134+
if args.analyzers.contains(&AvailableAnalyzers::ForgeInfo) {
135+
analyzers.push(Box::new(forge_info::ForgeInfo::new(&args.forge_info_args)));
136+
}
137+
124138
AnalyzerTools { analyzers }
125139
}

src/analyze/motd.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub fn sanitize_motd_args(args: &mut crate::BaseArgs) {
3030
if args.log_level > LogLevel::INFO {
3131
args.analyzer_args
3232
.analyzers
33-
.pop_if(|i| *i == AvailableAnalyzers::MOTD);
33+
.pop_if(|i| *i == AvailableAnalyzers::Motd);
3434
return;
3535
}
3636
let motd = &mut args.analyzer_args.motd_args;

src/mode/java.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ async fn check_java_server(
143143

144144
#[derive(Args, Debug)]
145145
pub struct JavaModeArgs {
146-
/// Do not follow SRV redirection for Java and Legacy query mode
146+
/// Do not follow SRV redirection for Java query modes
147147
#[arg(long)]
148148
pub no_srv: bool,
149149
/// Simulate the protocol version of the client

src/mode/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::analyze::StatusPayload;
22
use crate::mode::QueryMode::*;
33
use crate::mode::bedrock::BedrockQuery;
44
use crate::mode::java::{JavaModeArgs, JavaQuery};
5+
#[cfg(feature = "ping-legacy")]
56
use crate::mode::legacy::LegacyQuery;
67
use async_trait::async_trait;
78
use clap::{Args, ValueEnum};
@@ -10,12 +11,14 @@ use std::io::ErrorKind;
1011

1112
pub mod bedrock;
1213
pub mod java;
14+
#[cfg(feature = "ping-legacy")]
1315
pub mod legacy;
1416

1517
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, ValueEnum)]
1618
pub enum QueryMode {
1719
JAVA,
1820
BEDROCK,
21+
#[cfg(feature = "ping-legacy")]
1922
LEGACY,
2023
}
2124

@@ -50,7 +53,8 @@ pub struct ModeArgs {
5053
pub fn init_query_engine(args: &'_ ModeArgs) -> QueryEngine<'_> {
5154
let mut modes: HashMap<QueryMode, Box<dyn QueryModeHandler>> = HashMap::new();
5255
modes.insert(JAVA, Box::new(JavaQuery::new(&args.java)));
53-
modes.insert(LEGACY, Box::new(LegacyQuery::new(&args.java)));
5456
modes.insert(BEDROCK, Box::new(BedrockQuery::new()));
57+
#[cfg(feature = "ping-legacy")]
58+
modes.insert(LEGACY, Box::new(LegacyQuery::new(&args.java)));
5559
QueryEngine { modes }
5660
}

0 commit comments

Comments
 (0)