forge 1.16 and forge channels support

This commit is contained in:
LordMZTE 2020-10-28 14:45:56 +01:00
parent 60df88dee1
commit 6db20e631b
4 changed files with 127 additions and 113 deletions

View file

@ -11,7 +11,7 @@ clap = { version = "^2.33", features = ["yaml"] }
image-base64 = "0.1.0" image-base64 = "0.1.0"
image = "0.23.9" image = "0.23.9"
asciify = "0.1.6" asciify = "0.1.6"
async-minecraft-ping = { git = "https://github.com/LordMZTE/async-minecraft-ping.git", tag = "v0.2.3" } async-minecraft-ping = { git = "https://github.com/LordMZTE/async-minecraft-ping.git", tag = "v0.2.4" }
tokio = { version = "0.2.22", features = ["full"] } tokio = { version = "0.2.22", features = ["full"] }
itertools = "0.9.0" itertools = "0.9.0"
termcolor = "1" termcolor = "1"

View file

@ -1,69 +1,72 @@
name: "mcstat" name: "mcstat"
about: "queries information about a minecraft server" about: "queries information about a minecraft server"
args: args:
- ip: - ip:
help: "the ip of the server to ping" help: "the ip of the server to ping"
takes_value: true takes_value: true
index: 1 index: 1
required: true required: true
- port: - port:
help: "the port of the server" help: "the port of the server"
long: "port" long: "port"
short: p short: p
default_value: "25565" default_value: "25565"
takes_value: true takes_value: true
- protocol-version: - protocol-version:
long: "protocol" long: "protocol"
help: "the protocol version to use" help: "the protocol version to use"
default_value: "751" default_value: "751"
takes_value: true takes_value: true
- timeout: - timeout:
long: "timeout" long: "timeout"
short: t short: t
help: "the time before the server ping times out in milliseconds" help: "the time before the server ping times out in milliseconds"
takes_value: true takes_value: true
default_value: "500" default_value: "500"
- raw: - raw:
short: r short: r
help: "if supplied, the raw json response from the server will be printed" help: "if supplied, the raw json response from the server will be printed"
- mods: - mods:
short: m short: m
help: "if supplied, a mod list will be printed" help: "if supplied, a mod list will be printed"
- modversions: - modversions:
short: v short: v
help: "if supplied, mods will also have their version info printed" help: "if supplied, mods will also have their version info printed"
requires: "mods" requires: "mods"
- channels:
long: "channels"
help: "displays forge mod channels if the server sends them"
# IMAGE ARGS # IMAGE ARGS
# TODO due to a bug in clap, the image argument is always required because size has a default value # TODO due to a bug in clap, the image argument is always required because size has a default value
- image: - image:
short: "i" short: "i"
help: "if the server's favicon should be printed as ASCII art" help: "if the server's favicon should be printed as ASCII art"
required: false required: false
- color: - color:
short: "c" short: "c"
help: "if the favicon image should be printed with ANSI color formatting or monochrome" help: "if the favicon image should be printed with ANSI color formatting or monochrome"
- size: - size:
short: "s" short: "s"
help: "the size of the image" help: "the size of the image"
takes_value: true takes_value: true
default_value: "16" default_value: "16"
- deep: - deep:
short: "d" short: "d"
help: "if provided the ascii image will have more different characters" help: "if provided the ascii image will have more different characters"
- invert: - invert:
short: "n" short: "n"
help: "inverts the ascii image thickness" help: "inverts the ascii image thickness"
groups: groups:
- img-flags: - img-flags:
# TODO uncomment once bug is fixed # TODO uncomment once bug is fixed
# requires: # requires:
# - image # - image
multiple: true multiple: true
required: false required: false
args: args:
- color - color
- size - size
- deep - deep
- invert - invert

View file

@ -1,4 +1,5 @@
use asciify::AsciiBuilder; use asciify::AsciiBuilder;
use itertools::Itertools;
/// prints a table with the entries supplied /// prints a table with the entries supplied
/// the identifier at the start of each entry sets the type /// the identifier at the start of each entry sets the type
@ -84,3 +85,31 @@ pub fn remove_formatting(s: &str) -> String {
} }
buf buf
} }
/// formats a iterator to a readable list
///
/// if `second_column`, the right strings will also be displayed
pub fn get_table<'a>(
entries: impl Iterator<Item = (&'a str, &'a str)> + Clone,
second_column: bool,
) -> String {
// the width at which | characters should be placed this is the length of the
// longest entry
let max_width = if second_column {
entries.clone().map(|m| m.0.len()).max().unwrap_or_default()
} else {
// this will not be used in case second_column is off so we just use 0
0
};
entries
.map(|m| {
if second_column {
format!("{: <width$} | {}", m.0, m.1, width = max_width)
} else {
m.0.to_owned()
}
})
.intersperse("\n".to_owned())
.collect()
}

View file

