1use crate::prelude::*;
30
31use alloc::collections::{BTreeMap, BTreeSet};
32use core::fmt::Debug;
33
34use serde::Deserialize;
35use serde::de::DeserializeOwned;
36
37use crate::dialogue::{DialogueTable, LineId, NARRATION_SPEAKER, Scene};
38use crate::error::{ContentError, line_of};
39use crate::ron::parse_ron;
40
41pub mod plan;
42
43pub trait ClipVariant: Copy + Ord + Default + Debug + DeserializeOwned + 'static {
46 fn suffix(self) -> &'static str;
49
50 fn is_only(self) -> bool {
52 self == Self::default()
53 }
54
55 fn split() -> &'static [Self];
58
59 fn variants() -> Vec<Self> {
63 let mut all = vec![Self::default()];
64 all.extend(Self::split().iter().copied().filter(|v| !v.is_only()));
65 all
66 }
67
68 fn incomplete(line: &str) -> String {
71 format!("line \"{line}\" lacks some of its variants' clips")
72 }
73
74 fn mixed(line: &str) -> String {
76 format!("line \"{line}\" has an only clip and variants' clips; keep one kind")
77 }
78}
79
80impl ClipVariant for () {
83 fn suffix(self) -> &'static str {
84 ""
85 }
86
87 fn split() -> &'static [Self] {
88 &[]
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
94#[serde(deny_unknown_fields)]
95pub enum MadeBy {
96 Generated {
98 tool: String,
100 model: String,
102 date: String,
104 },
105 Recorded {
107 actor: String,
109 },
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct VoiceClip<V> {
115 pub line: LineId,
117 pub variant: V,
119 pub spoken: String,
122 pub direction: Option<String>,
124 pub voice: String,
126 pub made_by: MadeBy,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct VoiceManifest<V> {
133 pub clips: Vec<VoiceClip<V>>,
135}
136
137impl<V> Default for VoiceManifest<V> {
138 fn default() -> Self {
139 Self { clips: Vec::new() }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
146#[non_exhaustive]
147pub struct Cast<S> {
148 pub voices: BTreeMap<String, CastVoice<S>>,
152}
153
154impl<S> Default for Cast<S> {
155 fn default() -> Self {
156 Self {
157 voices: BTreeMap::new(),
158 }
159 }
160}
161
162impl<S> Cast<S> {
163 pub fn new(voices: BTreeMap<String, CastVoice<S>>) -> Self {
165 Self { voices }
166 }
167
168 pub fn voice_for<V: ClipVariant>(&self, speaker: &str, variant: V) -> Option<&CastVoice<S>> {
172 let suffix = variant.suffix();
173 (!suffix.is_empty())
174 .then(|| self.voices.get(&format!("{speaker}{suffix}")))
175 .flatten()
176 .or_else(|| self.voices.get(speaker))
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
182#[serde(deny_unknown_fields)]
183#[non_exhaustive]
184pub struct CastVoice<S> {
185 pub voice: String,
188 pub settings: S,
191}
192
193impl<S> CastVoice<S> {
194 pub fn new(voice: impl Into<String>, settings: S) -> Self {
196 Self {
197 voice: voice.into(),
198 settings,
199 }
200 }
201}
202
203#[derive(Deserialize)]
204#[serde(deny_unknown_fields)]
205#[serde(bound = "V: ClipVariant")]
206struct RawManifest<V> {
207 clips: Vec<RawClip<V>>,
208}
209
210#[derive(Deserialize)]
211#[serde(deny_unknown_fields)]
212#[serde(bound = "V: ClipVariant")]
213struct RawClip<V> {
214 line: String,
215 #[serde(default)]
216 variant: V,
217 spoken: String,
218 #[serde(default)]
219 direction: Option<String>,
220 voice: String,
221 made_by: MadeBy,
222}
223
224#[derive(Deserialize)]
225#[serde(deny_unknown_fields)]
226#[serde(bound = "S: DeserializeOwned")]
227struct RawCast<S> {
228 voices: BTreeMap<String, CastVoice<S>>,
229}
230
231pub fn clip_file<V: ClipVariant>(line: &LineId, variant: V) -> String {
235 format!("{}/{line}{}.ogg", line.scene_id(), variant.suffix())
236}
237
238pub fn line_texts<D>(dialogue: &DialogueTable<D>) -> BTreeMap<&str, &str> {
240 dialogue
241 .scenes
242 .values()
243 .flat_map(Scene::lines)
244 .map(|line| (line.id.as_str(), line.text))
245 .collect()
246}
247
248pub fn from_source<V: ClipVariant, D>(
256 file: &str,
257 source: &str,
258 dialogue: &DialogueTable<D>,
259) -> Result<VoiceManifest<V>, Vec<ContentError>> {
260 let raw: RawManifest<V> = parse_ron(file, source).map_err(|e| vec![e])?;
261 let texts = line_texts(dialogue);
262 let mut errors = Vec::new();
263 let mut err = |line: &str, message: String| {
264 let e = ContentError::new(file, message);
265 errors.push(match line_of(source, &format!("\"{line}\"")) {
266 Some(l) => e.at(l, None),
267 None => e,
268 });
269 };
270 let mut variants: BTreeMap<&str, Vec<V>> = BTreeMap::new();
271 for clip in &raw.clips {
272 let id = clip.line.as_str();
273 if !texts.contains_key(id) {
274 err(id, format!("no dialogue line has the id \"{id}\""));
275 }
276 let seen = variants.entry(id).or_default();
277 if seen.contains(&clip.variant) {
278 err(
279 id,
280 format!("line \"{id}\" has two {:?} clips", clip.variant),
281 );
282 }
283 seen.push(clip.variant);
284 if clip.spoken.trim().is_empty() {
285 err(id, format!("the clip of line \"{id}\" has no spoken text"));
286 }
287 if clip.voice.trim().is_empty() {
288 err(id, format!("the clip of line \"{id}\" has no voice"));
289 }
290 }
291 for (&id, seen) in &variants {
292 let split = V::split();
293 let some = split.iter().any(|v| seen.contains(v));
294 if some && !split.iter().all(|v| seen.contains(v)) {
295 err(id, V::incomplete(id));
296 }
297 if some && seen.iter().any(|v| v.is_only()) {
298 err(id, V::mixed(id));
299 }
300 }
301 if !errors.is_empty() {
302 return Err(errors);
303 }
304 let clips = raw
305 .clips
306 .into_iter()
307 .map(|c| VoiceClip {
308 line: LineId::new(c.line),
309 variant: c.variant,
310 spoken: c.spoken,
311 direction: c.direction,
312 voice: c.voice,
313 made_by: c.made_by,
314 })
315 .collect();
316 Ok(VoiceManifest { clips })
317}
318
319pub fn cast_from_source<S: DeserializeOwned, V: ClipVariant>(
327 file: &str,
328 source: &str,
329 can_speak: impl Fn(&str) -> bool,
330) -> Result<Cast<S>, Vec<ContentError>> {
331 let raw: RawCast<S> = parse_ron(file, source).map_err(|e| vec![e])?;
332 let speaks = |speaker: &str| speaker == NARRATION_SPEAKER || can_speak(speaker);
333 let suffixes: Vec<&str> = V::variants()
334 .into_iter()
335 .map(V::suffix)
336 .filter(|s| !s.is_empty())
337 .collect();
338 let mut errors = Vec::new();
339 for (key, entry) in &raw.voices {
340 let mut err = |message: String| {
341 let e = ContentError::new(file, message);
342 errors.push(match line_of(source, &format!("\"{key}\"")) {
343 Some(l) => e.at(l, None),
344 None => e,
345 });
346 };
347 if entry.voice.trim().is_empty() {
348 err(format!("\"{key}\" has no voice"));
349 }
350 if speaks(key) {
351 continue;
352 }
353 let mut speakers = suffixes.iter().filter_map(|s| key.strip_suffix(s));
354 if let Some(speaker) = speakers.clone().find(|s| speaks(s)).or(speakers.next()) {
355 if !speaks(speaker) {
356 err(format!(
357 "\"{key}\": \"{speaker}\" is not a character that speaks"
358 ));
359 }
360 } else if let Some(at) = (1..key.len())
361 .filter(|&at| key.is_char_boundary(at))
362 .find(|&at| speaks(&key[..at]))
363 {
364 let known: Vec<String> = suffixes.iter().map(|s| format!("\"{s}\"")).collect();
365 err(format!(
366 "\"{key}\": \"{}\" is not a variant's suffix (those are: {})",
367 &key[at..],
368 known.join(", ")
369 ));
370 } else {
371 err(format!("\"{key}\" is not a character that speaks"));
372 }
373 }
374 if errors.is_empty() {
375 Ok(Cast { voices: raw.voices })
376 } else {
377 Err(errors)
378 }
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
384pub struct Playable<V> {
385 clips: BTreeSet<(LineId, V)>,
386}
387
388impl<V> Default for Playable<V> {
389 fn default() -> Self {
390 Self {
391 clips: BTreeSet::new(),
392 }
393 }
394}
395
396impl<V: ClipVariant> Playable<V> {
397 pub fn variant_for(&self, line: &LineId, variant: V) -> Option<V> {
400 [V::default(), variant]
401 .into_iter()
402 .find(|&v| self.clips.contains(&(line.clone(), v)))
403 }
404
405 pub fn len(&self) -> usize {
407 self.clips.len()
408 }
409
410 pub fn is_empty(&self) -> bool {
412 self.clips.is_empty()
413 }
414}
415
416impl<V: ClipVariant> VoiceManifest<V> {
417 pub fn is_stale<D>(
422 clip: &VoiceClip<V>,
423 dialogue: &DialogueTable<D>,
424 spoken: impl Fn(&str, V) -> Option<String>,
425 ) -> bool {
426 let Some(&text) = line_texts(dialogue).get(clip.line.as_str()) else {
427 return true;
428 };
429 stale(clip, text, &spoken)
430 }
431
432 pub fn needs_remake<D>(
437 clip: &VoiceClip<V>,
438 dialogue: &DialogueTable<D>,
439 spoken: impl Fn(&str, V) -> Option<String>,
440 ) -> bool {
441 let line = dialogue
442 .get(clip.line.scene_id())
443 .map(Scene::lines)
444 .and_then(|lines| lines.into_iter().find(|line| *line.id == clip.line));
445 line.is_none_or(|line| {
446 stale(clip, line.text, &spoken) || clip.direction.as_deref() != line.direction
447 })
448 }
449
450 pub fn playable<D>(
452 &self,
453 dialogue: &DialogueTable<D>,
454 spoken: impl Fn(&str, V) -> Option<String>,
455 ) -> Playable<V> {
456 let texts = line_texts(dialogue);
457 let clips = self
458 .clips
459 .iter()
460 .filter(|clip| {
461 texts
462 .get(clip.line.as_str())
463 .is_some_and(|text| !stale(clip, text, &spoken))
464 })
465 .map(|clip| (clip.line.clone(), clip.variant))
466 .collect();
467 Playable { clips }
468 }
469}
470
471fn stale<V: ClipVariant>(
473 clip: &VoiceClip<V>,
474 text: &str,
475 spoken: &impl Fn(&str, V) -> Option<String>,
476) -> bool {
477 spoken(text, clip.variant).is_none_or(|words| words != clip.spoken)
478}
479
480#[cfg(test)]
481mod tests;