1use crate::prelude::*;
6
7use alloc::collections::BTreeMap;
8use core::fmt;
9
10use serde::de::DeserializeSeed;
11use serde::de::{self, Deserializer, MapAccess, Visitor};
12use serde::{Deserialize, Serialize};
13
14use super::source::{LineKind, SourceText};
15use super::{Lang, LangCode, LangPack, PackRules};
16use crate::dialogue::{
17 ChoiceOption, DialogueLimits, LineId, Scene, Step, caption_key, longest_width, scene_of,
18 token_problems,
19};
20use crate::error::{ContentError, codes};
21use crate::names::Names;
22
23const CAPTION_PREFIX: &str = "caption.";
25
26struct RawLine {
29 key: String,
30 source: String,
31 text: Option<String>,
32 variants: BTreeMap<String, String>,
34}
35
36struct RawLineSeed<'a> {
38 variants: &'a [&'a str],
39}
40
41impl<'de> DeserializeSeed<'de> for RawLineSeed<'_> {
42 type Value = RawLine;
43
44 fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<RawLine, D::Error> {
45 deserializer.deserialize_struct("RawLine", &["key", "source", "text"], self)
46 }
47}
48
49impl<'de> Visitor<'de> for RawLineSeed<'_> {
50 type Value = RawLine;
51
52 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.write_str("a dialogue entry: key, source and text")
54 }
55
56 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<RawLine, A::Error> {
57 let mut fields: BTreeMap<String, String> = BTreeMap::new();
58 while let Some(name) = map.next_key::<String>()? {
59 let known = matches!(name.as_str(), "key" | "source" | "text")
60 || name
61 .strip_prefix("text_")
62 .is_some_and(|v| self.variants.contains(&v));
63 if !known {
64 let mut expected = vec!["`key`".to_owned(), "`source`".to_owned()];
65 expected.push("`text`".to_owned());
66 expected.extend(self.variants.iter().map(|v| format!("`text_{v}`")));
67 return Err(de::Error::custom(format!(
68 "unknown field `{name}`, expected one of {}",
69 expected.join(", ")
70 )));
71 }
72 if fields.contains_key(&name) {
73 return Err(de::Error::custom(format!("duplicate field `{name}`")));
74 }
75 let value = map.next_value::<String>()?;
76 fields.insert(name, value);
77 }
78 let mut take = |name: &'static str| fields.remove(name);
79 let key = take("key").ok_or_else(|| de::Error::missing_field("key"))?;
80 let source = take("source").ok_or_else(|| de::Error::missing_field("source"))?;
81 let text = take("text");
82 let variants = fields
83 .into_iter()
84 .map(|(name, text)| (name["text_".len()..].to_owned(), text))
85 .collect();
86 Ok(RawLine {
87 key,
88 source,
89 text,
90 variants,
91 })
92 }
93}
94
95struct RawLines<'a> {
97 variants: &'a [&'a str],
98}
99
100impl<'de> DeserializeSeed<'de> for RawLines<'_> {
101 type Value = Vec<RawLine>;
102
103 fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Vec<RawLine>, D::Error> {
104 deserializer.deserialize_seq(self)
105 }
106}
107
108impl<'de> Visitor<'de> for RawLines<'_> {
109 type Value = Vec<RawLine>;
110
111 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str("a list of dialogue entries")
113 }
114
115 fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<RawLine>, A::Error> {
116 let mut lines = Vec::new();
117 let seed = || RawLineSeed {
118 variants: self.variants,
119 };
120 while let Some(line) = seq.next_element_seed(seed())? {
121 lines.push(line);
122 }
123 Ok(lines)
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub enum LineText {
130 One(String),
132 Variants(BTreeMap<String, String>),
136}
137
138impl LineText {
139 pub fn get(&self, variant: &str) -> Option<&str> {
142 match self {
143 LineText::One(text) => Some(text),
144 LineText::Variants(texts) => texts.get(variant).map(String::as_str),
145 }
146 }
147
148 pub fn all(&self) -> Vec<&str> {
150 match self {
151 LineText::One(text) => vec![text],
152 LineText::Variants(texts) => texts.values().map(String::as_str).collect(),
153 }
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct LineEntry {
160 pub key: String,
162 pub source: String,
164 pub text: LineText,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct Orphan {
171 pub key: String,
173 pub source: String,
175 pub text: LineText,
177 pub nearest: Option<String>,
181}
182
183fn scene_of_key(key: &str) -> Option<&str> {
186 scene_of(key.strip_prefix(CAPTION_PREFIX).unwrap_or(key))
187}
188
189fn is_caption(key: &str) -> bool {
191 key.starts_with(CAPTION_PREFIX)
192}
193
194fn limit(kind: LineKind, limits: &DialogueLimits) -> Option<usize> {
197 match kind {
198 LineKind::Speech => Some(limits.max_text),
199 LineKind::Reply => Some(limits.max_option),
200 LineKind::Caption => None,
201 }
202}
203
204fn variants_message(variants: &[&str]) -> String {
207 let fields: Vec<String> = variants.iter().map(|v| format!("`text_{v}`")).collect();
208 let all = match fields.as_slice() {
209 [] => return "an entry has `text`".to_owned(),
210 [one] => one.clone(),
211 [a, b] => format!("both {a} and {b}"),
212 [rest @ .., last] => format!("all of {}, and {last}", rest.join(", ")),
213 };
214 format!("an entry has `text`, or {all}")
215}
216
217pub(super) fn add_lines(
228 (file, source): (&str, &str),
229 data: &SourceText,
230 names: &Names,
231 rules: &PackRules<'_>,
232 lines: &mut BTreeMap<String, LineEntry>,
233 errors: &mut Vec<ContentError>,
234) {
235 let seed = RawLines {
236 variants: rules.variants,
237 };
238 let raw = crate::ron::parse_ron_seed(file, source, seed)
239 .map_err(|e| errors.push(e))
240 .unwrap_or_default();
241 let longest_name = names.longest(rules.widths);
242 for entry in raw {
243 let at = |message: &str| ContentError::new(file, format!("\"{}\": {message}", entry.key));
244 if scene_of_key(&entry.key).is_none() {
245 errors.push(at("not a line id or a caption key"));
246 }
247 if lines.contains_key(&entry.key) {
248 errors.push(at("the key is used twice").with_code(codes::DUPLICATE));
249 }
250 let every = entry.variants.len() == rules.variants.len();
251 let text = match (entry.text, entry.variants.is_empty()) {
252 (Some(text), true) => LineText::One(text),
253 (None, false) if every => LineText::Variants(entry.variants),
254 _ => {
255 errors.push(at(&variants_message(rules.variants)));
256 continue;
257 }
258 };
259 let english = data.lines.get(&entry.key);
260 for text in text.all() {
261 if text.trim().is_empty() {
262 errors.push(at("the text must not be empty"));
263 }
264 for problem in token_problems(text, rules.tokens, Some(&data.names)) {
265 errors.push(at(&problem));
266 }
267 let width = longest_width(text, rules.tokens, Some(longest_name), rules.widths);
268 match english.and_then(|line| limit(line.kind, &rules.limits)) {
269 Some(limit) if width > limit => {
270 errors.push(at(&format!("text is {width} cells; the limit is {limit}")));
271 }
272 _ => {}
273 }
274 }
275 let entry = LineEntry {
276 key: entry.key,
277 source: entry.source,
278 text,
279 };
280 lines.insert(entry.key.clone(), entry);
281 }
282}
283
284fn distance(a: &str, b: &str) -> usize {
286 let b: Vec<char> = b.chars().collect();
287 let mut row: Vec<usize> = (0..=b.len()).collect();
288 for (i, ca) in a.chars().enumerate() {
289 let mut diagonal = row[0];
290 row[0] = i + 1;
291 for (j, &cb) in b.iter().enumerate() {
292 let substituted = diagonal + usize::from(ca != cb);
293 diagonal = row[j + 1];
294 row[j + 1] = substituted.min(diagonal + 1).min(row[j] + 1);
295 }
296 }
297 row[b.len()]
298}
299
300impl LangPack {
301 pub fn line(&self, key: &str) -> Option<&LineEntry> {
304 self.lines.get(key)
305 }
306
307 pub(super) fn orphans(&self, data: &SourceText) -> Vec<Orphan> {
310 let orphan = |entry: &LineEntry| {
311 let scene = scene_of_key(&entry.key).unwrap_or_default();
312 let caption = is_caption(&entry.key);
313 let nearest = data
314 .lines
315 .iter()
316 .filter(|(key, line)| {
317 line.scene == scene
318 && (line.kind == LineKind::Caption) == caption
319 && !self.lines.contains_key(*key)
320 })
321 .min_by_key(|(_, line)| distance(&entry.source, &line.text))
322 .map(|(key, _)| key.clone());
323 Orphan {
324 key: entry.key.clone(),
325 source: entry.source.clone(),
326 text: entry.text.clone(),
327 nearest,
328 }
329 };
330 self.lines
331 .values()
332 .filter(|entry| !data.lines.contains_key(&entry.key))
333 .map(orphan)
334 .collect()
335 }
336}
337
338impl Lang {
339 pub fn line<'a>(
343 &'a self,
344 code: &LangCode,
345 key: &str,
346 english: &'a str,
347 variant: &str,
348 ) -> &'a str {
349 self.packs_along(code)
350 .find_map(|(_, pack)| pack.lines.get(key).filter(|entry| entry.source == english))
351 .map_or(english, |entry| entry.text.get(variant).unwrap_or(english))
352 }
353
354 #[must_use]
359 pub fn scene<D: Clone>(&self, code: &LangCode, scene: &Scene<D>, variant: &str) -> Scene<D> {
360 let mut scene = scene.clone();
361 if self.packs_along(code).next().is_some() {
362 let id = scene.id.clone();
363 self.translate(code, &id, &mut scene.steps, variant);
364 }
365 scene
366 }
367
368 fn translate<D>(&self, code: &LangCode, scene: &str, steps: &mut [Step<D>], variant: &str) {
370 let line = |id: &LineId, text: &mut String| {
371 let shown = self.line(code, id.as_str(), text, variant).to_owned();
372 *text = shown;
373 };
374 for step in steps {
375 match step {
376 Step::Caption { text } => {
377 let shown = self.line(code, &caption_key(scene, text), text, variant);
378 *text = shown.to_owned();
379 }
380 Step::Say { text, line: id, .. } | Step::Narrate { text, line: id, .. } => {
381 line(id, text);
382 }
383 Step::Choice { options } => {
384 for ChoiceOption {
385 text,
386 line: id,
387 steps,
388 ..
389 } in options
390 {
391 line(id, text);
392 self.translate(code, scene, steps, variant);
393 }
394 }
395 Step::If {
396 then, otherwise, ..
397 } => {
398 self.translate(code, scene, then, variant);
399 self.translate(code, scene, otherwise, variant);
400 }
401 Step::Place { .. } | Step::Clear { .. } | Step::Music(_) | Step::Directive(_) => {}
402 }
403 }
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use serde::de::IntoDeserializer;
410 use serde::de::value::{BoolDeserializer, Error};
411
412 use super::*;
413
414 #[test]
416 fn a_wrong_value_is_told_what_was_expected() {
417 let wrong = || -> BoolDeserializer<Error> { true.into_deserializer() };
418 let file = RawLines { variants: &[] }.deserialize(wrong());
419 assert_eq!(
420 file.err().map(|e| e.to_string()).as_deref(),
421 Some("invalid type: boolean `true`, expected a list of dialogue entries")
422 );
423 let entry = RawLineSeed { variants: &[] }.deserialize(wrong());
424 assert_eq!(
425 entry.err().map(|e| e.to_string()).as_deref(),
426 Some("invalid type: boolean `true`, expected a dialogue entry: key, source and text")
427 );
428 }
429
430 #[test]
431 fn distance_counts_single_character_edits() {
432 assert_eq!(distance("", ""), 0);
433 assert_eq!(distance("abc", "abc"), 0);
434 assert_eq!(distance("", "abc"), 3);
435 assert_eq!(distance("abc", ""), 3);
436 assert_eq!(distance("kitten", "sitting"), 3);
437 assert_eq!(distance("flaw", "lawn"), 2);
438 assert_eq!(distance("abc", "cab"), 2);
439 assert_eq!(distance("abcdef", "abdef"), 1);
440 assert_eq!(distance("abdef", "abcdef"), 1);
441 assert_eq!(distance("né", "ne"), 1);
443 }
444
445 #[test]
446 fn a_key_names_its_scene() {
447 assert_eq!(scene_of_key("ch1_gate_0123abcd"), Some("ch1_gate"));
448 assert_eq!(scene_of_key("ch1_gate_0123abcd_12"), Some("ch1_gate"));
449 assert_eq!(scene_of_key("caption.ch1_gate_0123abcd"), Some("ch1_gate"));
450 assert_eq!(scene_of_key("ch1_gate"), None);
451 assert_eq!(scene_of_key("caption.ch1_gate"), None);
452 assert!(is_caption("caption.a_0123abcd"));
453 assert!(!is_caption("a_0123abcd"));
454 }
455
456 #[test]
457 fn only_text_boxes_and_replies_have_a_limit() {
458 let limits = crate::dialogue::tests::LIMITS;
459 assert_eq!(limit(LineKind::Speech, &limits), Some(limits.max_text));
460 assert_eq!(limit(LineKind::Reply, &limits), Some(limits.max_option));
461 assert_eq!(limit(LineKind::Caption, &limits), None);
462 }
463
464 #[test]
465 fn the_message_names_every_variant() {
466 assert_eq!(variants_message(&[]), "an entry has `text`");
467 assert_eq!(variants_message(&["a"]), "an entry has `text`, or `text_a`");
468 assert_eq!(
469 variants_message(&["m", "f"]),
470 "an entry has `text`, or both `text_m` and `text_f`"
471 );
472 assert_eq!(
473 variants_message(&["a", "b", "c"]),
474 "an entry has `text`, or all of `text_a`, `text_b`, and `text_c`"
475 );
476 }
477
478 #[test]
479 fn a_line_has_its_one_text_or_its_variants() {
480 let one = LineText::One("x".to_owned());
481 assert_eq!(one.get("a"), Some("x"));
482 assert_eq!(one.all(), ["x"]);
483 let texts = [("a", "1"), ("b", "2")].map(|(v, t)| (v.to_owned(), t.to_owned()));
484 let both = LineText::Variants(texts.into());
485 assert_eq!(both.get("a"), Some("1"));
486 assert_eq!(both.get("b"), Some("2"));
487 assert_eq!(both.get("c"), None);
488 assert_eq!(both.all(), ["1", "2"]);
489 }
490}