1pub mod inspector;
2pub mod mcp;
3
4use std::sync::mpsc;
5
6#[derive(Debug, Clone)]
8pub struct CaptureFrameOptions {
9 pub scale: f32,
11 pub region: Option<(u32, u32, u32, u32)>,
13}
14
15impl Default for CaptureFrameOptions {
16 fn default() -> Self {
17 Self {
18 scale: 1.0,
19 region: None,
20 }
21 }
22}
23
24impl CaptureFrameOptions {
25 pub fn estimate_size(&self, base_width: u32, base_height: u32) -> usize {
27 let (w, h) = if let Some((_, _, rw, rh)) = self.region {
28 (rw, rh)
29 } else {
30 (base_width, base_height)
31 };
32 let scaled_w = (w as f32 * self.scale).ceil() as usize;
33 let scaled_h = (h as f32 * self.scale).ceil() as usize;
34 (scaled_w * scaled_h * 2) + 1024 }
37}
38
39#[derive(Debug)]
41pub enum InspectorRequest {
42 Health,
43 GetState { path: Option<String> },
44 Describe { verbosity: Option<String> },
45 ListActions,
46 ExecuteAction { name: String, payload: String },
47 Rewind { steps: u32 },
48 Simulate { action: String },
49 GetHistory,
50 GetFrameStats,
51 CaptureFrame { options: CaptureFrameOptions },
52}
53
54#[derive(Debug)]
56pub struct InspectorResponse {
57 pub status: u16,
58 pub content_type: String,
59 pub body: String,
60}
61
62impl InspectorResponse {
63 pub fn json(body: String) -> Self {
64 Self {
65 status: 200,
66 content_type: "application/json".into(),
67 body,
68 }
69 }
70
71 pub fn text(body: String) -> Self {
72 Self {
73 status: 200,
74 content_type: "text/plain".into(),
75 body,
76 }
77 }
78
79 pub fn error(status: u16, message: String) -> Self {
80 Self {
81 status,
82 content_type: "application/json".into(),
83 body: format!("{{\"error\":\"{message}\"}}"),
84 }
85 }
86}
87
88pub type ResponseSender = mpsc::Sender<InspectorResponse>;
90
91pub type InspectorMessage = (InspectorRequest, ResponseSender);
93
94pub type RequestSender = mpsc::Sender<InspectorMessage>;
96
97pub type RequestReceiver = mpsc::Receiver<InspectorMessage>;
99
100pub fn inspector_channel() -> (RequestSender, RequestReceiver) {
102 mpsc::channel()
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn inspector_channel_round_trip() {
111 let (tx, rx) = inspector_channel();
112
113 let (resp_tx, resp_rx) = mpsc::channel();
115 tx.send((InspectorRequest::Health, resp_tx)).unwrap();
116
117 let (req, sender) = rx.recv().unwrap();
119 assert!(matches!(req, InspectorRequest::Health));
120 sender
121 .send(InspectorResponse::json("{\"status\":\"ok\"}".into()))
122 .unwrap();
123
124 let response = resp_rx.recv().unwrap();
126 assert_eq!(response.status, 200);
127 assert_eq!(response.content_type, "application/json");
128 assert!(response.body.contains("ok"));
129 }
130
131 #[test]
132 fn inspector_response_constructors() {
133 let json = InspectorResponse::json("{\"key\":1}".into());
134 assert_eq!(json.status, 200);
135 assert_eq!(json.content_type, "application/json");
136
137 let text = InspectorResponse::text("hello".into());
138 assert_eq!(text.status, 200);
139 assert_eq!(text.content_type, "text/plain");
140
141 let err = InspectorResponse::error(404, "not found".into());
142 assert_eq!(err.status, 404);
143 assert!(err.body.contains("not found"));
144 }
145
146 #[test]
147 fn inspector_request_variants() {
148 let requests = vec![
150 InspectorRequest::Health,
151 InspectorRequest::GetState { path: None },
152 InspectorRequest::GetState {
153 path: Some("player.hp".into()),
154 },
155 InspectorRequest::Describe {
156 verbosity: Some("full".into()),
157 },
158 InspectorRequest::ListActions,
159 InspectorRequest::ExecuteAction {
160 name: "move".into(),
161 payload: "{}".into(),
162 },
163 InspectorRequest::Rewind { steps: 3 },
164 InspectorRequest::Simulate {
165 action: "attack".into(),
166 },
167 InspectorRequest::GetHistory,
168 InspectorRequest::GetFrameStats,
169 InspectorRequest::CaptureFrame { options: CaptureFrameOptions::default() },
170 ];
171 assert_eq!(requests.len(), 11);
172 }
173
174 #[test]
175 fn capture_frame_options_estimate_size() {
176 let opts = CaptureFrameOptions { scale: 0.5, region: None };
177 let size = opts.estimate_size(800, 600);
179 assert!(size > 200_000 && size < 300_000);
180 }
181
182 #[test]
183 fn capture_frame_options_with_region() {
184 let opts = CaptureFrameOptions { scale: 1.0, region: Some((0, 0, 400, 300)) };
185 let size = opts.estimate_size(800, 600);
187 assert!(size > 200_000 && size < 300_000);
188 }
189}