mc-minder 0.2.0

A smart management suite for Minecraft Fabric servers on Termux/Android
Documentation
use anyhow::{Result, Context};
use clap::Parser;
use log::{info, warn, error};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{mpsc, Notify, RwLock};

mod config;
mod monitor;
mod ai;
mod rcon;
mod context;
mod api;

use config::Config;
use monitor::{LogMonitor, LogEvent};
use ai::AiClient;
use rcon::RconClient;
use context::ContextManager;
use api::HttpApi;

const DEFAULT_LOG_FILE: &str = "logs/latest.log";

#[derive(Parser, Debug)]
#[command(name = "mc-minder")]
#[command(author = "SharkMI-0x7E")]
#[command(version = "0.2.0")]
#[command(about = "A smart management suite for Minecraft Fabric servers")]
struct Args {
    #[arg(short, long, value_name = "PATH", default_value = "../config.toml")]
    config: PathBuf,

    #[arg(short, long)]
    verbose: bool,

    #[arg(long, default_value = "8080")]
    http_port: u16,

    #[arg(long, default_value = DEFAULT_LOG_FILE)]
    log_file: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    let args = Args::parse();

    env_logger::Builder::new()
        .filter_level(if args.verbose {
            log::LevelFilter::Debug
        } else {
            log::LevelFilter::Info
        })
        .init();

    info!("MC-Minder starting up...");

    let config_path = args.config.clone();
    let config = Config::load(&config_path)
        .with_context(|| format!("Failed to load config from {:?}", config_path))?;

    info!("Configuration loaded successfully");

    let log_path = PathBuf::from(&args.log_file);
    let log_monitor = LogMonitor::new(log_path)?;

    let ai_client = if let Some(ref ai_config) = config.ai {
        Some(AiClient::new(ai_config.clone(), config.ollama.clone())?)
    } else {
        warn!("No AI configuration found, AI features disabled");
        None
    };

    let mut rcon_client = RconClient::new(
        config.rcon.host.clone(),
        config.rcon.port,
        config.rcon.password.clone(),
    );

    let context = Arc::new(ContextManager::new());
    context.add_system_message("You are a helpful Minecraft server assistant. Respond concisely and helpfully to player questions. Keep responses under 100 characters when possible.");

    let rcon = Arc::new(RwLock::new(None));
    {
        let mut rcon_guard = rcon.write().await;
        match rcon_client.connect() {
            Ok(_) => {
                *rcon_guard = Some(rcon_client);
                info!("RCON connection established");
            }
            Err(e) => {
                warn!("Failed to connect to RCON: {}. Will retry later.", e);
            }
        }
    }

    let (event_tx, mut event_rx) = mpsc::channel::<LogEvent>(100);
    let shutdown = Arc::new(Notify::new());
    let shutdown_clone = shutdown.clone();

    let monitor_handle = tokio::spawn(async move {
        if let Err(e) = log_monitor.start_monitoring(event_tx, shutdown_clone).await {
            error!("Log monitor error: {}", e);
        }
    });

    let http_api = Arc::new(HttpApi::new(args.http_port, context.clone(), rcon.clone()));
    let http_handle = tokio::spawn(async move {
        if let Err(e) = http_api.start().await {
            error!("HTTP API error: {}", e);
        }
    });

    info!("MC-Minder is running. Press Ctrl+C to stop.");

    let trigger = ai_client.as_ref().map(|a| a.get_trigger().to_string());

    while let Some(event) = event_rx.recv().await {
        match event {
            LogEvent::Chat(msg) => {
                info!("[Chat] {}: {}", msg.player, msg.content);

                if let (Some(ref ai), Some(ref trig)) = (&ai_client, &trigger) {
                    if msg.content.starts_with(trig) {
                        let question = msg.content.trim_start_matches(trig).trim();
                        if !question.is_empty() {
                            context.add_user_message(question, &msg.player);

                            let messages = context.get_messages_for_player(&msg.player);
                            
                            match ai.chat(messages).await {
                                Ok(response) => {
                                    context.add_assistant_message(&response);
                                    
                                    let mut rcon_guard = rcon.write().await;
                                    if let Some(ref mut rcon_client) = *rcon_guard {
                                        let tell_msg = format!("[AI] {}", response);
                                        if let Err(e) = rcon_client.tell(&msg.player, &tell_msg) {
                                            warn!("Failed to send AI response: {}", e);
                                        }
                                    }
                                }
                                Err(e) => {
                                    warn!("AI chat error: {}", e);
                                }
                            }
                        }
                    }
                }
            }
            LogEvent::PlayerJoin(player) => {
                info!("[Join] {} joined the game", player);
                let mut rcon_guard = rcon.write().await;
                if let Some(ref mut rcon_client) = *rcon_guard {
                    let _ = rcon_client.say(&format!("Welcome {}!", player));
                }
            }
            LogEvent::PlayerLeave(player) => {
                info!("[Leave] {} left the game", player);
            }
            LogEvent::PlayerDeath(player) => {
                info!("[Death] {} died", player);
            }
            LogEvent::ServerStart => {
                info!("[Server] Server started");
            }
            LogEvent::ServerStop => {
                info!("[Server] Server stopped");
            }
        }
    }

    shutdown.notify_waiters();
    let _ = tokio::try_join!(monitor_handle, http_handle);

    info!("MC-Minder stopped");
    Ok(())
}