use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; use flexi_logger::writers::FileLogWriter; use flexi_logger::{Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode}; use russh::keys::ssh_key::rand_core::OsRng; use russh::server::Server as _; use tokio::net::TcpListener; use mudserver::db; use mudserver::game; use mudserver::ssh; use mudserver::tick; use mudserver::world; const DEFAULT_PORT: u16 = 2222; const DEFAULT_WORLD_DIR: &str = "./world"; const DEFAULT_DB_PATH: &str = "./mudserver.db"; #[tokio::main] async fn main() { let mut port = DEFAULT_PORT; let mut jsonrpc_port = 2223; let mut world_dir = PathBuf::from(DEFAULT_WORLD_DIR); let mut db_path = PathBuf::from(DEFAULT_DB_PATH); let mut log_dir = "logs".to_string(); let mut log_level = "info".to_string(); let args: Vec = std::env::args().collect(); let mut i = 1; while i < args.len() { match args[i].as_str() { "--port" | "-p" => { i += 1; port = args .get(i) .and_then(|s| s.parse().ok()) .expect("--port requires a number"); } "--rpc-port" => { i += 1; jsonrpc_port = args .get(i) .and_then(|s| s.parse().ok()) .expect("--rpc-port requires a number"); } "--world" | "-w" => { i += 1; world_dir = PathBuf::from(args.get(i).expect("--world requires a path")); } "--db" | "-d" => { i += 1; db_path = PathBuf::from(args.get(i).expect("--db requires a path")); } "--log-dir" => { i += 1; log_dir = args.get(i).expect("--log-dir requires a path").to_string(); } "--log-level" => { i += 1; log_level = args.get(i).expect("--log-level requires a level").to_string(); } "--help" => { eprintln!("Usage: mudserver [OPTIONS]"); eprintln!(" --port, -p SSH listen port (default: {DEFAULT_PORT})"); eprintln!(" --rpc-port JSON-RPC listen port (default: 2223)"); eprintln!(" --world, -w World directory (default: {DEFAULT_WORLD_DIR})"); eprintln!(" --db, -d Database path (default: {DEFAULT_DB_PATH})"); eprintln!(" --log-dir Directory for log files (default: logs)"); eprintln!(" --log-level Logging level (default: info)"); std::process::exit(0); } other => { eprintln!("Unknown argument: {other}"); std::process::exit(1); } } i += 1; } // Ensure log directory exists std::fs::create_dir_all(&log_dir).unwrap_or_else(|e| { eprintln!("Failed to create log directory: {e}"); std::process::exit(1); }); // Initialize logger let combat_writer = FileLogWriter::builder(FileSpec::default().directory(&log_dir).basename("combat")) .rotate( Criterion::Size(10_000_000), // 10 MB Naming::Numbers, Cleanup::KeepLogFiles(7), ) .append() .write_mode(WriteMode::Direct) .try_build() .unwrap(); Logger::try_with_str(&log_level) .unwrap() .log_to_file(FileSpec::default().directory(&log_dir).basename("mudserver")) .append() .duplicate_to_stderr(Duplicate::All) .rotate( Criterion::Size(10_000_000), // 10 MB Naming::Numbers, Cleanup::KeepLogFiles(7), ) .write_mode(WriteMode::Direct) .add_writer("combat", Box::new(combat_writer)) .start() .unwrap_or_else(|e| { eprintln!("Failed to initialize logger: {e}"); std::process::exit(1); }); log::info!("Loading world from: {}", world_dir.display()); let loaded_world = world::World::load(&world_dir).unwrap_or_else(|e| { eprintln!("Failed to load world: {e}"); std::process::exit(1); }); log::info!("Opening database: {}", db_path.display()); let database = db::SqliteDb::open(&db_path).unwrap_or_else(|e| { eprintln!("Failed to open database: {e}"); std::process::exit(1); }); let db: Arc = Arc::new(database); let key = russh::keys::PrivateKey::random(&mut OsRng, russh::keys::Algorithm::Ed25519).unwrap(); let config = russh::server::Config { inactivity_timeout: Some(std::time::Duration::from_secs(3600)), auth_rejection_time: std::time::Duration::from_secs(1), auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)), keys: vec![key], ..Default::default() }; let config = Arc::new(config); let state = Arc::new(Mutex::new(game::GameState::new(loaded_world, db))); // Spawn tick engine let tick_state = state.clone(); tokio::spawn(async move { tick::run_tick_engine(tick_state).await; }); // Spawn JSON-RPC server let rpc_state = state.clone(); tokio::spawn(async move { mudserver::jsonrpc::run_jsonrpc_server(rpc_state, jsonrpc_port).await; }); let mut server = ssh::MudServer::new(state); let listener = TcpListener::bind(("0.0.0.0", port)).await.unwrap(); log::info!("MUD server listening on 0.0.0.0:{port}"); log::info!("Connect with: ssh @localhost -p {port}"); server.run_on_socket(config, &listener).await.unwrap(); }