use std::error::Error; use std::fs; use std::net::SocketAddr; use std::time::Duration; 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()) } #[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(); 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; }; }); while let Ok((mut inbound, _)) = listener.accept().await { let mut outbound = TcpStream::connect(&server_addr).await?; tokio::spawn(async move { if let Err(e) = copy_bidirectional(&mut inbound, &mut outbound).await { println!("Failed to transfer; error={e}"); } }); } Ok(()) }