Files
mudserver/src/jsonrpc.rs
AI Agent f183daa16c
Some checks failed
Smoke tests / Build and smoke test (push) Failing after 1m2s
Feature: dynamic command discovery for JSON-RPC and enhanced testing
2026-03-19 15:22:39 -06:00

168 lines
5.4 KiB
Rust

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<serde_json::Value>,
id: Option<serde_json::Value>,
}
#[derive(Serialize)]
struct JsonRpcResponse {
jsonrpc: String,
result: Option<serde_json::Value>,
error: Option<serde_json::Value>,
id: Option<serde_json::Value>,
}
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::<usize, String>::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<Mutex<HashMap<usize, String>>>,
) {
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<usize> = 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<Mutex<HashMap<usize, String>>>,
current_player_id: &mut Option<usize>,
) -> 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::<usize>();
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()
}