1use crate::prelude::*;
17
18use alloc::collections::{BTreeMap, BTreeSet};
19
20use super::{Cast, CastVoice, ClipVariant, MadeBy, VoiceClip, clip_file};
21use crate::dialogue::{DialogueTable, LineId};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum Why {
27 Missing,
29 Words,
31 Direction,
33 Recast,
35 Voice(String),
37}
38
39impl Why {
40 pub fn says(&self) -> &str {
42 match self {
43 Self::Missing => "no clip",
44 Self::Words => "the words changed",
45 Self::Direction => "the direction changed",
46 Self::Recast => "the cast names another voice",
47 Self::Voice(reason) => reason,
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54#[non_exhaustive]
55pub struct WantedClip<V> {
56 pub line: LineId,
58 pub variant: V,
60 pub speaker: String,
62 pub key: String,
65 pub spoken: String,
67 pub direction: Option<String>,
69 pub before: Option<(String, String)>,
72}
73
74impl<V: ClipVariant> WantedClip<V> {
75 pub fn file(&self) -> String {
77 clip_file(&self.line, self.variant)
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83#[non_exhaustive]
84pub struct Uncast<V> {
85 pub line: LineId,
87 pub variant: V,
89 pub speaker: String,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95#[non_exhaustive]
96pub struct VoicePlan<V> {
97 pub wanted: Vec<WantedClip<V>>,
100 pub make: Vec<(usize, Why)>,
103 pub record: Vec<(usize, Why)>,
106 pub remove: Vec<VoiceClip<V>>,
110 pub uncast: Vec<Uncast<V>>,
112}
113
114impl<V> Default for VoicePlan<V> {
115 fn default() -> Self {
116 Self {
117 wanted: Vec::new(),
118 make: Vec::new(),
119 record: Vec::new(),
120 remove: Vec::new(),
121 uncast: Vec::new(),
122 }
123 }
124}
125
126pub fn plan<V: ClipVariant, S, D>(
133 dialogue: &DialogueTable<D>,
134 voiced: &BTreeSet<String>,
135 cast: &Cast<S>,
136 clips: &[VoiceClip<V>],
137 variants: impl Fn(&str, &str) -> &'static [V],
138 spoken: impl Fn(&str, V) -> Option<String>,
139 voice_changed: impl Fn(&VoiceClip<V>, &CastVoice<S>) -> Option<String>,
140) -> VoicePlan<V> {
141 let mut plan = VoicePlan::default();
142 let mut needed = BTreeSet::new();
143 let mut in_voiced = BTreeSet::new();
144 let mut all = BTreeSet::new();
145 for (id, scene) in &dialogue.scenes {
146 let lines = scene.lines();
147 all.extend(lines.iter().map(|l| l.id.clone()));
148 if !voiced.contains(id) {
149 continue;
150 }
151 let mut before = None;
152 for line in lines {
153 in_voiced.insert(line.id.clone());
154 for &variant in variants(line.speaker, line.text) {
155 let Some(words) = spoken(line.text, variant) else {
156 continue;
157 };
158 needed.insert((line.id.clone(), variant));
159 let Some(key) = key_for(cast, line.speaker, variant) else {
160 plan.uncast.push(Uncast {
161 line: line.id.clone(),
162 variant,
163 speaker: line.speaker.to_owned(),
164 });
165 continue;
166 };
167 plan.wanted.push(WantedClip {
168 line: line.id.clone(),
169 variant,
170 speaker: line.speaker.to_owned(),
171 key,
172 spoken: words,
173 direction: line.direction.map(str::to_owned),
174 before: before.clone(),
175 });
176 }
177 let shown = V::variants()
178 .into_iter()
179 .find_map(|v| spoken(line.text, v))
180 .unwrap_or_else(|| line.text.to_owned());
181 before = Some((line.speaker.to_owned(), shown));
182 }
183 }
184 let made: BTreeMap<(&LineId, V), &VoiceClip<V>> = clips
185 .iter()
186 .map(|clip| ((&clip.line, clip.variant), clip))
187 .collect();
188 for (i, wanted) in plan.wanted.iter().enumerate() {
189 let Some(&clip) = made.get(&(&wanted.line, wanted.variant)) else {
190 plan.make.push((i, Why::Missing));
191 continue;
192 };
193 let voice = &cast.voices[&wanted.key];
194 if let Some(why) = why_again(clip, wanted, voice, &voice_changed) {
195 match clip.made_by {
196 MadeBy::Recorded { .. } => plan.record.push((i, why)),
197 MadeBy::Generated { .. } => plan.make.push((i, why)),
198 }
199 }
200 }
201 plan.remove = clips
202 .iter()
203 .filter(|clip| {
204 let ours = in_voiced.contains(&clip.line) || !all.contains(&clip.line);
205 ours && !needed.contains(&(clip.line.clone(), clip.variant))
206 })
207 .cloned()
208 .collect();
209 plan
210}
211
212fn key_for<S, V: ClipVariant>(cast: &Cast<S>, speaker: &str, variant: V) -> Option<String> {
215 let own = format!("{speaker}{}", variant.suffix());
216 [own.as_str(), speaker]
217 .into_iter()
218 .find(|key| cast.voices.contains_key(*key))
219 .map(str::to_owned)
220}
221
222fn why_again<V, S>(
226 clip: &VoiceClip<V>,
227 wanted: &WantedClip<V>,
228 voice: &CastVoice<S>,
229 voice_changed: &impl Fn(&VoiceClip<V>, &CastVoice<S>) -> Option<String>,
230) -> Option<Why> {
231 if clip.spoken != wanted.spoken {
232 Some(Why::Words)
233 } else if clip.direction != wanted.direction {
234 Some(Why::Direction)
235 } else if matches!(clip.made_by, MadeBy::Recorded { .. }) {
236 None
237 } else if clip.voice != voice.voice {
238 Some(Why::Recast)
239 } else {
240 voice_changed(clip, voice).map(Why::Voice)
241 }
242}
243
244pub fn recording_script<'a, V>(plan: &'a VoicePlan<V>, key: &str) -> Vec<&'a WantedClip<V>> {
248 plan.wanted.iter().filter(|w| w.key == key).collect()
249}
250
251#[cfg(test)]
252mod tests;