1use crate::prelude::*;
4
5use alloc::collections::BTreeSet;
6
7use super::{
8 DATA_FILE, INFO_FILE, LangPack, PackRules, PackSources, SourceLang, SourceText, UI_FILE,
9 english_from_source, is_dialogue_file, pack_code, pack_from_files_in,
10};
11use crate::dialogue::{DialogueLimits, Tokens};
12use crate::error::ContentError;
13use crate::font::{CellWidths, FontAtlasDef};
14use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, join, text};
15use crate::import_settings::is_settings_file;
16
17impl AssetKind for LangPack {
18 const NAME: &'static str = "language pack";
19}
20
21const NOT_A_LANGUAGE_FILE: &str = "not a file a language has";
23
24pub struct LangPackRon {
39 data_problem: fn(&str, &str) -> Option<String>,
40 tokens: &'static (dyn Tokens + Sync),
41 limits: DialogueLimits,
42 wide: BTreeSet<char>,
45 sets: Option<BTreeSet<String>>,
47 variants: &'static [&'static str],
48 data: Option<SourceText>,
49 source: SourceLang,
50}
51
52impl LangPackRon {
53 pub fn new(limits: DialogueLimits, tokens: &'static (dyn Tokens + Sync)) -> Self {
57 Self {
58 data_problem: |_, _| None,
59 tokens,
60 limits,
61 wide: BTreeSet::new(),
62 sets: None,
63 variants: &[],
64 data: None,
65 source: SourceLang::default(),
66 }
67 }
68
69 #[must_use]
72 pub fn source_lang(self, source: SourceLang) -> Self {
73 Self { source, ..self }
74 }
75
76 #[must_use]
80 pub fn widths_of(self, atlas: &FontAtlasDef) -> Self {
81 Self {
82 wide: atlas.wide_chars(),
83 sets: Some(atlas.wide_sets.keys().cloned().collect()),
84 ..self
85 }
86 }
87
88 #[must_use]
91 pub fn data_problem(self, data_problem: fn(&str, &str) -> Option<String>) -> Self {
92 Self {
93 data_problem,
94 ..self
95 }
96 }
97
98 #[must_use]
101 pub fn variants(self, variants: &'static [&'static str]) -> Self {
102 Self { variants, ..self }
103 }
104
105 #[must_use]
108 pub fn with_data(self, data: SourceText) -> Self {
109 Self {
110 data: Some(data),
111 ..self
112 }
113 }
114}
115
116impl core::fmt::Debug for LangPackRon {
117 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118 f.debug_struct("LangPackRon")
119 .field("limits", &self.limits)
120 .field("wide", &self.wide)
121 .field("sets", &self.sets)
122 .field("variants", &self.variants)
123 .field("data", &self.data.is_some())
124 .field("source", &self.source.code.as_str())
125 .finish_non_exhaustive()
126 }
127}
128
129impl Format for LangPackRon {
130 type Asset = LangPack;
131
132 fn id(&self) -> &'static str {
133 "lang-pack"
134 }
135
136 fn extensions(&self) -> &'static [&'static str] {
137 &["ron"]
138 }
139
140 fn max_bytes(&self) -> usize {
141 TEXT_MAX_BYTES
142 }
143
144 fn decode(
145 &self,
146 file: &AssetPath,
147 bytes: &[u8],
148 cx: &DecodeCx<'_>,
149 ) -> Result<LangPack, Vec<ContentError>> {
150 let info = text(file, bytes)?;
151 let dir = file.dir();
152 let (languages, code) = dir.rsplit_once('/').unwrap_or(("", dir));
153 let source = &self.source.code;
154 if file.name() != INFO_FILE || code == source.as_str() {
155 let message = format!("{NOT_A_LANGUAGE_FILE}: a pack is read from its {INFO_FILE}");
156 return Err(vec![ContentError::new(file.shown(), message)]);
157 }
158 if let Err(message) = pack_code(code) {
159 return Err(vec![ContentError::new(cx.shown(dir), message)]);
160 }
161 let english_path = join(&join(languages, source.as_str()), UI_FILE);
162 let english_shown = cx.shown(&english_path);
163 let english = english_from_source(
164 &english_shown,
165 cx.text(&english_path)
166 .ok_or_else(|| vec![ContentError::not_found(&english_shown)])?,
167 )?;
168 let mut errors = Vec::new();
169 let mut read = |path: &str| {
170 let source = cx.text(path);
171 if source.is_none() {
172 errors.push(ContentError::not_found(cx.shown(path)));
173 }
174 source.map(|source| (cx.shown(path), source))
175 };
176 let ui = read(&join(dir, UI_FILE));
177 let data_path = join(dir, DATA_FILE);
178 let data = cx.has(&data_path).then(|| read(&data_path)).flatten();
179 let mut dialogue = Vec::new();
180 let mut unknown = Vec::new();
181 for path in cx.files_under(dir) {
182 let name = path
183 .strip_prefix(dir)
184 .and_then(|name| name.strip_prefix('/'))
185 .unwrap_or(path);
186 if is_dialogue_file(name) {
187 dialogue.extend(read(path));
188 } else if !is_settings_file(name) && ![UI_FILE, INFO_FILE, DATA_FILE].contains(&name) {
189 unknown.push(ContentError::new(cx.shown(path), NOT_A_LANGUAGE_FILE));
190 }
191 }
192 errors.extend(unknown);
193 let Some((ui_shown, ui)) = ui else {
194 return Err(errors);
195 };
196 let sources = PackSources {
197 info: (file.shown(), info),
198 ui: (&ui_shown, ui),
199 data: data
200 .as_ref()
201 .map(|(shown, source)| (shown.as_str(), *source)),
202 dialogue: dialogue.iter().map(|(n, s)| (n.as_str(), *s)).collect(),
203 };
204 let widths = CellWidths::wide(self.wide.iter().copied());
205 let rules = PackRules {
206 data_problem: self.data_problem,
207 tokens: self.tokens,
208 limits: self.limits,
209 widths: &widths,
210 variants: self.variants,
211 };
212 let pack = pack_from_files_in(source, &sources, &english, self.data.as_ref(), &rules);
213 if let (Ok(pack), Some(sets)) = (&pack, &self.sets)
214 && let Some(set) = pack.info.wide_font.as_deref()
215 && !sets.contains(set)
216 {
217 let message = format!(
218 "wide_font names {set:?}, a wide set the font atlas doesn't have: it has {}",
219 if sets.is_empty() {
220 "none".to_owned()
221 } else {
222 let names: Vec<String> = sets.iter().map(|s| format!("{s:?}")).collect();
223 names.join(", ")
224 }
225 );
226 errors.push(ContentError::new(file.shown(), message));
227 }
228 match pack {
229 Ok(pack) if errors.is_empty() => Ok(pack),
230 Ok(_) => Err(errors),
231 Err(e) => {
232 errors.extend(e);
233 Err(errors)
234 }
235 }
236 }
237}