1use 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
30pub const NO_SCENE: &str = "no scene over the link";
32
33pub const NO_EVENTS: &str = "no events over the link";
35
36pub struct Attached {
38 link: Connection,
39 address: String,
40 token: String,
41 ended: bool,
43 snapshots: Vec<HeapSnapshot>,
45}
46
47impl Attached {
48 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 pub fn ended(&self) -> bool {
101 self.ended
102 }
103
104 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 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 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 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 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 process_heap: false,
247 })
248 })
249 }
250
251 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
276fn wrong(error: String) -> Failed {
278 (codes::WRONG_ARGUMENTS, error)
279}
280
281fn 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;