refractor into smaller modules

This commit is contained in:
2026-02-02 15:52:17 +01:00
parent 67220e8d11
commit 84b56a163a
6 changed files with 210 additions and 132 deletions

22
src/config.rs Normal file
View File

@@ -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<Self, Box<dyn Error>> {
let content = fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
}

View File

@@ -1,144 +1,29 @@
mod config;
mod motd;
mod monitor;
mod proxy;
mod rcon;
use std::error::Error; use std::error::Error;
use std::fs; use tokio::main;
use std::net::SocketAddr;
use std::time::Duration;
use rcon::Connection; use config::Config;
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<Self, Box<dyn Error>> {
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<CleanupFn, Text> {
Err("You are not meant to join this example".color(Color::rgb(250, 30, 21)))
}
}
fn parse_players_online(s: &str) -> Option<u32> {
s.split_whitespace()
.find_map(|tok| tok.parse::<u32>().ok())
}
#[main] #[main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let config = Config::load("config.toml")?; let config = Config::load("config.toml")?;
App::new() let motd_config = config.clone();
.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 { tokio::spawn(async move {
let mut conn = <Connection<TcpStream>>::builder() motd::create_motd_server(&motd_config).run();
.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 monitor_config = config.clone();
let mut outbound = TcpStream::connect(&server_addr).await?; tokio::spawn(async move {
if let Err(e) = monitor::run_idle_monitor(monitor_config).await {
eprintln!("Idle monitor error: {e}");
}
});
tokio::spawn(async move { proxy::run_proxy(config.listen_addr, config.server_addr).await
if let Err(e) = copy_bidirectional(&mut inbound, &mut outbound).await {
println!("Failed to transfer; error={e}");
}
});
}
Ok(())
} }

43
src/monitor.rs Normal file
View File

@@ -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<u32> {
s.split_whitespace()
.find_map(|tok| tok.parse::<u32>().ok())
}
pub async fn run_idle_monitor(config: Config) -> Result<(), Box<dyn Error>> {
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;
}
}

62
src/motd.rs Normal file
View File

@@ -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<CleanupFn, Text> {
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
}

29
src/proxy.rs Normal file
View File

@@ -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<dyn Error>> {
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(())
}

37
src/rcon.rs Normal file
View File

@@ -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<Connection<TcpStream>, Box<dyn Error>> {
let conn = <Connection<TcpStream>>::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<Connection<TcpStream>, Box<dyn Error>> {
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;
}
}
}
}