use std::collections::HashMap; use std::sync::Arc; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::Mutex; use serde::{Deserialize, Serialize}; use serde_json::json; use crate::game::SharedState; use crate::commands; #[derive(Deserialize)] struct JsonRpcRequest { _jsonrpc: String, method: String, params: Option, id: Option, } #[derive(Serialize)] struct JsonRpcResponse { jsonrpc: String, result: Option, error: Option, id: Option, } pub async fn run_jsonrpc_server(state: SharedState, port: u16) { let listener = TcpListener::bind(("0.0.0.0", port)).await.unwrap(); log::info!("JSON-RPC server listening on 0.0.0.0:{port}"); let sessions = Arc::new(Mutex::new(HashMap::::new())); loop { let (stream, addr) = match listener.accept().await { Ok(s) => s, Err(e) => { log::error!("Failed to accept connection: {e}"); continue; } }; log::info!("New JSON-RPC connection from {addr:?}"); let state = state.clone(); let sessions = sessions.clone(); tokio::spawn(async move { handle_connection(stream, state, sessions).await; }); } } async fn handle_connection( mut stream: TcpStream, state: SharedState, sessions: Arc>>, ) { let (reader, mut writer) = stream.split(); let mut reader = BufReader::new(reader); let mut line = String::new(); // Map RPC session ID to player ID let mut current_player_id: Option = None; loop { line.clear(); match reader.read_line(&mut line).await { Ok(0) => break, // Connection closed Ok(_) => { let req: JsonRpcRequest = match serde_json::from_str(&line) { Ok(r) => r, Err(e) => { let resp = JsonRpcResponse { jsonrpc: "2.0".to_string(), result: None, error: Some(json!({"code": -32700, "message": format!("Parse error: {e}")})), id: None, }; let _ = writer.write_all(format!("{}\n", serde_json::to_string(&resp).unwrap()).as_bytes()).await; continue; } }; let resp = handle_request(req, &state, &sessions, &mut current_player_id).await; let _ = writer.write_all(format!("{}\n", serde_json::to_string(&resp).unwrap()).as_bytes()).await; } Err(e) => { log::error!("Error reading from JSON-RPC stream: {e}"); break; } } } // Cleanup session if needed if let Some(pid) = current_player_id { let mut st = state.lock().await; st.remove_player(pid); } } async fn handle_request( req: JsonRpcRequest, state: &SharedState, _sessions: &Arc>>, current_player_id: &mut Option, ) -> JsonRpcResponse { let method = req.method.as_str(); let id = req.id.clone(); let result = match method { "login" => { let username = req.params.as_ref() .and_then(|p| p.get("username")) .and_then(|u| u.as_str()) .unwrap_or("anonymous"); let mut st = state.lock().await; let player_id = rand::random::(); let saved = st.db.load_player(username); if let Some(saved) = saved { st.load_existing_player(player_id, saved, None, None); *current_player_id = Some(player_id); json!({"status": "success", "session_id": player_id}) } else { json!({"status": "error", "message": "Player not found. Create character via SSH first."}) } }, "list_commands" => { json!(commands::get_command_list()) }, "execute" => { if let Some(pid) = *current_player_id { let command = req.params.as_ref() .and_then(|p| p.get("command")) .and_then(|c| c.as_str()) .unwrap_or(""); let args = req.params.as_ref() .and_then(|p| p.get("args")) .and_then(|a| a.as_str()) .unwrap_or(""); let input = if args.is_empty() { command.to_string() } else { format!("{command} {args}") }; let result = commands::execute(&input, pid, state).await; json!({ "output": strip_ansi(&result.output), "quit": result.quit }) } else { json!({"error": "Not logged in"}) } }, _ => json!({"error": "Method not found"}), }; JsonRpcResponse { jsonrpc: "2.0".to_string(), result: Some(result), error: None, id, } } fn strip_ansi(text: &str) -> String { let re = regex::Regex::new(r"\x1b\[[0-9;]*[a-zA-Z]").unwrap(); re.replace_all(text, "").to_string() }