@ -11,11 +11,11 @@ use tokio::time;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use asciify::AsciiBuilder; use asciify::AsciiBuilder;
use async_minecraft_ping::{ConnectionConfig, ModInfo, ServerDescription, StatusResponse}; use async_minecraft_ping::{ConnectionConfig, ServerDescription, StatusResponse};
use clap::App; use clap::App;
use image::ImageFormat; use image::ImageFormat;
use itertools::Itertools; use itertools::Itertools;
use mcstat::{remove_formatting, AsciiConfig}; use mcstat::{remove_formatting, AsciiConfig, get_table};
use termcolor::{Buffer, BufferWriter, ColorChoice, WriteColor}; use termcolor::{Buffer, BufferWriter, ColorChoice, WriteColor};
/// this message is used if getting a value from the arguments fails /// this message is used if getting a value from the arguments fails
@ -90,10 +90,10 @@ async fn main() -> Result<()> {
.context("image size must be number")?; .context("image size must be number")?;
let mut image = None; let mut image = None;
if let (Some(favicon), true) = (response.favicon, matches.is_present("image")) { if let (Some(favicon), true) = (&response.favicon, matches.is_present("image")) {
// The image parsing and asciifying is done while the table is printing // The image parsing and asciifying is done while the table is printing
image = Some(tokio::spawn(asciify_base64_image( image = Some(tokio::spawn(asciify_base64_image(
favicon, favicon.clone(),
AsciiConfig { AsciiConfig {
size: Some(image_size), size: Some(image_size),
colored: matches.is_present("color"), colored: matches.is_present("color"),
@ -114,7 +114,8 @@ async fn main() -> Result<()> {
let player_sample = response let player_sample = response
.players .players
.sample .sample
.unwrap_or_default() .as_ref()
.unwrap_or(&vec![])
.iter() .iter()
.map(|p| p.name.as_str()) .map(|p| p.name.as_str())
.intersperse("\n") .intersperse("\n")
@ -124,12 +125,12 @@ async fn main() -> Result<()> {
40; 40;
bo "Description" => none_if_empty!(remove_formatting(&response.description.get_text())), bo "Description" => none_if_empty!(remove_formatting(&response.description.get_text())),
bo "Extra Description" => { bo "Extra Description" => {
if let ServerDescription::Big(big_desc) = response.description { if let ServerDescription::Big(big_desc) = &response.description {
let desc = big_desc.extra; let desc = &big_desc.extra;
if desc.is_empty() { if desc.is_empty() {
None None
} else { } else {
Some(desc.into_iter().map(|p| p.text).collect::<String>()) Some(desc.into_iter().map(|p| p.text.clone()).collect::<String>())
} }
} else { } else {
None None
@ -137,14 +138,31 @@ async fn main() -> Result<()> {
}, },
bo "Player Sample" => none_if_empty!(remove_formatting(&player_sample)), bo "Player Sample" => none_if_empty!(remove_formatting(&player_sample)),
lo "Server Version" => none_if_empty!(remove_formatting(&response.version.name)), lo "Server Version" => none_if_empty!(remove_formatting(&response.version.name)),
l "Online Players" => response.players.online, l "Online Players" => &response.players.online,
l "Max Players" => response.players.max, l "Max Players" => &response.players.max,
bo "Mods" => if let (Some(mods), true) = (response.modinfo, matches.is_present("mods")) { bo "Mods" => if let (Some(mod_list), true) = (response.forge_mod_info(), matches.is_present("mods")) {
Some(get_modlist(mods, matches.is_present("modversions"))) Some(get_table(
mod_list
.iter()
.sorted_by(|a, b| a.modid.cmp(&b.modid))
.map(|m| (&*m.modid, &*m.version)),
matches.is_present("modversions")
))
} else { } else {
None None
}, },
l "Server Protocol" => response.version.protocol, l "Server Protocol" => &response.version.protocol,
bo "Forge Channels" => if let (true, Some(fd)) = (matches.is_present("channels"), response.forge_data) {
Some(get_table(
fd.channels
.iter()
.sorted_by(|a, b| a.res.cmp(&b.res))
.map(|c| (&*c.res, &*c.version)),
true
))
} else {
None
}
}; };
if let Some(img) = image { if let Some(img) = image {
@ -191,39 +209,3 @@ async fn asciify_base64_image(favicon: String, config: AsciiConfig) -> Result<St
Ok(out) Ok(out)
} }
/// formats a ModInfo to a readable list of mods
///
/// if `version_info`, the version of the mods will also be displayed
fn get_modlist(list: ModInfo, version_info: bool) -> String {
let infos = match list {
ModInfo::Forge { mod_list: l } => l,
};
// the width at which | characters should be placed this is the length of the
// longest modid
let max_width = if version_info {
infos
.iter()
.map(|m| m.modid.len())
.max()
.unwrap_or_default()
} else {
// this will not be used in case version_info is off so we just use 0
0
};
infos
// we use into_iter instead of iter because a String cannot be collected from &String
// and since we don't need infos again
.into_iter()
.map(|m| {
if version_info {
format!("{: <width$} | {}", m.modid, m.version, width = max_width)
} else {
m.modid
}
})
.intersperse("\n".to_owned())
.collect()
}