Skip to main content

shuyi_assets/lang/
formats.rs

1//! A language pack as a format (ADR-0007): read from its `lang.ron`, with
2//! the other files of its folder and the source's screen text beside it.
3use crate::prelude::*;
4
5use alloc::collections::BTreeSet;
6
7use super::{
8    DATA_FILE, INFO_FILE, LangPack, PackRules, PackSources, SourceLang, SourceText, UI_FILE,
9    english_from_source, is_dialogue_file, pack_code, pack_from_files_in,
10};
11use crate::dialogue::{DialogueLimits, Tokens};
12use crate::error::ContentError;
13use crate::font::{CellWidths, FontAtlasDef};
14use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, join, text};
15use crate::import_settings::is_settings_file;
16
17impl AssetKind for LangPack {
18    const NAME: &'static str = "language pack";
19}
20
21/// What a file a language doesn't have is told.
22const NOT_A_LANGUAGE_FILE: &str = "not a file a language has";
23
24/// A language pack (`lang-pack`, `.ron`), read from its `lang.ron` in its
25/// folder (`lang/pt-BR/lang.ron`), which is named by its tag as BCP 47
26/// writes it: its `ui.ron` beside it, checked against the source's
27/// (`lang/en/ui.ron`, or the folder of the source the game declares,
28/// [`LangPackRon::source_lang`]), and its `data.ron` and
29/// `dialogue/*.ron`, checked against the game's data text when it gives it
30/// ([`LangPackRon::with_data`]; left out otherwise). Any other file in the
31/// folder is an error, and the source's folder is no pack. Checked as
32/// [`pack_from_files`](super::pack_from_files) says, with the game's
33/// rules, its limits counting a character of the font atlas's wide glyphs
34/// as two cells when the game gives the atlas
35/// ([`LangPackRon::widths_of`]), as the pack has them
36/// ([`LangInfo::widths`](super::LangInfo::widths)); with the atlas, the
37/// wide set its `wide_font` names must be one the atlas has.
38pub struct LangPackRon {
39    data_problem: fn(&str, &str) -> Option<String>,
40    tokens: &'static (dyn Tokens + Sync),
41    limits: DialogueLimits,
42    /// The characters that take two cells ([`CellWidths`], which can't be
43    /// shared between threads, is made of them as a pack is read).
44    wide: BTreeSet<char>,
45    /// The atlas's named wide sets, when the game gives the atlas.
46    sets: Option<BTreeSet<String>>,
47    variants: &'static [&'static str],
48    data: Option<SourceText>,
49    source: SourceLang,
50}
51
52impl LangPackRon {
53    /// Packs whose dialogue fits `limits` and may use the game's `tokens`,
54    /// with no rules of the game's for its data text, no variants, no data
55    /// text to check theirs against, and English as the source.
56    pub fn new(limits: DialogueLimits, tokens: &'static (dyn Tokens + Sync)) -> Self {
57        Self {
58            data_problem: |_, _| None,
59            tokens,
60            limits,
61            wide: BTreeSet::new(),
62            sets: None,
63            variants: &[],
64            data: None,
65            source: SourceLang::default(),
66        }
67    }
68
69    /// These packs, of a game written in `source`: checked against its
70    /// screen text (`<languages>/<code>/ui.ron`), its folder no pack.
71    #[must_use]
72    pub fn source_lang(self, source: SourceLang) -> Self {
73        Self { source, ..self }
74    }
75
76    /// These packs, their limits counting a character with a wide glyph in
77    /// `atlas` as two cells ([`CellWidths::of`]), as the screen prints it,
78    /// and their `wide_font` naming one of its wide sets.
79    #[must_use]
80    pub fn widths_of(self, atlas: &FontAtlasDef) -> Self {
81        Self {
82            wide: atlas.wide_chars(),
83            sets: Some(atlas.wide_sets.keys().cloned().collect()),
84            ..self
85        }
86    }
87
88    /// These packs, their data text kept to `data_problem`
89    /// ([`PackRules::data_problem`]).
90    #[must_use]
91    pub fn data_problem(self, data_problem: fn(&str, &str) -> Option<String>) -> Self {
92        Self {
93            data_problem,
94            ..self
95        }
96    }
97
98    /// These packs, whose dialogue entries may have `variants`
99    /// ([`PackRules::variants`]).
100    #[must_use]
101    pub fn variants(self, variants: &'static [&'static str]) -> Self {
102        Self { variants, ..self }
103    }
104
105    /// These packs, their data and dialogue text read and checked against
106    /// the source text of the game's data, `data`.
107    #[must_use]
108    pub fn with_data(self, data: SourceText) -> Self {
109        Self {
110            data: Some(data),
111            ..self
112        }
113    }
114}
115
116impl core::fmt::Debug for LangPackRon {
117    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118        f.debug_struct("LangPackRon")
119            .field("limits", &self.limits)
120            .field("wide", &self.wide)
121            .field("sets", &self.sets)
122            .field("variants", &self.variants)
123            .field("data", &self.data.is_some())
124            .field("source", &self.source.code.as_str())
125            .finish_non_exhaustive()
126    }
127}
128
129impl Format for LangPackRon {
130    type Asset = LangPack;
131
132    fn id(&self) -> &'static str {
133        "lang-pack"
134    }
135
136    fn extensions(&self) -> &'static [&'static str] {
137        &["ron"]
138    }
139
140    fn max_bytes(&self) -> usize {
141        TEXT_MAX_BYTES
142    }
143
144    fn decode(
145        &self,
146        file: &AssetPath,
147        bytes: &[u8],
148        cx: &DecodeCx<'_>,
149    ) -> Result<LangPack, Vec<ContentError>> {
150        let info = text(file, bytes)?;
151        let dir = file.dir();
152        let (languages, code) = dir.rsplit_once('/').unwrap_or(("", dir));
153        let source = &self.source.code;
154        if file.name() != INFO_FILE || code == source.as_str() {
155            let message = format!("{NOT_A_LANGUAGE_FILE}: a pack is read from its {INFO_FILE}");
156            return Err(vec![ContentError::new(file.shown(), message)]);
157        }
158        if let Err(message) = pack_code(code) {
159            return Err(vec![ContentError::new(cx.shown(dir), message)]);
160        }
161        let english_path = join(&join(languages, source.as_str()), UI_FILE);
162        let english_shown = cx.shown(&english_path);
163        let english = english_from_source(
164            &english_shown,
165            cx.text(&english_path)
166                .ok_or_else(|| vec![ContentError::not_found(&english_shown)])?,
167        )?;
168        let mut errors = Vec::new();
169        let mut read = |path: &str| {
170            let source = cx.text(path);
171            if source.is_none() {
172                errors.push(ContentError::not_found(cx.shown(path)));
173            }
174            source.map(|source| (cx.shown(path), source))
175        };
176        let ui = read(&join(dir, UI_FILE));
177        let data_path = join(dir, DATA_FILE);
178        let data = cx.has(&data_path).then(|| read(&data_path)).flatten();
179        let mut dialogue = Vec::new();
180        let mut unknown = Vec::new();
181        for path in cx.files_under(dir) {
182            let name = path
183                .strip_prefix(dir)
184                .and_then(|name| name.strip_prefix('/'))
185                .unwrap_or(path);
186            if is_dialogue_file(name) {
187                dialogue.extend(read(path));
188            } else if !is_settings_file(name) && ![UI_FILE, INFO_FILE, DATA_FILE].contains(&name) {
189                unknown.push(ContentError::new(cx.shown(path), NOT_A_LANGUAGE_FILE));
190            }
191        }
192        errors.extend(unknown);
193        let Some((ui_shown, ui)) = ui else {
194            return Err(errors);
195        };
196        let sources = PackSources {
197            info: (file.shown(), info),
198            ui: (&ui_shown, ui),
199            data: data
200                .as_ref()
201                .map(|(shown, source)| (shown.as_str(), *source)),
202            dialogue: dialogue.iter().map(|(n, s)| (n.as_str(), *s)).collect(),
203        };
204        let widths = CellWidths::wide(self.wide.iter().copied());
205        let rules = PackRules {
206            data_problem: self.data_problem,
207            tokens: self.tokens,
208            limits: self.limits,
209            widths: &widths,
210            variants: self.variants,
211        };
212        let pack = pack_from_files_in(source, &sources, &english, self.data.as_ref(), &rules);
213        if let (Ok(pack), Some(sets)) = (&pack, &self.sets)
214            && let Some(set) = pack.info.wide_font.as_deref()
215            && !sets.contains(set)
216        {
217            let message = format!(
218                "wide_font names {set:?}, a wide set the font atlas doesn't have: it has {}",
219                if sets.is_empty() {
220                    "none".to_owned()
221                } else {
222                    let names: Vec<String> = sets.iter().map(|s| format!("{s:?}")).collect();
223                    names.join(", ")
224                }
225            );
226            errors.push(ContentError::new(file.shown(), message));
227        }
228        match pack {
229            Ok(pack) if errors.is_empty() => Ok(pack),
230            Ok(_) => Err(errors),
231            Err(e) => {
232                errors.extend(e);
233                Err(errors)
234            }
235        }
236    }
237}