Skip to main content

shuyi_cli/mcp/
attached.rs

1//! The session over the live link (ADR-0047): `start` with `link`
2//! attaches the session to a debug build already running in its window,
3//! and the session tools play a player's input on it and read it as
4//! `attach` does, through `attach`'s own connection. Nothing else in the
5//! build changes: `reset` is refused, and `stop` detaches and leaves it
6//! running.
7//!
8//! The link carries no scene, events or sounds: `scene` answers the
9//! frame's text in their place, and the answers say so in their limits.
10//! `perf` reads the build's frames' records over the link and answers as
11//! a headless session's does, the heap snapshots it takes kept here.
12
13use serde_json::Value;
14use shuyi_platform::link::Phase;
15use shuyi_ui::perf::HeapSnapshot;
16
17use super::host::{PerfTaken, failed, perf_answer, read, to_json};
18use super::{
19    EventsOutput, FramePngInput, FramePngOutput, NoInput, PerfInput, PerfOutput, SceneInput,
20    StartLink, StartOutput, StepInput, StopOutput,
21};
22use crate::contract::attach::{self, AttachInput, Connection, Failed, Seen};
23use crate::contract::codes;
24use crate::contract::perf::PerfRead;
25use crate::contract::run::{self, RunOutput, SceneOutput};
26use crate::contract::session::{LogEntry, Scene};
27use crate::contract::{Answer, Op};
28use crate::frame_png::Step;
29
30/// The limit of an answer that has no scene.
31pub const NO_SCENE: &str = "no scene over the link";
32
33/// The limit of an answer that has no events or sounds.
34pub const NO_EVENTS: &str = "no events over the link";
35
36/// The session's build, reached over the live link.
37pub struct Attached {
38    link: Connection,
39    address: String,
40    token: String,
41    /// Whether the session has ended: stopped, or the link lost.
42    ended: bool,
43    /// The heap snapshots `perf` took of the build, by frame.
44    snapshots: Vec<HeapSnapshot>,
45}
46
47impl Attached {
48    /// The session attached to the build `to` names, and `start`'s answer;
49    /// or `start`'s answer that says why not, with `attach`'s codes.
50    pub fn start(to: &StartLink) -> (Option<Self>, Value) {
51        let op = Op::Start;
52        let mut asked = AttachInput::default();
53        asked.address.clone_from(&to.address);
54        asked.token.clone_from(&to.token);
55        if let Err(e) = asked.check() {
56            return (
57                None,
58                failed(op, codes::WRONG_ARGUMENTS, format!("link: {e}")),
59            );
60        }
61        let attached = Connection::open(&to.address).and_then(|mut link| {
62            let game = attach::hello(&mut link, &to.token)?;
63            let status = attach::status(&mut link)?;
64            Ok((link, game, status))
65        });
66        let (link, game, status) = match attached {
67            Ok(attached) => attached,
68            Err((code, error)) => return (None, failed(op, code, error)),
69        };
70        let output = StartOutput {
71            project: String::new(),
72            game,
73            start: Vec::new(),
74            options: Vec::new(),
75            frames: status.frames,
76            top_screen: status.top_screen,
77            screens: status.screens,
78            session: None,
79        };
80        let limits = vec![format!(
81            "attached to the build at {}: it runs in its window with its own options; {NO_SCENE}, \
82             {NO_EVENTS}, reset is refused, and stop leaves it running",
83            to.address
84        )];
85        let session = Self {
86            link,
87            address: asked.address,
88            token: asked.token,
89            ended: false,
90            snapshots: Vec::new(),
91        };
92        (
93            Some(session),
94            to_json(&Answer::new(op, true, output, limits)),
95        )
96    }
97
98    /// Whether the session has ended: `stop` detached, or the link was
99    /// lost.
100    pub fn ended(&self) -> bool {
101        self.ended
102    }
103
104    /// Session tool `op`'s answer to `input` over the link, and its
105    /// picture, if it has one.
106    pub fn call(&mut self, op: Op, input: Value) -> (Value, Option<String>) {
107        let answered = match op {
108            Op::Step => read(input)
109                .map_err(wrong)
110                .and_then(|input| self.step(&input)),
111            Op::FramePng => read(input)
112                .map_err(wrong)
113                .and_then(|input| self.frame_png(&input)),
114            Op::Scene => read(input).map_err(wrong).and_then(|input: SceneInput| {
115                let output = self.scene(&input)?;
116                Ok((output, None))
117            }),
118            Op::Events => read(input).map_err(wrong).and_then(|NoInput {}| {
119                let output = EventsOutput {
120                    frames: attach::status(&mut self.link)?.frames,
121                    events: Vec::new(),
122                    sounds: Vec::new(),
123                    sounds_added: Vec::new(),
124                };
125                let limits = vec![NO_EVENTS.to_owned()];
126                Ok((to_json(&Answer::new(op, true, output, limits)), None))
127            }),
128            Op::Perf => read(input)
129                .map_err(wrong)
130                .and_then(|input| self.perf(&input))
131                .map(|answer| (to_json(&answer), None)),
132            Op::Reset => Err(wrong(
133                "the link never changes a running build: stop, then start with `project` to run \
134                 the game headless"
135                    .to_owned(),
136            )),
137            Op::Stop => read(input).map_err(wrong).and_then(|NoInput {}| {
138                self.ended = true;
139                let output = StopOutput {
140                    frames: attach::status(&mut self.link)?.frames,
141                };
142                let limits = vec![format!("the build at {} runs on", self.address)];
143                Ok((to_json(&Answer::new(op, true, output, limits)), None))
144            }),
145            _ => Err(wrong(format!("{} is not a session tool", op.name()))),
146        };
147        match answered {
148            Ok(answered) => answered,
149            Err((code, error)) => {
150                if code == codes::LINK_LOST {
151                    self.ended = true;
152                }
153                (failed(op, code, error), None)
154            }
155        }
156    }
157
158    /// `attach`'s input for the build: `steps` and `frames` after.
159    fn input(&self, steps: Vec<Step>, frames: u32) -> AttachInput {
160        let mut input = AttachInput::default();
161        input.address.clone_from(&self.address);
162        input.token.clone_from(&self.token);
163        input.steps = steps;
164        input.frames = frames;
165        input
166    }
167
168    /// `step`: `input` played over the link, answered as `run` answers.
169    fn step(&mut self, input: &StepInput) -> Result<(Value, Option<String>), Failed> {
170        let asked = self.input(input.script.steps.clone(), input.frames);
171        asked.check().map_err(wrong)?;
172        let seen = attach::played(&mut self.link, &asked)?;
173        let mut limits = attach::limits_of(&asked, &seen);
174        if seen.status.quit {
175            limits.push("the build's game has quit".to_owned());
176        }
177        limits.extend([NO_SCENE, NO_EVENTS].map(str::to_owned));
178        let Seen {
179            status, frame, log, ..
180        } = seen;
181        let output = RunOutput {
182            frames: status.frames,
183            top_screen: status.top_screen,
184            screens: status.screens,
185            quit: None,
186            scene: None,
187            frame,
188            events: Vec::new(),
189            sounds: Vec::new(),
190            sounds_added: Vec::new(),
191            log: log.lines.into_iter().map(log_entry).collect(),
192            modules: Vec::new(),
193            perf: None,
194            session: None,
195        };
196        Ok((to_json(&Answer::new(Op::Step, true, output, limits)), None))
197    }
198
199    /// `scene`: the frame's text in place of a scene, and the log; asked
200    /// for a path or a shape, a frame's scene has no data to read them in.
201    fn scene(&mut self, input: &SceneInput) -> Result<Value, Failed> {
202        let asked = self.input(Vec::new(), 0);
203        let seen = attach::played(&mut self.link, &asked)?;
204        let mut limits = attach::limits_of(&asked, &seen);
205        limits.push(NO_SCENE.to_owned());
206        let Seen {
207            status, frame, log, ..
208        } = seen;
209        let scene = (status.phase != Phase::Loading && !frame.is_empty())
210            .then(|| Scene::new(status.top_screen.clone(), "frame", frame));
211        let mut output = SceneOutput {
212            frames: status.frames,
213            top_screen: status.top_screen,
214            scene,
215            log: log.lines.into_iter().map(log_entry).collect(),
216            session: None,
217            value: None,
218            shape: None,
219        };
220        let ok = output.scene.is_some();
221        run::ask_scene(&mut output, &mut limits, input.ask())?;
222        Ok(to_json(&Answer::new(Op::Scene, ok, output, limits)))
223    }
224
225    /// `perf`: the build's frames' records over the link, answered as a
226    /// headless session's; the heap snapshots are kept here.
227    fn perf(&mut self, input: &PerfInput) -> Result<Answer<PerfOutput>, Failed> {
228        let (link, address) = (&mut self.link, &self.address);
229        perf_answer(input, &mut self.snapshots, |snapshot| {
230            let Some(answer) = attach::perf_read(link, input.since, input.max, snapshot)? else {
231                let error = format!("the build at {address} closed the link");
232                return Err((codes::LINK_LOST, error));
233            };
234            let read = PerfRead {
235                records: answer.records,
236                next: answer.next,
237                dropped: answer.dropped,
238                measured: answer.measured,
239            };
240            Ok(PerfTaken {
241                read,
242                snapshot: answer.snapshot,
243                frames: answer.frames,
244                // The build counts its own heap, and nothing of this
245                // program's.
246                process_heap: false,
247            })
248        })
249    }
250
251    /// `frame_png`: the window's picture, at the window's size.
252    fn frame_png(&mut self, input: &FramePngInput) -> Result<(Value, Option<String>), Failed> {
253        if input.scale.is_some() {
254            return Err(wrong(
255                "scale: the window's picture has the window's size".to_owned(),
256            ));
257        }
258        let status = attach::status(&mut self.link)?;
259        let shot = attach::picture(&mut self.link)?;
260        let output = FramePngOutput {
261            frames: status.frames,
262            top_screen: status.top_screen,
263            width: shot.width,
264            height: shot.height,
265        };
266        let limits = shot
267            .limit
268            .map(|limit| format!("the picture: {limit}"))
269            .into_iter()
270            .collect();
271        let answer = to_json(&Answer::new(Op::FramePng, true, output, limits));
272        Ok((answer, Some(shot.png)))
273    }
274}
275
276/// A wrong input's failure: `error`, with [`codes::WRONG_ARGUMENTS`].
277fn wrong(error: String) -> Failed {
278    (codes::WRONG_ARGUMENTS, error)
279}
280
281/// A line of the build's log, as the session answers it.
282fn log_entry(line: shuyi_platform::link::LogEntry) -> LogEntry {
283    LogEntry {
284        frame: line.frame,
285        level: line.level,
286        category: line.category,
287        text: line.text,
288    }
289}
290
291#[cfg(test)]
292mod tests;