From 84b56a163a7de06364b810043dce1f5bc9dfbd96 Mon Sep 17 00:00:00 2001 From: Lukrecja Date: Mon, 2 Feb 2026 15:52:17 +0100 Subject: [PATCH] refractor into smaller modules --- src/config.rs | 22 ++++++++ src/main.rs | 149 ++++++------------------------------------------- src/monitor.rs | 43 ++++++++++++++ src/motd.rs | 62 ++++++++++++++++++++ src/proxy.rs | 29 ++++++++++ src/rcon.rs | 37 ++++++++++++ 6 files changed, 210 insertions(+), 132 deletions(-) create mode 100644 src/config.rs create mode 100644 src/monitor.rs create mode 100644 src/motd.rs create mode 100644 src/proxy.rs create mode 100644 src/rcon.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4f82863 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,22 @@ +use std::error::Error; +use std::fs; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct Config { + pub listen_addr: String, + pub server_addr: String, + pub motd_server_addr: String, + pub rcon_addr: String, + pub rcon_password: String, + pub idle_timeout_secs: u64, + pub polling_interval_millis: u64, +} + +impl Config { + pub fn load(path: &str) -> Result> { + let content = fs::read_to_string(path)?; + let config: Config = toml::from_str(&content)?; + Ok(config) + } +} diff --git a/src/main.rs b/src/main.rs index 1806f94..082cb95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,144 +1,29 @@ +mod config; +mod motd; +mod monitor; +mod proxy; +mod rcon; + use std::error::Error; -use std::fs; -use std::net::SocketAddr; -use std::time::Duration; +use tokio::main; -use rcon::Connection; -use serde::Deserialize; -use tokio::time::Instant; -use tokio::{io::copy_bidirectional, main}; -use tokio::net::{TcpListener, TcpStream}; -use valence::network::{ - async_trait, BroadcastToLan, CleanupFn, ConnectionMode, HandshakeData, ServerListPing, -}; - -use valence::prelude::*; -use valence::MINECRAFT_VERSION; - -#[derive(Debug, Deserialize)] -struct Config { - listen_addr: String, - server_addr: String, - motd_server_addr: String, - rcon_addr: String, - rcon_password: String, - idle_timeout_secs: u64, - polling_interval_millis: u64, -} - -impl Config { - fn load(path: &str) -> Result> { - let content = fs::read_to_string(path)?; - let config: Config = toml::from_str(&content)?; - Ok(config) - } -} - -struct MotdCallbacks; - -#[async_trait] -impl NetworkCallbacks for MotdCallbacks { - async fn server_list_ping( - &self, - _shared: &SharedNetworkState, - _remote_addr: SocketAddr, - handshake_data: &HandshakeData, - ) -> ServerListPing { - ServerListPing::Respond { - online_players: 0, - max_players: 0, - player_sample: vec![], - description: "Serwer jest ".into_text() + - "wyłączony".into_text().color(Color::rgb(250, 50, 50)) + "!\n" + - "Dołącz aby uruchomić serwer! ".into_text().color(Color::rgb(255, 150, 230)) + - format!("⏲ {}s", 20).into_text().color(Color::rgb(80, 80, 80)), - favicon_png: include_bytes!("../assets/icon.png"), - version_name: MINECRAFT_VERSION.to_string(), - protocol: handshake_data.protocol_version, - } - } - - async fn broadcast_to_lan(&self, _shared: &SharedNetworkState) -> BroadcastToLan { - BroadcastToLan::Enabled("Hello Valence!".into()) - } - - async fn login( - &self, - _shared: &SharedNetworkState, - _info: &NewClientInfo, - ) -> Result { - Err("You are not meant to join this example".color(Color::rgb(250, 30, 21))) - } -} - -fn parse_players_online(s: &str) -> Option { - s.split_whitespace() - .find_map(|tok| tok.parse::().ok()) -} +use config::Config; #[main] async fn main() -> Result<(), Box> { let config = Config::load("config.toml")?; - App::new() - .insert_resource(NetworkSettings { - connection_mode: ConnectionMode::Offline, - callbacks: MotdCallbacks.into(), - address: config.motd_server_addr.parse().unwrap(), - ..Default::default() - }) - .add_plugins(DefaultPlugins) - .run(); - - println!("Listening on {}", config.listen_addr); - println!("Proxying to {}", config.server_addr); - println!("Motd server running on {}", config.motd_server_addr); - - let listener = TcpListener::bind(&config.listen_addr).await?; - let server_addr = config.server_addr.clone(); - + let motd_config = config.clone(); tokio::spawn(async move { - let mut conn = >::builder() - .enable_minecraft_quirks(true) - .connect(&config.rcon_addr, &config.rcon_password) - .await.unwrap(); - - let mut idle = false; - let mut last_online = Instant::now(); - let idle_timeout = Duration::from_secs(config.idle_timeout_secs); - let polling_interval = Duration::from_millis(config.polling_interval_millis); - - loop { - let players_cmd_output = conn.cmd("list").await.unwrap(); - let players_number = parse_players_online(&players_cmd_output).unwrap(); - - if players_number > 0 { - last_online = Instant::now(); - } - - println!("{players_number} {idle}"); - - if Instant::now() - last_online > idle_timeout { - idle = true; - println!("Stopping the server"); - conn.cmd("stop").await.unwrap(); - } else { - idle = false; - } - - tokio::time::sleep(polling_interval).await; - }; + motd::create_motd_server(&motd_config).run(); }); - while let Ok((mut inbound, _)) = listener.accept().await { - let mut outbound = TcpStream::connect(&server_addr).await?; + let monitor_config = config.clone(); + tokio::spawn(async move { + if let Err(e) = monitor::run_idle_monitor(monitor_config).await { + eprintln!("Idle monitor error: {e}"); + } + }); - tokio::spawn(async move { - if let Err(e) = copy_bidirectional(&mut inbound, &mut outbound).await { - println!("Failed to transfer; error={e}"); - } - }); - } - - Ok(()) + proxy::run_proxy(config.listen_addr, config.server_addr).await } diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..3e0f91b --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,43 @@ +use std::error::Error; +use std::time::Duration; +use tokio::time::Instant; + +use crate::config::Config; +use crate::rcon; + +fn parse_players_online(s: &str) -> Option { + s.split_whitespace() + .find_map(|tok| tok.parse::().ok()) +} + +pub async fn run_idle_monitor(config: Config) -> Result<(), Box> { + let mut conn = rcon::connect_rcon(&config.rcon_addr, &config.rcon_password).await?; + + let mut idle = false; + let mut last_online = Instant::now(); + let idle_timeout = Duration::from_secs(config.idle_timeout_secs); + let polling_interval = Duration::from_millis(config.polling_interval_millis); + + loop { + let players_cmd_output = conn.cmd("list").await?; + let players_number = parse_players_online(&players_cmd_output).unwrap_or(0); + + if players_number > 0 { + last_online = Instant::now(); + } + + println!("{players_number} {idle}"); + + if Instant::now() - last_online > idle_timeout { + if !idle { + idle = true; + println!("Stopping the server"); + conn.cmd("stop").await?; + } + } else { + idle = false; + } + + tokio::time::sleep(polling_interval).await; + } +} diff --git a/src/motd.rs b/src/motd.rs new file mode 100644 index 0000000..d8e2812 --- /dev/null +++ b/src/motd.rs @@ -0,0 +1,62 @@ +use std::net::SocketAddr; +use valence::network::{ + async_trait, BroadcastToLan, CleanupFn, ConnectionMode, HandshakeData, ServerListPing, +}; +use valence::prelude::*; +use valence::MINECRAFT_VERSION; + +use crate::config::Config; + +struct MotdCallbacks; + +#[async_trait] +impl NetworkCallbacks for MotdCallbacks { + async fn server_list_ping( + &self, + _shared: &SharedNetworkState, + _remote_addr: SocketAddr, + handshake_data: &HandshakeData, + ) -> ServerListPing { + ServerListPing::Respond { + online_players: 0, + max_players: 0, + player_sample: vec![], + description: "Serwer jest ".into_text() + + "wyłączony".into_text().color(Color::rgb(250, 50, 50)) + + "!\n" + + "Dołącz aby uruchomić serwer! " + .into_text() + .color(Color::rgb(255, 150, 230)) + + format!("⏲ {}s", 20) + .into_text() + .color(Color::rgb(80, 80, 80)), + favicon_png: include_bytes!("../assets/icon.png"), + version_name: MINECRAFT_VERSION.to_string(), + protocol: handshake_data.protocol_version, + } + } + + async fn broadcast_to_lan(&self, _shared: &SharedNetworkState) -> BroadcastToLan { + BroadcastToLan::Enabled("Hello Valence!".into()) + } + + async fn login( + &self, + _shared: &SharedNetworkState, + _info: &NewClientInfo, + ) -> Result { + Err("You are not meant to join this example".color(Color::rgb(250, 30, 21))) + } +} + +pub fn create_motd_server(config: &Config) -> App { + let mut app = App::new(); + app.insert_resource(NetworkSettings { + connection_mode: ConnectionMode::Offline, + callbacks: MotdCallbacks.into(), + address: config.motd_server_addr.parse().unwrap(), + ..Default::default() + }) + .add_plugins(DefaultPlugins); + app +} diff --git a/src/proxy.rs b/src/proxy.rs new file mode 100644 index 0000000..ab9d24e --- /dev/null +++ b/src/proxy.rs @@ -0,0 +1,29 @@ +use std::error::Error; +use tokio::io::copy_bidirectional; +use tokio::net::{TcpListener, TcpStream}; + +pub async fn run_proxy(listen_addr: String, server_addr: String) -> Result<(), Box> { + println!("Listening on {}", listen_addr); + println!("Proxying to {}", server_addr); + + let listener = TcpListener::bind(&listen_addr).await?; + + while let Ok((mut inbound, _)) = listener.accept().await { + let server_addr = server_addr.clone(); + + tokio::spawn(async move { + match TcpStream::connect(&server_addr).await { + Ok(mut outbound) => { + if let Err(e) = copy_bidirectional(&mut inbound, &mut outbound).await { + println!("Failed to transfer; error={e}"); + } + } + Err(e) => { + println!("Failed to connect to server; error={e}"); + } + } + }); + } + + Ok(()) +} diff --git a/src/rcon.rs b/src/rcon.rs new file mode 100644 index 0000000..b7de686 --- /dev/null +++ b/src/rcon.rs @@ -0,0 +1,37 @@ +use std::error::Error; +use std::time::Duration; +use rcon::Connection; +use tokio::net::TcpStream; + +pub async fn connect_rcon( + addr: &str, + password: &str, +) -> Result, Box> { + let conn = >::builder() + .enable_minecraft_quirks(true) + .connect(addr, password) + .await?; + Ok(conn) +} + +pub async fn wait_for_rcon( + addr: &str, + password: &str, + timeout: Duration, + retry_interval: Duration, +) -> Result, Box> { + let start = tokio::time::Instant::now(); + + loop { + if tokio::time::Instant::now() - start > timeout { + return Err("RCON connection timeout".into()); + } + + match connect_rcon(addr, password).await { + Ok(conn) => return Ok(conn), + Err(_) => { + tokio::time::sleep(retry_interval).await; + } + } + } +}