diff --git a/Cargo.lock b/Cargo.lock index f136fbf..e063508 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -613,7 +613,7 @@ dependencies = [ [[package]] name = "glados" -version = "0.2.0" +version = "0.2.1" dependencies = [ "dotenv", "influx_db_client", diff --git a/Cargo.toml b/Cargo.toml index 8caedd1..dbc76df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "glados" -version = "0.2.0" +version = "0.2.1" edition = "2021" license = "MIT" diff --git a/src/conf.rs b/src/conf.rs index b745f98..d710eb8 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -11,7 +11,7 @@ lazy_static! { #[derive(Debug, StructOpt)] pub struct Args { - #[structopt(long, env = "DATA_COLLECTOR_ENABLE", takes_value = false)] + #[structopt(long, env = "DATA_COLLECTOR_ENABLE", takes_value = false, requires_all = &["influx-host", "influx-db", "influx-user", "influx-password"])] pub enable_data_collector: bool, #[structopt(long = "token", diff --git a/src/error.rs b/src/error.rs index 58ea4cd..7028394 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,6 +24,8 @@ pub enum Error { RconListCmd(#[source] nom::Err>), #[error("Parsing results of `whois` command: {_0}")] RconWhoIsCmd(#[source] nom::Err>), + #[error("Parsing results of `paper chunkinfo` command: {_0}")] + RconWorldInfoCmd(#[source] nom::Err>), #[error("Parsing member filter of `~filter` command: {_0}")] ParseMemberFilter(#[source] nom::Err>), #[error("Unknown role {_0:?} in filter")] diff --git a/src/tracking/status.rs b/src/tracking/status.rs index 8a6b3e9..ef1dd23 100644 --- a/src/tracking/status.rs +++ b/src/tracking/status.rs @@ -3,6 +3,7 @@ use serenity::client::Context; use std::collections::HashMap; +mod world_info; mod player_info; mod tps; @@ -14,6 +15,7 @@ use crate::{ }; use self::{ + world_info::{WorldInfo, WorldInfoCmd}, player_info::{ListCmd, PlayerInfo}, tps::{Tps, TpsCmd}, }; @@ -23,6 +25,7 @@ pub struct Status { pub amount_online: u32, /// List of players associated with their online status. pub players: HashMap, + pub world_info: WorldInfo, } impl Status { @@ -45,11 +48,13 @@ impl Status { for (_ign, uuid) in online_players { players.insert(MinecraftPlayer::from_uuid(ctx, uuid).await?, true); } + let world_info = RconHandle::cmd(&WorldInfoCmd, ctx).await?; Ok(Status { tps, amount_online, players, + world_info, }) } @@ -58,7 +63,84 @@ impl Status { let status = Point::new("status") .add_field("tps", self.tps.min1) // Safe, since we started with u32 - .add_field("player_count", self.amount_online as i64); + .add_field("player_count", self.amount_online as i64) + .add_field( + "world_total_chunks", + self.world_info.overworld.total_chunks as i64, + ) + .add_field( + "world_inactive_chunks", + self.world_info.overworld.inactive_chunks as i64, + ) + .add_field( + "world_border_chunks", + self.world_info.overworld.border_chunks as i64, + ) + .add_field( + "world_ticking_chunks", + self.world_info.overworld.ticking_chunks as i64, + ) + .add_field( + "world_entity_chunks", + self.world_info.overworld.entity_chunks as i64, + ) + .add_field( + "nether_total_chunks", + self.world_info.nether.total_chunks as i64, + ) + .add_field( + "nether_inactive_chunks", + self.world_info.nether.inactive_chunks as i64, + ) + .add_field( + "nether_border_chunks", + self.world_info.nether.border_chunks as i64, + ) + .add_field( + "nether_ticking_chunks", + self.world_info.nether.ticking_chunks as i64, + ) + .add_field( + "nether_entity_chunks", + self.world_info.nether.entity_chunks as i64, + ) + .add_field( + "the_end_total_chunks", + self.world_info.the_end.total_chunks as i64, + ) + .add_field( + "the_end_inactive_chunks", + self.world_info.the_end.inactive_chunks as i64, + ) + .add_field( + "the_end_border_chunks", + self.world_info.the_end.border_chunks as i64, + ) + .add_field( + "the_end_ticking_chunks", + self.world_info.the_end.ticking_chunks as i64, + ) + .add_field( + "the_end_entity_chunks", + self.world_info.the_end.entity_chunks as i64, + ) + .add_field("all_total_chunks", self.world_info.all.total_chunks as i64) + .add_field( + "all_inactive_chunks", + self.world_info.all.inactive_chunks as i64, + ) + .add_field( + "all_border_chunks", + self.world_info.all.border_chunks as i64, + ) + .add_field( + "all_ticking_chunks", + self.world_info.all.ticking_chunks as i64, + ) + .add_field( + "all_entity_chunks", + self.world_info.all.entity_chunks as i64, + ); points.push(status); let mut activity = Point::new("activity"); for (player, is_online) in &self.players { diff --git a/src/tracking/status/world_info.rs b/src/tracking/status/world_info.rs new file mode 100644 index 0000000..37283bb --- /dev/null +++ b/src/tracking/status/world_info.rs @@ -0,0 +1,118 @@ +use std::marker::PhantomData; + +use nom::{ + bytes::complete::tag, + character::complete::char, + combinator::{map, opt, recognize}, + sequence::{pair, preceded, tuple}, + IResult, Parser, +}; + +use crate::{ + error::{Error, Result}, + rcon::RconCommand, +}; + +pub struct WorldInfoCmd; + +pub struct WorldInfo { + pub overworld: WorldInfoPart, + pub nether: WorldInfoPart, + pub the_end: WorldInfoPart, + pub all: WorldInfoPart, +} + +pub trait World { + const NAME: &'static str; +} + +macro_rules! world { + ($ident:ident: $name:literal) => { + pub struct $ident; + impl World for $ident { + const NAME: &'static str = $name; + } + }; +} + +world!(Overworld: "world"); +world!(Nether: "world_nether"); +world!(TheEnd: "world_the_end"); +world!(AllWorlds: "all listed worlds"); + +pub struct WorldInfoPart { + pub total_chunks: u32, + pub inactive_chunks: u32, + pub border_chunks: u32, + pub ticking_chunks: u32, + pub entity_chunks: u32, + _world: PhantomData, +} + +impl RconCommand for WorldInfoCmd { + type Output = WorldInfo; + + fn as_string(&self) -> String { + String::from("paper chunkinfo") + } + + fn parse(&self, raw: String) -> Result { + parse_chunkinfo_result(&raw) + .map(|(_, world_info)| world_info) + .map_err(|why| Error::RconWorldInfoCmd(why.to_owned())) + } +} + +fn parse_chunkinfo_result(inp: &str) -> IResult<&str, WorldInfo> { + let world = WorldInfoPart::::parse; + let world_nether = WorldInfoPart::::parse; + let world_the_end = WorldInfoPart::::parse; + let all_worlds = WorldInfoPart::::parse; + map( + tuple((world, world_nether, world_the_end, all_worlds)), + |(overworld, nether, the_end, all)| WorldInfo { + overworld, + nether, + the_end, + all, + }, + )(inp) +} + +impl WorldInfoPart { + pub fn parse(inp: &str) -> IResult<&str, Self> { + preceded(Self::parse_headline, Self::parse_info)(inp) + } + + fn parse_headline(inp: &str) -> IResult<&str, &str> { + recognize(tuple((tag("§9Chunks in §a"), tag(W::NAME), tag("§3:\n"))))(inp) + } + + fn parse_info<'i>(inp: &'i str) -> IResult<&'i str, Self> { + let total = Self::parse_entry("Total"); + let inactive = Self::parse_entry("Inactive"); + let border = Self::parse_entry("Border"); + let ticking = Self::parse_entry("Ticking"); + let entity = Self::parse_entry("Entity"); + map( + tuple((total, inactive, border, ticking, entity, char('\n'))), + |(total, inactive, border, ticking, entity, _)| Self { + total_chunks: total, + inactive_chunks: inactive, + border_chunks: border, + ticking_chunks: ticking, + entity_chunks: entity, + _world: PhantomData, + }, + )(inp) + } + + fn parse_entry<'i, E>(key: &'static str) -> impl Parser<&'i str, u32, E> + where + E: nom::error::ParseError<&'i str>, + { + use nom::character::complete::u32; + let key = preceded(pair(tag("§9"), opt(char(' '))), tag(key)); + map(tuple((key, tag(": §3"), u32)), |(_, _, num)| num) + } +}