1use std::io::{BufRead, Write};
11use std::path::Path;
12
13use serde::de::DeserializeOwned;
14use serde::{Deserialize, Serialize};
15use serde_json::{Value, json};
16use shuyi_ui::perf::HeapSnapshot;
17
18use super::server::{Launch, Link};
19use super::{
20 EventsOutput, FramePngInput, FramePngOutput, KEPT_SNAPSHOTS, NoInput, PerfInput, PerfOutput,
21 SceneInput, StartInput, StartOption, StartOutput, StepInput, StopOutput, base64,
22};
23use crate::codec::encode_png;
24use crate::contract::attach::Failed;
25use crate::contract::codes::{self, Code};
26use crate::contract::import::Importers;
27use crate::contract::perf::{self, PerfRead};
28use crate::contract::run::{self, RunInput};
29use crate::contract::session::{Session, SessionInput, SessionPlayed, from_record, measured_start};
30use crate::contract::{
31 Answer, Op, add, attach, autobalance, bake, check, doctor, explain, export, import, lint,
32 modules, new, playtest, replay,
33};
34use crate::frame_png::MAX_SCALE;
35use crate::game::{GameArg, GameTools, check_start, code_of};
36use crate::project::{FILE, Project};
37
38pub const REPLY: &str = "shuyi-mcp-reply ";
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct Request {
46 pub op: Op,
48 #[serde(default)]
50 pub input: Value,
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct Reply {
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub answer: Option<Value>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub png: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub error: Option<String>,
66}
67
68impl Reply {
69 fn of(answer: Value) -> Self {
71 Self {
72 answer: Some(answer),
73 ..Self::default()
74 }
75 }
76}
77
78pub fn to_json<T: Serialize>(answer: &Answer<T>) -> Value {
80 serde_json::to_value(answer).unwrap_or(Value::Null)
82}
83
84pub fn failed(op: Op, code: Code, error: impl Into<String>) -> Value {
87 to_json(&Answer::<()>::failed(op, code, error))
88}
89
90pub fn answered<I: DeserializeOwned, T: Serialize>(
93 op: Op,
94 input: Value,
95 f: impl FnOnce(I) -> Answer<T>,
96) -> Value {
97 match read(input) {
98 Ok(input) => to_json(&f(input)),
99 Err(e) => failed(op, codes::WRONG_ARGUMENTS, e),
100 }
101}
102
103pub(super) fn read<I: DeserializeOwned>(input: Value) -> Result<I, String> {
105 let input = if input.is_null() { json!({}) } else { input };
106 serde_json::from_value(input).map_err(|e| format!("input: {e}"))
107}
108
109pub fn one_off(
115 op: Op,
116 input: Value,
117 cwd: &Path,
118 project: &Result<Project, (Code, String)>,
119 game: Option<&dyn GameTools>,
120) -> Value {
121 let no_game = || {
122 let error = format!(
123 "{FILE} names no game tool program (`game`), and {} runs the game",
124 op.name()
125 );
126 failed(op, codes::PROJECT_FILE, error)
127 };
128 let needs = |f: &dyn Fn(&dyn GameTools, &Project) -> Value| match (game, project) {
129 (_, Err((code, error))) => failed(op, *code, error.clone()),
130 (None, Ok(_)) => no_game(),
131 (Some(game), Ok(project)) => f(game, project),
132 };
133 match op {
134 Op::New => answered(op, input, |i| new::new(cwd, &i)),
135 Op::Doctor => answered(op, input, |i| doctor::doctor(&doctor::ThisMachine, &i)),
136 Op::Explain => answered(op, input, |i| explain::explain(&i)),
137 Op::Attach => answered(op, input, |i| attach::attach(&i, cwd)),
138 Op::Check => {
139 let input: check::CheckInput = match read(input) {
140 Ok(input) => input,
141 Err(e) => return failed(op, codes::WRONG_ARGUMENTS, e),
142 };
143 match (&input.target, project) {
144 (Some(_), Ok(p)) => to_json(&check::targets(&p.config, &p.root, &input, game)),
145 (None, _) => needs(&|game, _| to_json(&check::check(game, &input))),
146 (Some(_), Err((code, error))) => failed(op, *code, error.clone()),
147 }
148 }
149 Op::Import => {
150 let (importers, limit) = match (game, project) {
151 (Some(game), _) => (game.importers(), None),
152 (None, Ok(_)) => (
153 Importers::standard(),
154 Some(format!(
155 "{FILE} names no game tool program: only the engine's standard kinds \
156 were tried"
157 )),
158 ),
159 (None, Err(_)) => (
160 Importers::standard(),
161 Some("no project here: only the engine's standard kinds were tried".to_owned()),
162 ),
163 };
164 let root = project.as_ref().map_or(cwd, |p| p.root.as_path());
165 answered(op, input, |i| {
166 let answer = import::import(&importers, root, &i);
167 match &limit {
168 Some(limit) => answer.limited(limit),
169 None => answer,
170 }
171 })
172 }
173 Op::Export => match project {
174 Ok(p) => answered(op, input, |i| export::export(p, &i, game)),
175 Err((code, error)) => failed(op, *code, error.clone()),
176 },
177 Op::Lint => match project {
178 Ok(p) => answered(op, input, |i| lint::lint(&lint::ThisMachine, p, &i)),
179 Err((code, error)) => failed(op, *code, error.clone()),
180 },
181 Op::Modules => match project {
182 Ok(p) => answered(op, input, |i| modules::modules(p, &i)),
183 Err((code, error)) => failed(op, *code, error.clone()),
184 },
185 Op::Add => match project {
186 Ok(p) => answered(op, input, |i| add::add(p, &i)),
187 Err((code, error)) => failed(op, *code, error.clone()),
188 },
189 Op::Run => {
190 needs(&|game, p| answered(op, input.clone(), |i| run::run_at(game, &p.root, &i)))
191 }
192 Op::Scene => {
193 needs(&|game, p| answered(op, input.clone(), |i| run::scene_at(game, &p.root, &i)))
194 }
195 Op::Replay => {
196 needs(&|game, p| answered(op, input.clone(), |i| replay::replay(game, &p.root, &i)))
197 }
198 Op::Playtest => {
199 needs(&|game, p| answered(op, input.clone(), |i| playtest::kept(game, &p.root, &i)))
200 }
201 Op::Autobalance => {
202 needs(&|game, _| answered(op, input.clone(), |i| autobalance::autobalance(game, &i)))
203 }
204 Op::Bake => needs(&|game, p| answered(op, input.clone(), |i| bake::bake(game, p, &i))),
205 Op::Start | Op::Step | Op::FramePng | Op::Events | Op::Perf | Op::Reset | Op::Stop => {
206 failed(
207 op,
208 codes::WRONG_ARGUMENTS,
209 format!("{} is a session tool, not an operation", op.name()),
210 )
211 }
212 }
213}
214
215struct Live {
218 start: Vec<GameArg>,
219 session: Option<SessionInput>,
220 game: Box<dyn Session>,
221 snapshots: Vec<HeapSnapshot>,
222}
223
224pub struct Host<'a> {
227 tools: &'a dyn GameTools,
228 project: Project,
229 live: Option<Live>,
230}
231
232impl<'a> Host<'a> {
233 pub fn new(tools: &'a dyn GameTools, project: Project) -> Self {
235 Self {
236 tools,
237 project,
238 live: None,
239 }
240 }
241
242 pub fn call(&mut self, request: &Request) -> Reply {
244 let (op, input) = (request.op, request.input.clone());
245 match op {
246 Op::Start => Reply::of(answered(op, input, |i: StartInput| {
247 self.start(op, i.start, i.session)
248 })),
249 Op::Reset => match (read::<NoInput>(input), &self.live) {
250 (Err(e), _) => Reply::of(failed(op, codes::WRONG_ARGUMENTS, e)),
251 (Ok(_), Some(live)) => {
252 let (start, session) = (live.start.clone(), live.session.clone());
253 Reply::of(to_json(&self.start(op, start, session)))
254 }
255 (Ok(_), None) => Reply::of(no_session(op)),
256 },
257 Op::Step => self.with_game(op, |game| {
258 answered(op, input, |i: StepInput| step(game, &i))
259 }),
260 Op::Scene => self.with_game(op, |game| {
261 answered(op, input, |i: SceneInput| {
262 run::scene_answer(op, game, i.ask(), None, false)
263 })
264 }),
265 Op::Events => self.with_game(op, |game| {
266 answered(op, input, |NoInput {}| {
267 let output = EventsOutput {
268 frames: game.frame_count(),
269 events: game.events(),
270 sounds: game.sounds(),
271 sounds_added: game.sounds_added(),
272 };
273 Answer::new(op, true, output, Vec::new())
274 })
275 }),
276 Op::Perf => match self.live.as_mut() {
277 Some(live) => Reply::of(answered(op, input, |i: PerfInput| perf_read(live, &i))),
278 None => Reply::of(no_session(op)),
279 },
280 Op::FramePng => match self.live.as_ref() {
281 Some(live) => frame_png(&*live.game, input),
282 None => Reply::of(no_session(op)),
283 },
284 Op::Stop => match (read::<NoInput>(input), self.live.take()) {
285 (Err(e), live) => {
286 self.live = live;
287 Reply::of(failed(op, codes::WRONG_ARGUMENTS, e))
288 }
289 (Ok(_), Some(live)) => {
290 let output = StopOutput {
291 frames: live.game.frame_count(),
292 };
293 Reply::of(to_json(&Answer::new(op, true, output, Vec::new())))
294 }
295 (Ok(_), None) => Reply::of(no_session(op)),
296 },
297 _ => Reply::of(one_off(
298 op,
299 input,
300 &self.project.root,
301 &Ok(self.project.clone()),
302 Some(self.tools),
303 )),
304 }
305 }
306
307 fn with_game(&mut self, op: Op, f: impl FnOnce(&mut dyn Session) -> Value) -> Reply {
310 match self.live.as_mut() {
311 Some(live) => Reply::of(f(&mut *live.game)),
312 None => Reply::of(no_session(op)),
313 }
314 }
315
316 fn start(
320 &mut self,
321 op: Op,
322 start: Vec<GameArg>,
323 session: Option<SessionInput>,
324 ) -> Answer<StartOutput> {
325 if let Err(e) = check_start(self.tools.frame_options(), &start) {
326 return Answer::failed(op, codes::WRONG_ARGUMENTS, e);
327 }
328 if session.is_some() && !start.is_empty() {
329 let error =
330 "start: a session record starts as the game really starts, with no start options";
331 return Answer::failed(op, codes::WRONG_ARGUMENTS, error);
332 }
333 let root = self.project.root.clone();
334 let started = measured_start(|| match &session {
335 Some(input) => {
336 from_record(self.tools, &root, input).map(|(game, played)| (game, Some(played)))
337 }
338 None => self
339 .tools
340 .session(&start)
341 .map(|game| (game, None))
342 .map_err(|e| (code_of(&e), e)),
343 });
344 let (mut game, played) = match started {
345 Ok(started) => started,
346 Err((code, e)) => return Answer::failed(op, code, e),
347 };
348 game.set_perf(true);
350 let screens = game.screens();
351 let output = StartOutput {
352 project: self.project.root.display().to_string(),
353 game: String::new(),
354 start: start.clone(),
355 options: self
356 .tools
357 .frame_options()
358 .iter()
359 .map(StartOption::from)
360 .collect(),
361 frames: game.frame_count(),
362 top_screen: screens.last().cloned().unwrap_or_default(),
363 screens,
364 session: session
365 .as_ref()
366 .zip(played.as_ref())
367 .map(|(input, played)| SessionPlayed::of(&input.file, played)),
368 };
369 self.live = Some(Live {
370 start,
371 session,
372 game,
373 snapshots: Vec::new(),
374 });
375 let (differs, limits) = played.map_or((false, Vec::new()), |played| {
376 (played.first_difference.is_some(), played.limits)
377 });
378 Answer::new(op, !differs, output, limits)
379 }
380}
381
382pub fn no_session(op: Op) -> Value {
384 failed(
385 op,
386 codes::NO_SESSION,
387 "no game is running: call `start` first",
388 )
389}
390
391fn step(game: &mut dyn Session, input: &StepInput) -> Answer<run::RunOutput> {
393 let checked = RunInput {
394 script: input.script.clone(),
395 frames: input.frames,
396 ..RunInput::default()
397 }
398 .check();
399 if let Err(e) = checked.and_then(|()| run::check_actions(game, &input.script)) {
400 return Answer::failed(Op::Step, codes::WRONG_ARGUMENTS, e);
401 }
402 game.play(&input.script);
403 game.idle(input.frames);
404 let (output, limits) = run::run_output(game);
405 Answer::new(Op::Step, true, output, limits)
406}
407
408fn frames(snapshots: &[HeapSnapshot]) -> Vec<u32> {
410 snapshots.iter().map(|s| s.frame).collect()
411}
412
413fn keep(kept: &mut Vec<HeapSnapshot>, snapshot: HeapSnapshot) {
416 kept.retain(|s| s.frame != snapshot.frame);
417 let at = kept.partition_point(|s| s.frame < snapshot.frame);
418 kept.insert(at, snapshot);
419 let extra = kept.len().saturating_sub(KEPT_SNAPSHOTS);
420 kept.drain(..extra);
421}
422
423fn kept_of(kept: &[HeapSnapshot], frame: u32) -> Result<HeapSnapshot, String> {
425 if let Some(snapshot) = kept.iter().find(|s| s.frame == frame) {
426 return Ok(snapshot.clone());
427 }
428 let frames: Vec<String> = frames(kept).iter().map(u32::to_string).collect();
429 let frames = if frames.is_empty() {
430 "none: take one with `snapshot`".to_owned()
431 } else {
432 frames.join(", ")
433 };
434 Err(format!(
435 "diff_from: no snapshot of frame {frame} is kept (kept: {frames})"
436 ))
437}
438
439fn perf_read(live: &mut Live, input: &PerfInput) -> Answer<PerfOutput> {
442 let game = &mut live.game;
443 let taken = perf_answer(input, &mut live.snapshots, |snapshot| {
444 let snapshot = if snapshot { game.heap_snapshot() } else { None };
445 Ok(PerfTaken {
446 read: game.perf(input.since, input.max),
447 snapshot,
448 frames: game.frame_count(),
449 process_heap: game.counts_process_heap(),
450 })
451 });
452 taken.unwrap_or_else(|(code, error)| Answer::failed(Op::Perf, code, error))
453}
454
455pub(crate) struct PerfTaken {
459 pub(crate) read: PerfRead,
460 pub(crate) snapshot: Option<HeapSnapshot>,
461 pub(crate) frames: u32,
462 pub(crate) process_heap: bool,
463}
464
465pub(crate) fn perf_answer(
470 input: &PerfInput,
471 kept: &mut Vec<HeapSnapshot>,
472 take: impl FnOnce(bool) -> Result<PerfTaken, Failed>,
473) -> Result<Answer<PerfOutput>, Failed> {
474 input.check().map_err(|e| (codes::WRONG_ARGUMENTS, e))?;
475 let from = input
476 .diff_from
477 .map(|frame| kept_of(kept, frame))
478 .transpose()
479 .map_err(|e| (codes::WRONG_ARGUMENTS, e))?;
480 let PerfTaken {
481 read,
482 snapshot,
483 frames: ran,
484 process_heap,
485 } = take(input.snapshot)?;
486 if let Some(snapshot) = &snapshot {
487 keep(kept, snapshot.clone());
488 }
489 let to = if input.snapshot {
490 snapshot.as_ref()
491 } else {
492 kept.last()
493 };
494 let diff = from.zip(to).map(|(from, to)| from.diff(to));
495 let mut limits = vec![perf::TIMES.to_owned()];
496 if !read.measured {
497 limits.push(perf::unmeasured_limit(ran));
498 }
499 if input.since > read.next {
500 limits.push(perf::RESET.to_owned());
501 }
502 if read.dropped > 0 {
503 limits.push(perf::dropped_limit(read.dropped));
504 }
505 limits.extend(perf::heap_limits(
506 &read.records,
507 process_heap,
508 perf::other_threads(),
509 ));
510 if input.snapshot && snapshot.is_none() {
511 limits.push(perf::HEAP_UNTAGGED.to_owned());
512 }
513 let output = PerfOutput {
514 summary: perf::summary(&read.records),
515 records: read.records,
516 next: read.next,
517 dropped: read.dropped,
518 snapshot,
519 diff,
520 kept: frames(kept),
521 };
522 Ok(Answer::new(Op::Perf, true, output, limits))
523}
524
525fn frame_png(game: &dyn Session, input: Value) -> Reply {
527 let op = Op::FramePng;
528 let input: FramePngInput = match read(input) {
529 Ok(input) => input,
530 Err(e) => return Reply::of(failed(op, codes::WRONG_ARGUMENTS, e)),
531 };
532 if let Some(scale) = input.scale.filter(|s| !(1..=MAX_SCALE).contains(s)) {
533 let error = format!("scale: 1 to {MAX_SCALE}, not {scale}");
534 return Reply::of(failed(op, codes::WRONG_ARGUMENTS, error));
535 }
536 let image = match game.picture(input.scale) {
537 Ok(image) => image,
538 Err(e) => return Reply::of(failed(op, code_of(&e), e)),
539 };
540 let png = match encode_png(image.width, image.height, &image.rgba) {
541 Ok(png) => png,
542 Err(e) => return Reply::of(failed(op, codes::GAME_ERROR, format!("the picture: {e}"))),
543 };
544 let screens = game.screens();
545 let output = FramePngOutput {
546 frames: game.frame_count(),
547 top_screen: screens.last().cloned().unwrap_or_default(),
548 width: image.width,
549 height: image.height,
550 };
551 Reply {
552 answer: Some(to_json(&Answer::new(op, true, output, Vec::new()))),
553 png: Some(base64(&png)),
554 error: None,
555 }
556}
557
558pub fn serve(
562 game: &dyn GameTools,
563 project: Project,
564 input: impl BufRead,
565 mut output: impl Write,
566) -> u8 {
567 let mut host = Host::new(game, project);
568 for line in input.lines() {
569 let Ok(line) = line else { return 1 };
570 if line.trim().is_empty() {
571 continue;
572 }
573 let reply = match serde_json::from_str::<Request>(&line) {
574 Ok(request) => host.call(&request),
575 Err(e) => Reply {
576 error: Some(format!(
577 "the game's tool program doesn't read the request ({e}): is its engine older \
578 than the server's?"
579 )),
580 ..Reply::default()
581 },
582 };
583 let line = serde_json::to_string(&reply).unwrap_or_default();
585 if writeln!(output, "{REPLY}{line}")
586 .and_then(|()| output.flush())
587 .is_err()
588 {
589 return 1;
590 }
591 }
592 0
593}
594
595pub struct InProcess<'a> {
598 tools: &'a dyn GameTools,
599}
600
601impl<'a> InProcess<'a> {
602 pub fn new(tools: &'a dyn GameTools) -> Self {
604 Self { tools }
605 }
606}
607
608impl<'a> Launch<'a> for InProcess<'a> {
609 fn launch(&self, project: &Project) -> Result<Box<dyn Link + 'a>, (Code, String)> {
610 Ok(Box::new(Host::new(self.tools, project.clone())))
611 }
612}
613
614impl Link for Host<'_> {
615 fn call(&mut self, request: &Request) -> Result<Reply, String> {
616 Ok(Host::call(self, request))
617 }
618}
619
620#[cfg(test)]
621mod tests;