1use crate::prelude::*;
7
8use alloc::collections::BTreeMap;
9
10use shuyi_core::text::{Part, split_tokens};
11
12use super::line_id::line_hash;
13use super::parse::{Lines, ParsedScene, PartLines};
14use super::{
15 CharacterId, ChoiceOption, DialogueLimits, Directive, MusicLine, NARRATION_SPEAKER,
16 REPLY_SPEAKER, Side, Step,
17};
18use crate::error::ContentError;
19use crate::font::CellWidths;
20use crate::lang::text_width;
21use crate::names::{Literals, Names, is_name_id, name_token};
22use crate::text::word_wrap;
23
24pub trait Tokens {
27 fn longest(&self, token: &str) -> Option<usize>;
30
31 fn unknown(&self, token: &str) -> String {
34 format!("unknown token \"{{{token}}}\"; use a name, {{n:<id>}}")
35 }
36
37 fn reserved_name(&self, _id: &str, _token: &str) -> Option<String> {
41 None
42 }
43
44 fn unknown_name(&self, id: &str, token: &str) -> String {
47 format!("unknown name id \"{id}\" in {{{token}}}")
48 }
49
50 fn written_name(&self, _id: &str, _form: &str) -> Option<String> {
54 None
55 }
56}
57
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
61pub struct NoTokens;
62
63impl Tokens for NoTokens {
64 fn longest(&self, _token: &str) -> Option<usize> {
65 None
66 }
67}
68
69pub trait SceneRules<D> {
73 fn limits(&self) -> &DialogueLimits;
75
76 fn widths(&self) -> &CellWidths {
80 &CellWidths::NARROW
81 }
82
83 fn tokens(&self) -> &dyn Tokens;
85
86 fn names(&self) -> Option<&Names> {
90 None
91 }
92
93 fn can_speak(&self, _character: &CharacterId) -> bool {
95 true
96 }
97
98 fn expression_problem(&self, _character: &CharacterId, _expression: &str) -> Option<String> {
100 None
101 }
102
103 fn speech_problem(&self, _speaker: &CharacterId, _cells: usize) -> Option<String> {
106 None
107 }
108
109 fn music_problem(&self, _cue: &str) -> Option<String> {
111 None
112 }
113
114 fn scene_problems(&self, _parsed: &ParsedScene<D>) -> Vec<ContentError> {
118 Vec::new()
119 }
120}
121
122type Screen<'a> = [Option<&'a CharacterId>; 2];
124
125pub fn check_scene<D: Directive>(
128 parsed: &ParsedScene<D>,
129 rules: &impl SceneRules<D>,
130) -> Vec<ContentError> {
131 let mut c = Checker {
132 file: &parsed.file,
133 rules,
134 limits: *rules.limits(),
135 widths: rules.widths(),
136 tokens: rules.tokens(),
137 names: rules.names(),
138 longest_name: rules.names().map(|names| names.longest(rules.widths())),
139 literals: rules.names().map(Literals::of),
140 hashes: BTreeMap::new(),
141 errors: Vec::new(),
142 };
143 let mut state = State::default();
144 c.steps(&parsed.scene.steps, &parsed.lines, &mut state);
145 if !parsed.scene.steps.iter().any(Step::has_text) {
146 c.err(
147 parsed.line,
148 format!("scene \"{}\" has no speech or narration", parsed.scene.id),
149 );
150 }
151 c.errors
152}
153
154fn texts<D>(steps: &[Step<D>]) -> usize {
157 let count = |step: &Step<D>| match step {
158 Step::If {
159 then, otherwise, ..
160 } => texts(then).max(texts(otherwise)),
161 _ => usize::from(step.text().is_some()),
162 };
163 steps.iter().map(count).sum()
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
170struct State<'a, D> {
171 screen: Screen<'a>,
172 caption: Option<&'a str>,
173 in_force: BTreeMap<&'static str, &'a D>,
174}
175
176impl<D> Default for State<'_, D> {
177 fn default() -> Self {
178 Self {
179 screen: [None; 2],
180 caption: None,
181 in_force: BTreeMap::new(),
182 }
183 }
184}
185
186impl<D: PartialEq> State<'_, D> {
187 fn other_directive(&self, other: &Self) -> Option<&'static str> {
190 let mut kinds: Vec<&'static str> = self
191 .in_force
192 .keys()
193 .chain(other.in_force.keys())
194 .copied()
195 .collect();
196 kinds.sort_unstable();
197 kinds
198 .into_iter()
199 .find(|kind| self.in_force.get(kind) != other.in_force.get(kind))
200 }
201}
202
203struct Checker<'a, R> {
204 file: &'a str,
205 rules: &'a R,
206 limits: DialogueLimits,
207 widths: &'a CellWidths,
209 tokens: &'a dyn Tokens,
210 names: Option<&'a Names>,
211 longest_name: Option<usize>,
214 literals: Option<Literals<'a>>,
216 hashes: BTreeMap<String, (u32, String, String)>,
219 errors: Vec<ContentError>,
220}
221
222pub fn longest_width(
229 text: &str,
230 tokens: &dyn Tokens,
231 longest_name: Option<usize>,
232 widths: &CellWidths,
233) -> usize {
234 split_tokens(text)
235 .map(|part| match part {
236 Part::Text(t) | Part::Unclosed(t) => text_width(t, widths),
237 Part::Token(t) => token_cells(t, tokens, longest_name, widths),
238 })
239 .sum()
240}
241
242fn token_cells(
244 token: &str,
245 tokens: &dyn Tokens,
246 longest_name: Option<usize>,
247 widths: &CellWidths,
248) -> usize {
249 match (name_token(token), longest_name) {
250 (Some(_), Some(longest)) => longest,
251 _ => tokens
252 .longest(token)
253 .unwrap_or(text_width(token, widths) + 2),
254 }
255}
256
257pub fn lines_at_longest(
264 text: &str,
265 tokens: &dyn Tokens,
266 longest_name: Option<usize>,
267 width: usize,
268 widths: &CellWidths,
269) -> usize {
270 let mut filled = String::new();
271 for part in split_tokens(text) {
272 match part {
273 Part::Text(t) | Part::Unclosed(t) => filled.push_str(t),
274 Part::Token(t) => {
275 filled.push_str(&"W".repeat(token_cells(t, tokens, longest_name, widths)));
276 }
277 }
278 }
279 word_wrap(&filled, width, widths).len()
280}
281
282fn name_token_problem(token: &str, tokens: &dyn Tokens, names: Option<&Names>) -> Option<String> {
285 let id = name_token(token).map_or("", |t| t.id);
286 if let Some(problem) = tokens.reserved_name(id, token) {
287 Some(problem)
288 } else if !is_name_id(id) || names.is_some_and(|n| n.get(id).is_none()) {
289 Some(tokens.unknown_name(id, token))
290 } else {
291 None
292 }
293}
294
295pub fn token_problems(text: &str, tokens: &dyn Tokens, names: Option<&Names>) -> Vec<String> {
300 split_tokens(text)
301 .filter_map(|part| match part {
302 Part::Token(t) if name_token(t).is_some() => name_token_problem(t, tokens, names),
303 Part::Token(t) if tokens.longest(t).is_none() => Some(tokens.unknown(t)),
304 Part::Unclosed(_) => Some("\"{\" has no closing \"}\"".to_owned()),
305 Part::Text(_) | Part::Token(_) => None,
306 })
307 .collect()
308}
309
310fn slot(side: Side) -> usize {
311 match side {
312 Side::Left => 0,
313 Side::Right => 1,
314 }
315}
316
317impl<R> Checker<'_, R> {
318 fn err(&mut self, line: u32, message: String) {
319 self.errors
320 .push(ContentError::new(self.file, message).at(line, None));
321 }
322
323 fn line_id(&mut self, line: u32, speaker: &str, text: &str) {
327 let hash = line_hash(speaker, text);
328 match self.hashes.get(&hash) {
329 None => {
330 self.hashes
331 .insert(hash, (line, speaker.to_owned(), text.to_owned()));
332 }
333 Some((_, s, t)) if s == speaker && t == text => {}
334 Some((first, _, t)) => {
335 let message = format!(
336 "this line and line {first} (\"{t}\") get the same line id hash \
337 {hash}; reword one of them"
338 );
339 self.err(line, message);
340 }
341 }
342 }
343
344 fn pages(&self, text: &str) -> usize {
346 lines_at_longest(
347 text,
348 self.tokens,
349 self.longest_name,
350 self.limits.box_width,
351 self.widths,
352 )
353 }
354
355 fn len(&self, text: &str) -> usize {
357 longest_width(text, self.tokens, self.longest_name, self.widths)
358 }
359
360 fn literal_names(&mut self, line: u32, text: &str) {
363 let (Some(names), Some(literals)) = (self.names, &self.literals) else {
364 return;
365 };
366 let Some((id, form)) = literals.find_in(text) else {
367 return;
368 };
369 let message = self.tokens.written_name(id, form).unwrap_or_else(|| {
370 let name = names.get(id).unwrap_or(form);
371 format!(
372 "\"{form}\" is written out; write {{n:{id}}} (\"{name}\") so a rename reaches this line"
373 )
374 });
375 self.err(line, message);
376 }
377
378 fn tokens(&mut self, line: u32, text: &str) {
381 for message in token_problems(text, self.tokens, self.names) {
382 self.err(line, message);
383 }
384 }
385}
386
387impl<R> Checker<'_, R> {
388 fn steps<'s, D: Directive>(
390 &mut self,
391 steps: &'s [Step<D>],
392 lines: &Lines,
393 state: &mut State<'s, D>,
394 ) where
395 R: SceneRules<D>,
396 {
397 let mut blocks = lines.blocks.iter();
398 for (step, &line) in steps.iter().zip(&lines.steps) {
399 match step {
400 Step::Choice { options } => {
401 if let Some(parts) = blocks.next() {
402 self.choice(line, options, parts, state);
403 }
404 }
405 Step::If {
406 character,
407 then,
408 otherwise,
409 } => {
410 if let Some(parts) = blocks.next() {
411 self.branch(line, character, [then, otherwise], parts, state);
412 }
413 }
414 _ => self.step(step, line, state),
415 }
416 }
417 }
418
419 fn branch<'s, D: Directive>(
423 &mut self,
424 line: u32,
425 character: &CharacterId,
426 branches: [&'s [Step<D>]; 2],
427 parts: &[PartLines],
428 state: &mut State<'s, D>,
429 ) where
430 R: SceneRules<D>,
431 {
432 if !self.rules.can_speak(character) {
433 self.err(line, format!("unknown character \"{}\"", character.0));
434 }
435 let mut after = [state.clone(), state.clone()];
436 for ((steps, part), after) in branches.into_iter().zip(parts).zip(&mut after) {
437 self.steps(steps, &part.lines, after);
438 }
439 let [with, without] = after;
440 let at = parts.get(1).map_or(line, |p| p.line);
442 if with.screen != without.screen {
443 self.err(
444 at,
445 format!(
446 "with \"{}\" here {}; without, {}; an @if block must leave the same \
447 characters on screen either way",
448 character.0,
449 describe(with.screen),
450 describe(without.screen)
451 ),
452 );
453 } else if with.caption != without.caption {
454 self.err(
455 at,
456 format!(
457 "this @if block leaves a different caption with \"{}\" here and without; \
458 it must leave the same caption either way",
459 character.0
460 ),
461 );
462 } else if let Some(kind) = with.other_directive(&without) {
463 self.err(
464 at,
465 format!(
466 "this @if block leaves a different @{kind} with \"{}\" here and without; \
467 it must leave the same {kind} either way",
468 character.0
469 ),
470 );
471 }
472 *state = with;
473 }
474
475 fn step<'s, D: Directive>(&mut self, step: &'s Step<D>, line: u32, state: &mut State<'s, D>)
477 where
478 R: SceneRules<D>,
479 {
480 if let Some(text) = step.text() {
481 self.tokens(line, text);
482 self.literal_names(line, text);
483 let len = self.len(text);
484 let limit = self.limits.max_text;
485 if len > limit {
486 self.err(
487 line,
488 format!("text is {len} characters; the limit is {limit}"),
489 );
490 } else if self.pages(text) > self.limits.box_lines {
491 let lines = self.pages(text);
492 let box_lines = self.limits.box_lines;
493 self.err(
494 line,
495 format!(
496 "text takes {lines} lines of {} characters; the box shows {box_lines}",
497 self.limits.box_width
498 ),
499 );
500 }
501 }
502 match step {
503 Step::Caption { text } => {
504 self.tokens(line, text);
505 self.literal_names(line, text);
506 state.caption = Some(text);
507 }
508 Step::Place {
509 side,
510 character,
511 expression,
512 } => {
513 if !self.rules.can_speak(character) {
514 self.err(line, format!("unknown character \"{}\"", character.0));
515 }
516 if let Some(message) = self.rules.expression_problem(character, expression) {
517 self.err(line, message);
518 }
519 if state.screen[slot(side.other())] == Some(character) {
520 self.err(
521 line,
522 format!(
523 "\"{}\" is already on the {}; a character can't be on both sides",
524 character.0,
525 side.other().name()
526 ),
527 );
528 }
529 state.screen[slot(*side)] = Some(character);
530 }
531 Step::Clear { side } => state.screen[slot(*side)] = None,
532 Step::Say {
533 speaker,
534 expression,
535 text,
536 ..
537 } => {
538 self.line_id(line, &speaker.0, text);
539 if !self.rules.can_speak(speaker) {
540 self.err(line, format!("unknown character \"{}\"", speaker.0));
541 } else if !state.screen.contains(&Some(speaker)) {
542 self.err(
543 line,
544 format!(
545 "\"{}\" speaks but is not on screen; place them with @left or @right first",
546 speaker.0
547 ),
548 );
549 }
550 let problem = expression
551 .as_deref()
552 .and_then(|e| self.rules.expression_problem(speaker, e));
553 if let Some(message) = problem {
554 self.err(line, message);
555 }
556 let len = self.len(text);
557 if let Some(message) = self.rules.speech_problem(speaker, len) {
558 self.err(line, message);
559 }
560 }
561 Step::Music(MusicLine::Cue(cue)) => {
562 if let Some(message) = self.rules.music_problem(cue) {
563 self.err(line, message);
564 }
565 }
566 Step::Narrate { text, .. } => self.line_id(line, NARRATION_SPEAKER, text),
567 Step::Directive(directive) => {
568 if let Some(kind) = directive.kind() {
569 state.in_force.insert(kind, directive);
570 }
571 }
572 Step::Choice { .. } | Step::If { .. } | Step::Music(MusicLine::Stop) => {}
573 }
574 }
575
576 fn choice<'s, D: Directive>(
579 &mut self,
580 line: u32,
581 options: &'s [ChoiceOption<D>],
582 parts: &[PartLines],
583 state: &mut State<'s, D>,
584 ) where
585 R: SceneRules<D>,
586 {
587 let DialogueLimits {
588 min_options: min,
589 max_options: max,
590 max_option,
591 max_reaction_texts,
592 ..
593 } = self.limits;
594 let n = options.len();
595 if !(min..=max).contains(&n) {
596 let needs = match max - min {
597 0 => format!("{min}"),
598 1 => format!("{min} or {max}"),
599 _ => format!("{min} to {max}"),
600 };
601 self.err(line, format!("@choice has {n} options; it needs {needs}"));
602 }
603 let mut first: Option<State<'s, D>> = None;
604 for (option, part) in options.iter().zip(parts) {
605 let at = part.line;
606 self.line_id(at, REPLY_SPEAKER, &option.text);
607 self.tokens(at, &option.text);
608 self.literal_names(at, &option.text);
609 let len = self.len(&option.text);
610 if len > max_option {
611 self.err(
612 at,
613 format!(
614 "option text is {len} characters; the limit is {max_option}, so it fits the menu"
615 ),
616 );
617 }
618 let texts = texts(&option.steps);
619 if texts > max_reaction_texts {
620 self.err(
621 at,
622 format!(
623 "reaction has {texts} speech or narration lines; the limit is \
624 {max_reaction_texts}, so the scene rejoins quickly"
625 ),
626 );
627 }
628 let mut after = state.clone();
629 self.steps(&option.steps, &part.lines, &mut after);
630 let Some(f) = &first else {
631 first = Some(after);
632 continue;
633 };
634 if f.screen != after.screen {
635 self.err(
636 at,
637 format!(
638 "after this reaction {}; after the first option's {}; every option \
639 must leave the same characters on screen",
640 describe(after.screen),
641 describe(f.screen)
642 ),
643 );
644 } else if f.caption != after.caption {
645 self.err(
646 at,
647 "this reaction leaves a different caption from the first option's; every \
648 option must leave the same caption"
649 .to_owned(),
650 );
651 } else if let Some(kind) = f.other_directive(&after) {
652 self.err(
653 at,
654 format!(
655 "this reaction leaves a different @{kind} from the first option's; \
656 every option must leave the same {kind}"
657 ),
658 );
659 }
660 }
661 if let Some(f) = first {
662 *state = f;
663 }
664 }
665}
666
667fn describe(screen: Screen) -> String {
670 fn name(c: Option<&CharacterId>) -> &str {
671 c.map_or("nobody", |c| c.0.as_str())
672 }
673 format!(
674 "{} is on the left and {} on the right",
675 name(screen[0]),
676 name(screen[1])
677 )
678}
679
680pub fn check_duplicates<D>(scenes: &[ParsedScene<D>]) -> Vec<ContentError> {
683 let mut first: BTreeMap<&str, &ParsedScene<D>> = BTreeMap::new();
684 let mut errors = Vec::new();
685 for s in scenes {
686 if let Some(f) = first.get(s.scene.id.as_str()) {
687 errors.push(
688 ContentError::new(
689 &s.file,
690 format!(
691 "duplicate scene id \"{}\"; first used at {}:{}",
692 s.scene.id, f.file, f.line
693 ),
694 )
695 .at(s.line, None),
696 );
697 } else {
698 first.insert(&s.scene.id, s);
699 }
700 }
701 errors
702}