Skip to main content

shuyi_assets/lang/
texts.rs

1//! Every text a [`Lang`] holds, each with where it is from
2//! ([`Lang::texts`]): the one scan of the game's text that the font
3//! atlas's tool and its check ([`crate::font::undrawn`]) read.
4use crate::prelude::*;
5
6use super::{Lang, LangCode};
7
8/// Which part of a language a text is from.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[non_exhaustive]
11pub enum TextPart {
12    /// Screen text, by key (English's screen text, a pack's `ui.ron`).
13    Screen,
14    /// Data text, by key: names (`names.<id>`) and the game's other data
15    /// text (a pack's `data.ron`).
16    Data,
17    /// A dialogue line or caption (a `.dlg` file, a pack's
18    /// `dialogue/<file>.ron`).
19    Dialogue,
20    /// A pack's name for its language (`lang.ron`'s `name`), which the
21    /// language menu shows whatever language is on.
22    LangName,
23}
24
25/// Where a text of [`Lang::texts`] is from.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[non_exhaustive]
28pub struct TextAt<'a> {
29    /// The language: `None` for the source, else the pack's tag.
30    pub lang: Option<&'a LangCode>,
31    /// The part of the language.
32    pub part: TextPart,
33    /// Its key: a screen or data text's key, a line id or caption key, and
34    /// `name` for a [`TextPart::LangName`].
35    pub key: &'a str,
36    /// For a dialogue line, the stem of its source line's `.dlg` file
37    /// ([`SourceLine::file`](super::SourceLine::file)); empty otherwise,
38    /// and for a pack's line the source no longer has.
39    pub file: &'a str,
40    /// The source language's tag, which names its folder.
41    source: &'a LangCode,
42}
43
44/// The key a [`TextPart::LangName`] text is listed under.
45const NAME_KEY: &str = "name";
46
47impl TextAt<'_> {
48    /// The file the text is from, as messages name it. A pack's is its file
49    /// under the languages' directory, as [`from_files`](super::from_files)
50    /// reads it: `ru/lang.ron` for its name, `ru/ui.ron`, `ru/data.ron`,
51    /// and for a line `ru/dialogue/` and its English line's `.dlg` stem
52    /// (`ru/dialogue/ch01.ron`; just `ru/dialogue/` for a line the source
53    /// no longer has). The source's is a line's `.dlg` file (`ch01.dlg`),
54    /// and its folder for the rest (`en`, or the tag of the source the game
55    /// declares): a [`Lang`] doesn't keep which file the game read them
56    /// from.
57    pub fn path(&self) -> String {
58        let Some(code) = self.lang else {
59            return if self.part == TextPart::Dialogue && !self.file.is_empty() {
60                format!("{}.dlg", self.file)
61            } else {
62                self.source.to_string()
63            };
64        };
65        match self.part {
66            TextPart::Screen => format!("{code}/{}", super::UI_FILE),
67            TextPart::Data => format!("{code}/{}", super::DATA_FILE),
68            TextPart::LangName => format!("{code}/{}", super::INFO_FILE),
69            TextPart::Dialogue if self.file.is_empty() => format!("{code}/{}", super::DIALOGUE_DIR),
70            TextPart::Dialogue => format!(
71                "{code}/{}{}{}",
72                super::DIALOGUE_DIR,
73                self.file,
74                super::DIALOGUE_EXTENSION
75            ),
76        }
77    }
78}
79
80impl Lang {
81    /// Every text the languages hold, with where each is from. First the
82    /// source: its screen text, its data text (names included, as
83    /// `names.<id>`), then its dialogue lines and captions, each in key
84    /// order. Then each pack in tag order: its name, its screen text, its
85    /// data text and its dialogue lines, every variant of a line with
86    /// variants ([`LineText::all`](super::LineText::all)).
87    pub fn texts(&self) -> Vec<(TextAt<'_>, &str)> {
88        let source = &self.written_in.code;
89        let at = |lang, part, key, file| TextAt {
90            lang,
91            part,
92            key,
93            file,
94            source,
95        };
96        let mut texts = Vec::new();
97        for (key, text) in &self.ui {
98            texts.push((at(None, TextPart::Screen, key, ""), text.as_str()));
99        }
100        for (key, text) in &self.source.data {
101            texts.push((at(None, TextPart::Data, key, ""), text.as_str()));
102        }
103        for (key, line) in &self.source.lines {
104            let file = line.file.as_str();
105            texts.push((at(None, TextPart::Dialogue, key, file), line.text.as_str()));
106        }
107        for (code, pack) in &self.packs {
108            let lang = Some(code);
109            texts.push((
110                at(lang, TextPart::LangName, NAME_KEY, ""),
111                pack.info.name.as_str(),
112            ));
113            for (key, entry) in &pack.ui {
114                texts.push((at(lang, TextPart::Screen, key, ""), entry.text.as_str()));
115            }
116            for (key, entry) in &pack.data {
117                texts.push((at(lang, TextPart::Data, key, ""), entry.text.as_str()));
118            }
119            for (key, entry) in &pack.lines {
120                let file = self.source.lines.get(key).map_or("", |l| l.file.as_str());
121                for text in entry.text.all() {
122                    texts.push((at(lang, TextPart::Dialogue, key, file), text));
123                }
124            }
125        }
126        texts
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use alloc::collections::BTreeMap;
133
134    use super::*;
135    use crate::lang::{Entry, LangInfo, LangPack, LineEntry, LineText, MadeBy};
136    use crate::lang::{LineKind, SourceLang, SourceLine, SourceText};
137    use crate::names::Names;
138
139    fn owned(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
140        pairs
141            .iter()
142            .map(|&(k, v)| (k.to_owned(), v.to_owned()))
143            .collect()
144    }
145
146    fn entry(key: &str, text: &str) -> (String, Entry) {
147        let entry = Entry {
148            key: key.to_owned(),
149            source: String::new(),
150            text: text.to_owned(),
151        };
152        (key.to_owned(), entry)
153    }
154
155    fn line(key: &str, text: LineText) -> (String, LineEntry) {
156        let source = String::new();
157        let key = key.to_owned();
158        (key.clone(), LineEntry { key, source, text })
159    }
160
161    /// English with a screen text, a name and a line of `gate.dlg`, and the
162    /// pack `ru` with one of each, the line with two variants, and a line
163    /// English no longer has.
164    fn lang() -> Lang {
165        let names = Names {
166            names: owned(&[("king", "Osgar")]),
167        };
168        let mut source = SourceText::new(owned(&[("items.rope", "Rope")]), names);
169        let english_line = SourceLine {
170            scene: "gate".to_owned(),
171            text: "Halt.".to_owned(),
172            kind: LineKind::Speech,
173            file: "gate".to_owned(),
174        };
175        source.lines.insert("gate.l1".to_owned(), english_line);
176        let pack = LangPack {
177            info: LangInfo {
178                name: "Русский".to_owned(),
179                made_by: MadeBy::Machine,
180                wide_font: None,
181                wide_punctuation: false,
182            },
183            ui: [entry("menu.title", "Жa")].into(),
184            data: [entry("names.king", "Осгар")].into(),
185            lines: [
186                line(
187                    "gate.l1",
188                    LineText::Variants(owned(&[("a", "Стой."), ("b", "Стой!")])),
189                ),
190                line("gate.old", LineText::One("Был.".to_owned())),
191            ]
192            .into(),
193        };
194        let code = LangCode::new("ru").unwrap();
195        Lang::with_source(
196            owned(&[("menu.title", "Start")]),
197            source,
198            [(code, pack)].into(),
199        )
200    }
201
202    #[test]
203    fn texts_list_english_then_each_pack_with_part_key_and_file() {
204        use TextPart::{Data, Dialogue, LangName, Screen};
205        let lang = lang();
206        let ru = LangCode::new("ru").unwrap();
207        let found: Vec<(Option<&str>, TextPart, &str, &str, &str)> = lang
208            .texts()
209            .into_iter()
210            .map(|(at, text)| {
211                (
212                    at.lang.map(LangCode::as_str),
213                    at.part,
214                    at.key,
215                    at.file,
216                    text,
217                )
218            })
219            .collect();
220        let (en, ru) = (None, Some(ru.as_str()));
221        assert_eq!(
222            found,
223            [
224                (en, Screen, "menu.title", "", "Start"),
225                (en, Data, "items.rope", "", "Rope"),
226                (en, Data, "names.king", "", "Osgar"),
227                (en, Dialogue, "gate.l1", "gate", "Halt."),
228                (ru, LangName, "name", "", "Русский"),
229                (ru, Screen, "menu.title", "", "Жa"),
230                (ru, Data, "names.king", "", "Осгар"),
231                (ru, Dialogue, "gate.l1", "gate", "Стой."),
232                (ru, Dialogue, "gate.l1", "gate", "Стой!"),
233                (ru, Dialogue, "gate.old", "", "Был."),
234            ]
235        );
236        assert_eq!(Lang::default().texts(), []);
237    }
238
239    #[test]
240    fn each_text_names_the_file_it_is_from() {
241        let lang = lang();
242        let paths: Vec<String> = lang.texts().iter().map(|(at, _)| at.path()).collect();
243        assert_eq!(
244            paths,
245            [
246                "en",
247                "en",
248                "en",
249                "gate.dlg",
250                "ru/lang.ron",
251                "ru/ui.ron",
252                "ru/data.ron",
253                "ru/dialogue/gate.ron",
254                "ru/dialogue/gate.ron",
255                "ru/dialogue/",
256            ]
257        );
258        // A source line not read from a file is the source's too.
259        let en = LangCode::default();
260        let unfiled = TextAt {
261            lang: None,
262            part: TextPart::Dialogue,
263            key: "a.l1",
264            file: "",
265            source: &en,
266        };
267        assert_eq!(unfiled.path(), "en");
268    }
269
270    #[test]
271    fn a_declared_sources_texts_name_its_folder() {
272        let ja = SourceLang::new(LangCode::new("ja").unwrap(), "日本語");
273        let lang = lang().with_source_lang(ja);
274        let paths: Vec<String> = lang.texts().iter().map(|(at, _)| at.path()).collect();
275        assert_eq!(paths[..4], ["ja", "ja", "ja", "gate.dlg"]);
276    }
277}