Skip to main content

shuyi_assets/lang/
source.rs

1//! The source text a pack's `data.ron` and `dialogue/` files translate:
2//! the game's data text by key, every dialogue line and caption by key, and
3//! the names table ([`SourceText`]); and the lookups that give each in a
4//! language. The source text stays in the game's files; a lookup is handed
5//! today's and returns a pack's text only when its entry was made from
6//! exactly that.
7use crate::prelude::*;
8
9use alloc::collections::BTreeMap;
10
11use super::{Lang, LangCode};
12use crate::dialogue::{DialogueTable, REPLY_SPEAKER, Step, caption_key};
13use crate::names::Names;
14
15/// What every key of a name of the names table starts with.
16pub(super) const NAMES_PREFIX: &str = "names.";
17
18/// The key of the name `id` of the names table: `names.<id>`.
19pub fn names_key(id: &str) -> String {
20    format!("{NAMES_PREFIX}{id}")
21}
22
23/// What kind of dialogue text a key is for: its length limit depends on it.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum LineKind {
26    /// A speech or narration line (one text box).
27    Speech,
28    /// One of the replies the player picks from (a menu row).
29    Reply,
30    /// A `@caption`.
31    Caption,
32}
33
34/// One dialogue line or caption in English.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct SourceLine {
37    /// The id of its scene.
38    pub scene: String,
39    /// Its text as written (tokens unexpanded).
40    pub text: String,
41    /// What it is.
42    pub kind: LineKind,
43    /// The stem of the `.dlg` file its scene is in (`ch01` for
44    /// `ch01.dlg`); empty if the dialogue wasn't read from files.
45    pub file: String,
46}
47
48/// Today's English for everything `data.ron` and `dialogue/*.ron` can
49/// translate, by key.
50#[derive(Debug, Clone, Default, PartialEq, Eq)]
51pub struct SourceText {
52    /// Data text by key: `names.<id>` for the names table, and whatever
53    /// else the game hands over (`items.<id>.name`, say).
54    pub data: BTreeMap<String, String>,
55    /// Dialogue lines by line id, and captions by [`caption_key`].
56    pub lines: BTreeMap<String, SourceLine>,
57    /// The names table: what a name token in a pack's text may refer to.
58    pub names: Names,
59}
60
61/// `ch01` for `story/ch01.dlg`.
62fn file_stem(path: &str) -> &str {
63    let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
64    name.strip_suffix(".dlg").unwrap_or(name)
65}
66
67/// Adds the captions of `steps` (of scene `scene`) to `lines`.
68fn add_captions<D>(
69    scene: &str,
70    file: &str,
71    steps: &[Step<D>],
72    lines: &mut BTreeMap<String, SourceLine>,
73) {
74    for step in steps {
75        match step {
76            Step::Caption { text } => {
77                let line = SourceLine {
78                    scene: scene.to_owned(),
79                    text: text.clone(),
80                    kind: LineKind::Caption,
81                    file: file.to_owned(),
82                };
83                lines.insert(caption_key(scene, text), line);
84            }
85            Step::Choice { options } => {
86                for option in options {
87                    add_captions(scene, file, &option.steps, lines);
88                }
89            }
90            Step::If {
91                then, otherwise, ..
92            } => {
93                add_captions(scene, file, then, lines);
94                add_captions(scene, file, otherwise, lines);
95            }
96            Step::Place { .. }
97            | Step::Clear { .. }
98            | Step::Say { .. }
99            | Step::Narrate { .. }
100            | Step::Music(_)
101            | Step::Directive(_) => {}
102        }
103    }
104}
105
106impl SourceText {
107    /// The data text `data` and the names table `names` (whose names are
108    /// added to `data` by [`names_key`]), with no dialogue yet.
109    pub fn new(mut data: BTreeMap<String, String>, names: Names) -> Self {
110        for (id, value) in &names.names {
111            data.insert(names_key(id), value.clone());
112        }
113        Self {
114            data,
115            lines: BTreeMap::new(),
116            names,
117        }
118    }
119
120    /// Adds every line, reply and caption of `dialogue`.
121    pub fn add_dialogue<D>(&mut self, dialogue: &DialogueTable<D>) {
122        for scene in dialogue.scenes.values() {
123            let file = dialogue
124                .files
125                .get(&scene.id)
126                .map_or("", |path| file_stem(path));
127            for line in scene.lines() {
128                let kind = if line.speaker == REPLY_SPEAKER {
129                    LineKind::Reply
130                } else {
131                    LineKind::Speech
132                };
133                let source = SourceLine {
134                    scene: scene.id.clone(),
135                    text: line.text.to_owned(),
136                    kind,
137                    file: file.to_owned(),
138                };
139                self.lines.insert(line.id.as_str().to_owned(), source);
140            }
141            add_captions(&scene.id, file, &scene.steps, &mut self.lines);
142        }
143    }
144}
145
146impl Lang {
147    /// `english` (today's source text) in language `code`: the first
148    /// `data.ron` entry along [`chain`](Self::chain) for the key `key`
149    /// gives that was made from exactly `english`; otherwise `english`. The
150    /// key is only built when the chain has a pack.
151    pub fn data_text<'a>(
152        &'a self,
153        code: &LangCode,
154        key: impl FnOnce() -> String,
155        english: &'a str,
156    ) -> &'a str {
157        let mut packs = self.packs_along(code).peekable();
158        if packs.peek().is_none() {
159            return english;
160        }
161        let key = key();
162        packs
163            .find_map(|(_, pack)| pack.data.get(&key).filter(|entry| entry.source == english))
164            .map_or(english, |entry| &entry.text)
165    }
166
167    /// The display name `id` of the names table in language `code`;
168    /// `english` is its value in the table.
169    pub fn name<'a>(&'a self, code: &LangCode, id: &str, english: &'a str) -> &'a str {
170        self.data_text(code, || names_key(id), english)
171    }
172
173    /// The names table in language `code`: each name translated along
174    /// [`chain`](Self::chain), the others as the source has them. What a
175    /// name token in a line shown in `code` is filled from.
176    pub fn names(&self, code: &LangCode, names: &Names) -> Names {
177        if self.packs_along(code).next().is_none() {
178            return names.clone();
179        }
180        let translated = |(id, english): (&String, &String)| {
181            (id.clone(), self.name(code, id, english).to_owned())
182        };
183        Names {
184            names: names.names.iter().map(translated).collect(),
185        }
186    }
187
188    /// `english` in language `code`, for a text whose key isn't known
189    /// where it is shown: the first pack along [`chain`](Self::chain) with
190    /// an entry for a key starting with `prefix` whose source text is
191    /// `english` today and was when the entry was made; otherwise
192    /// `english`.
193    pub fn found<'a>(&'a self, code: &LangCode, prefix: &str, english: &'a str) -> &'a str {
194        let mut packs = self.packs_along(code).peekable();
195        if packs.peek().is_none() {
196            return english;
197        }
198        let end = format!("{prefix}\u{10ffff}");
199        let keys = self
200            .source
201            .data
202            .range(prefix.to_owned()..end)
203            .filter(|&(_, today)| today == english);
204        packs
205            .find_map(|(_, pack)| {
206                keys.clone()
207                    .find_map(|(key, today)| pack.data.get(key).filter(|e| e.source == *today))
208            })
209            .map_or(english, |entry| entry.text.as_str())
210    }
211}