pub mod inspector;
pub mod mcp;
use std::sync::mpsc;
#[derive(Debug, Clone)]
pub struct CaptureFrameOptions {
pub scale: f32,
pub region: Option<(u32, u32, u32, u32)>,
}
impl Default for CaptureFrameOptions {
fn default() -> Self {
Self {
scale: 1.0,
region: None,
}
}
}
impl CaptureFrameOptions {
pub fn estimate_size(&self, base_width: u32, base_height: u32) -> usize {
let (w, h) = if let Some((_, _, rw, rh)) = self.region {
(rw, rh)
} else {
(base_width, base_height)
};
let scaled_w = (w as f32 * self.scale).ceil() as usize;
let scaled_h = (h as f32 * self.scale).ceil() as usize;
(scaled_w * scaled_h * 2) + 1024 }
}
#[derive(Debug)]
pub enum InspectorRequest {
Health,
GetState { path: Option<String> },
Describe { verbosity: Option<String> },
ListActions,
ExecuteAction { name: String, payload: String },
Rewind { steps: u32 },
Simulate { action: String },
GetHistory,
GetFrameStats,
CaptureFrame { options: CaptureFrameOptions },
}
#[derive(Debug)]
pub struct InspectorResponse {
pub status: u16,
pub content_type: String,
pub body: String,
}
impl InspectorResponse {
pub fn json(body: String) -> Self {
Self {
status: 200,
content_type: "application/json".into(),
body,
}
}
pub fn text(body: String) -> Self {
Self {
status: 200,
content_type: "text/plain".into(),
body,
}
}
pub fn error(status: u16, message: String) -> Self {
Self {
status,
content_type: "application/json".into(),
body: format!("{{\"error\":\"{message}\"}}"),
}
}
}
pub type ResponseSender = mpsc::Sender<InspectorResponse>;
pub type InspectorMessage = (InspectorRequest, ResponseSender);
pub type RequestSender = mpsc::Sender<InspectorMessage>;
pub type RequestReceiver = mpsc::Receiver<InspectorMessage>;
pub fn inspector_channel() -> (RequestSender, RequestReceiver) {
mpsc::channel()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inspector_channel_round_trip() {
let (tx, rx) = inspector_channel();
let (resp_tx, resp_rx) = mpsc::channel();
tx.send((InspectorRequest::Health, resp_tx)).unwrap();
let (req, sender) = rx.recv().unwrap();
assert!(matches!(req, InspectorRequest::Health));
sender
.send(InspectorResponse::json("{\"status\":\"ok\"}".into()))
.unwrap();
let response = resp_rx.recv().unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.content_type, "application/json");
assert!(response.body.contains("ok"));
}
#[test]
fn inspector_response_constructors() {
let json = InspectorResponse::json("{\"key\":1}".into());
assert_eq!(json.status, 200);
assert_eq!(json.content_type, "application/json");
let text = InspectorResponse::text("hello".into());
assert_eq!(text.status, 200);
assert_eq!(text.content_type, "text/plain");
let err = InspectorResponse::error(404, "not found".into());
assert_eq!(err.status, 404);
assert!(err.body.contains("not found"));
}
#[test]
fn inspector_request_variants() {
let requests = vec![
InspectorRequest::Health,
InspectorRequest::GetState { path: None },
InspectorRequest::GetState {
path: Some("player.hp".into()),
},
InspectorRequest::Describe {
verbosity: Some("full".into()),
},
InspectorRequest::ListActions,
InspectorRequest::ExecuteAction {
name: "move".into(),
payload: "{}".into(),
},
InspectorRequest::Rewind { steps: 3 },
InspectorRequest::Simulate {
action: "attack".into(),
},
InspectorRequest::GetHistory,
InspectorRequest::GetFrameStats,
InspectorRequest::CaptureFrame { options: CaptureFrameOptions::default() },
];
assert_eq!(requests.len(), 11);
}
#[test]
fn capture_frame_options_estimate_size() {
let opts = CaptureFrameOptions { scale: 0.5, region: None };
let size = opts.estimate_size(800, 600);
assert!(size > 200_000 && size < 300_000);
}
#[test]
fn capture_frame_options_with_region() {
let opts = CaptureFrameOptions { scale: 1.0, region: Some((0, 0, 400, 300)) };
let size = opts.estimate_size(800, 600);
assert!(size > 200_000 && size < 300_000);
}
}