168 lines
5.4 KiB
Rust
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()
|
|
}
|