Skip to main content

shuyi_cli/
basic.rs

1//! [`BasicTools`]: the [`GameTools`] of a game on the short path
2//! (ADR-0036), from its `BasicGame`: `frame-png`, `lang-status`, the
3//! contract's `check`, `run` and `scene` (a session with no scene, or one
4//! played from a session record), the font atlas `check` checks the text
5//! against, and the controls `check --target` checks. A game that adds a scene, a
6//! record, bots, importers or a bake writes its own `GameTools` and calls
7//! these methods for the rest.
8
9use std::fmt::Debug;
10
11use shuyi_assets::ContentError;
12use shuyi_assets::font::FontAtlasDef;
13use shuyi_assets::keymap::ActionSet;
14use shuyi_core::Made;
15use shuyi_platform::BasicGame;
16use shuyi_platform::harness::Harness;
17use shuyi_platform::session::{SessionRecord, Stamps};
18use shuyi_ui::GameData;
19use shuyi_ui::basic::{BasicData, FONT_PNG};
20use shuyi_ui::input::fit::{Controls, GameControls, ScreenActions};
21
22use crate::contract::session::{Headless, Session};
23use crate::frame_png::{Image, Painter, Script, Shot};
24use crate::game::{GameArg, GameLang, GameOption, GameTools};
25
26/// The tools of a game on the short path.
27#[derive(Debug)]
28pub struct BasicTools<A: ActionSet, T: Debug + 'static = ()> {
29    game: BasicGame<A, T>,
30}
31
32/// `errors`, one per line.
33fn in_lines(errors: &[ContentError]) -> String {
34    let lines: Vec<String> = errors.iter().map(ToString::to_string).collect();
35    lines.join(
36        "
37",
38    )
39}
40
41/// No option of the game's own.
42fn no_options(options: &[GameArg]) -> Result<(), String> {
43    match options.first() {
44        Some(option) => Err(format!("unknown option {}", option.flag)),
45        None => Ok(()),
46    }
47}
48
49impl<A: ActionSet, T: Debug + 'static> BasicTools<A, T> {
50    /// The tools of `game`.
51    pub fn new(game: BasicGame<A, T>) -> Self {
52        Self { game }
53    }
54
55    /// The game in the Harness, at its first screen; or its files'
56    /// problems, one per line, or the module that can't be installed.
57    pub fn harness(&self) -> Result<Harness<BasicData<A, T>>, String> {
58        let game = self.game.game(self.game.ctx()?);
59        Ok(Harness::new(game.map_err(|e| e.to_string())?))
60    }
61
62    /// The frame `h` shows, each console pixel `scale` × `scale` pixels,
63    /// painted with the game's font and pictures.
64    pub fn picture(h: &Harness<BasicData<A, T>>, scale: u32) -> Result<Image, String> {
65        let ctx = h.ctx();
66        let data = &ctx.game;
67        let png = data.bundle.bytes(FONT_PNG).ok_or("no font atlas picture")?;
68        let clear = data.blank_cell(&ctx.palette).bg;
69        let painter = Painter::new(
70            data.font.clone(),
71            png,
72            &data.images,
73            |path| data.bundle.bytes(path),
74            clear,
75        )?;
76        Ok(painter.render(h.game().buffer(), scale))
77    }
78}
79
80impl<A: ActionSet, T: Debug + 'static> GameTools for BasicTools<A, T> {
81    fn frame_options(&self) -> &[GameOption] {
82        &[]
83    }
84
85    fn frame(
86        &self,
87        options: &[GameArg],
88        script: &Script,
89        scale: Option<u32>,
90    ) -> Result<Shot, String> {
91        no_options(options)?;
92        let mut h = self.harness()?;
93        script.play(&mut h);
94        Ok(Shot {
95            image: Self::picture(&h, scale.unwrap_or(1))?,
96            shows: h.top_screen().to_owned(),
97            warning: None,
98        })
99    }
100
101    fn lang(&self) -> Result<GameLang, String> {
102        let data = self.game.data().map_err(|errors| {
103            let lines: Vec<String> = errors.iter().map(ToString::to_string).collect();
104            lines.join("\n")
105        })?;
106        Ok(GameLang {
107            lang: data.lang().clone(),
108            variants: Vec::new(),
109        })
110    }
111
112    fn check(&self) -> Result<Vec<ContentError>, String> {
113        Ok(self.game.data().err().unwrap_or_default())
114    }
115
116    fn session(&self, options: &[GameArg]) -> Result<Box<dyn Session>, String> {
117        no_options(options)?;
118        let session = Headless::new(self.harness()?)
119            .with_picture(|h, scale| Self::picture(h, scale.unwrap_or(1)));
120        Ok(Box::new(session))
121    }
122
123    /// The record played from the game's real start (`BasicGame::start`).
124    fn session_from(
125        &self,
126        record: &SessionRecord,
127        stamps: Stamps,
128    ) -> Result<Box<dyn Session>, String> {
129        let harness =
130            Harness::from_session(self.game.start(), record, stamps).map_err(|e| e.to_string())?;
131        let session =
132            Headless::new(harness).with_picture(|h, scale| Self::picture(h, scale.unwrap_or(1)));
133        Ok(Box::new(session))
134    }
135
136    /// `BasicGame::made`, which its session records carry.
137    fn made(&self) -> Option<Made> {
138        Some(self.game.made())
139    }
140
141    /// The font atlas it loads, which `check` checks its text against.
142    fn font_atlas(&self) -> Option<Result<FontAtlasDef, String>> {
143        let data = self.game.data().map_err(|errors| in_lines(&errors));
144        Some(data.map(|data| data.font))
145    }
146
147    /// Its keymap and every screen it can show (`BasicGame::screens`).
148    fn controls(&self) -> Option<Result<Box<dyn Controls>, String>> {
149        let controls = self.game.ctx().map(|mut ctx| {
150            let keymap = ctx.game.keymap_def().clone();
151            let screens = self.game.every_screen(&mut ctx);
152            let game = screens
153                .iter()
154                .fold(GameControls::new(keymap), |game, screen| {
155                    game.with(ScreenActions::of(screen.as_ref()))
156                });
157            Box::new(game) as Box<dyn Controls>
158        });
159        Some(controls)
160    }
161}
162
163#[cfg(test)]
164mod tests;