1use std::collections::BTreeMap;
14use std::fmt::{Debug, Write as _};
15use std::fs;
16use std::path::Path;
17
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20use shuyi_assets::ContentError;
21use shuyi_assets::audio::{AudioManifest, MusicTrack, Sound};
22use shuyi_assets::bundle::{Bundle, Layer};
23use shuyi_assets::credits::CreditsFile;
24use shuyi_assets::dialogue::{DialogueFile, NoDirective};
25use shuyi_assets::font::FontAtlasDef;
26use shuyi_assets::format::{AssetKind, AssetPath, DecodeCx, Formats};
27use shuyi_assets::image::ImageInfo;
28use shuyi_assets::lighting::Lighting;
29use shuyi_assets::names::Names;
30use shuyi_assets::palette::PaletteDef;
31
32use super::{Answer, Op, Problem, codes};
33
34pub const MAX_FOLDER_FILES: usize = 1_000;
36
37pub const MAX_FOLDER_BYTES: usize = 64 << 20;
39
40#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
42#[serde(deny_unknown_fields)]
43#[non_exhaustive]
46pub struct ImportInput {
47 pub file: String,
50 #[serde(default)]
52 pub kind: Option<String>,
53 #[serde(default)]
55 pub print: bool,
56}
57
58impl ImportInput {
59 pub fn new(file: impl Into<String>) -> Self {
61 Self {
62 file: file.into(),
63 kind: None,
64 print: false,
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
71#[serde(deny_unknown_fields)]
72pub struct Imported {
73 pub kind: String,
75 pub summary: String,
77 pub text: Option<String>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
84#[serde(deny_unknown_fields)]
85pub struct ImportOutput {
86 pub file: String,
88 pub read_as: Vec<Imported>,
90 pub errors: Vec<Problem>,
92}
93
94type Read = (String, Option<String>, bool);
97
98trait ImportKind: Send + Sync {
100 fn name(&self) -> &'static str;
101
102 fn claims(&self, formats: &Formats, path: &str) -> bool;
104
105 fn read(
106 &self,
107 formats: &Formats,
108 file: &AssetPath,
109 bytes: &[u8],
110 cx: &DecodeCx<'_>,
111 print: bool,
112 ) -> Result<Read, Vec<ContentError>>;
113}
114
115struct KindOf<K> {
118 summary: fn(&K) -> String,
119 text: Option<fn(&K) -> String>,
120}
121
122impl<K: AssetKind + Debug> ImportKind for KindOf<K> {
123 fn name(&self) -> &'static str {
124 K::NAME
125 }
126
127 fn claims(&self, formats: &Formats, path: &str) -> bool {
128 formats.claims::<K>(path)
129 }
130
131 fn read(
132 &self,
133 formats: &Formats,
134 file: &AssetPath,
135 bytes: &[u8],
136 cx: &DecodeCx<'_>,
137 print: bool,
138 ) -> Result<Read, Vec<ContentError>> {
139 let asset = formats.decode::<K>(file, bytes, cx)?;
140 let (text, ours) = match (print, self.text) {
141 (false, _) => (None, true),
142 (true, Some(write)) => (Some(write(&asset)), true),
143 (true, None) => (Some(format!("{asset:#?}")), false),
144 };
145 Ok(((self.summary)(&asset), text, ours))
146 }
147}
148
149pub struct Importers {
151 pub formats: Formats,
153 kinds: Vec<Box<dyn ImportKind>>,
154}
155
156impl Debug for Importers {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.debug_struct("Importers")
159 .field("formats", &self.formats)
160 .field("kinds", &self.kinds())
161 .finish()
162 }
163}
164
165fn count(n: usize, one: &str, many: &str) -> String {
167 if n == 1 {
168 format!("1 {one}")
169 } else {
170 format!("{n} {many}")
171 }
172}
173
174impl Importers {
175 pub fn new(formats: Formats) -> Self {
177 Self {
178 formats,
179 kinds: Vec::new(),
180 }
181 }
182
183 pub fn standard() -> Self {
186 Self::new(Formats::standard())
187 .kind::<ImageInfo>(|i| format!("{}×{} pixels", i.width, i.height))
188 .kind::<Sound>(|s| format!("{} channel(s) at {} Hz", s.channels, s.sample_rate))
189 .kind::<MusicTrack>(|m| {
190 format!(
191 "{} channel(s) at {} Hz, {} ms long",
192 m.channels, m.sample_rate, m.length_ms
193 )
194 })
195 .kind::<AudioManifest>(|a| {
196 format!(
197 "{}, {}",
198 count(a.sounds.len(), "sound", "sounds"),
199 count(a.music.len(), "music track", "music tracks")
200 )
201 })
202 .kind::<CreditsFile>(|c| count(c.credits.len(), "credit", "credits"))
203 .kind::<FontAtlasDef>(|f| {
204 format!(
205 "{} of {}×{} pixels",
206 count(f.glyphs.len(), "glyph", "glyphs"),
207 f.cell_w,
208 f.cell_h
209 )
210 })
211 .kind::<PaletteDef>(|p| count(p.colors.len(), "color", "colors"))
212 .kind::<Lighting>(|l| count(l.lights.len(), "light", "lights"))
213 .kind::<Names>(|n| count(n.names.len(), "name", "names"))
214 .kind::<DialogueFile<NoDirective>>(|d| {
215 let ids: Vec<&str> = d.scenes.iter().map(|s| s.scene.id.as_str()).collect();
216 format!(
217 "{}: {}",
218 count(ids.len(), "scene", "scenes"),
219 ids.join(", ")
220 )
221 })
222 }
223
224 #[must_use]
227 pub fn kind<K: AssetKind + Debug>(mut self, summary: fn(&K) -> String) -> Self {
228 self.kinds.push(Box::new(KindOf::<K> {
229 summary,
230 text: None,
231 }));
232 self
233 }
234
235 #[must_use]
238 pub fn kind_with_text<K: AssetKind + Debug>(
239 mut self,
240 summary: fn(&K) -> String,
241 text: fn(&K) -> String,
242 ) -> Self {
243 self.kinds.push(Box::new(KindOf::<K> {
244 summary,
245 text: Some(text),
246 }));
247 self
248 }
249
250 pub fn kinds(&self) -> Vec<&'static str> {
252 self.kinds.iter().map(|k| k.name()).collect()
253 }
254}
255
256fn folder_bundle(dir: &Path, max_files: usize, max_bytes: usize) -> Result<Bundle, String> {
262 let mut files: BTreeMap<String, Vec<u8>> = BTreeMap::new();
263 let mut pending = vec![(dir.to_path_buf(), String::new())];
264 let mut bytes = 0;
265 while let Some((path, prefix)) = pending.pop() {
266 let entries = fs::read_dir(&path).map_err(|e| format!("{}: {e}", path.display()))?;
267 for entry in entries.flatten() {
268 let name = entry.file_name().to_string_lossy().into_owned();
269 let inner = if prefix.is_empty() {
270 name
271 } else {
272 format!("{prefix}/{name}")
273 };
274 let kind = entry.file_type().map_err(|e| e.to_string())?;
275 if kind.is_dir() {
276 pending.push((entry.path(), inner));
277 } else if kind.is_file() {
278 let data = fs::read(entry.path()).map_err(|e| format!("{inner}: {e}"))?;
279 bytes += data.len();
280 files.insert(inner, data);
281 }
282 if files.len() > max_files || bytes > max_bytes {
283 return Err(format!(
284 "its folder holds more than {max_files} files or {max_bytes} bytes"
285 ));
286 }
287 }
288 }
289 let layer = Layer::owned(String::new(), files).map_err(|e| e.to_string())?;
290 Ok(Bundle::new(vec![layer]))
291}
292
293pub fn import(importers: &Importers, root: &Path, input: &ImportInput) -> Answer<ImportOutput> {
295 let path = root.join(&input.file);
296 let bytes = match fs::read(&path) {
297 Ok(bytes) => bytes,
298 Err(e) => return Answer::failed(Op::Import, codes::FILE, format!("{}: {e}", input.file)),
299 };
300 let kinds: Vec<&dyn ImportKind> = importers
301 .kinds
302 .iter()
303 .map(|k| &**k)
304 .filter(|k| input.kind.as_deref().is_none_or(|kind| kind == k.name()))
305 .collect();
306 if kinds.is_empty() {
307 let known = importers.kinds().join(", ");
308 return Answer::failed(
309 Op::Import,
310 codes::WRONG_ARGUMENTS,
311 format!(
312 "no kind `{}`: the kinds are {known}",
313 input.kind.as_deref().unwrap_or_default()
314 ),
315 );
316 }
317 let mut limits = Vec::new();
318 let name = path
319 .file_name()
320 .map(|n| n.to_string_lossy().into_owned())
321 .unwrap_or_default();
322 let folder = path
323 .parent()
324 .map(|dir| folder_bundle(dir, MAX_FOLDER_FILES, MAX_FOLDER_BYTES));
325 let bundle = match folder {
326 Some(Ok(bundle)) => Some(bundle),
327 Some(Err(why)) => {
328 limits.push(format!("the files it names were not read: {why}"));
329 None
330 }
331 None => None,
332 };
333 let cx = bundle
334 .as_ref()
335 .map_or_else(DecodeCx::new, DecodeCx::in_bundle);
336 let file = AssetPath::new(name.clone()).shown_as(input.file.clone());
337 let mut read_as = Vec::new();
338 let mut errors = Vec::new();
339 let mut claimed = false;
340 for kind in kinds {
341 let claims = kind.claims(&importers.formats, &name);
342 claimed |= claims;
343 match kind.read(&importers.formats, &file, &bytes, &cx, input.print) {
344 Ok((summary, text, ours)) => {
345 if !ours {
346 limits.push(format!(
347 "no writer of our text format for the {} kind yet: it is printed as read, \
348 in Rust's debug form",
349 kind.name()
350 ));
351 }
352 read_as.push(Imported {
353 kind: kind.name().to_owned(),
354 summary: format!("{}: {summary}", kind.name()),
355 text,
356 });
357 }
358 Err(e) if claims => errors.extend(e.iter().map(Problem::from)),
359 Err(_) => {}
360 }
361 }
362 if !read_as.is_empty() {
363 errors.clear();
364 if read_as.len() > 1 {
365 limits.push(format!(
366 "it reads as {} kinds: name the one meant with --kind",
367 read_as.len()
368 ));
369 }
370 } else if !claimed {
371 let message = format!(
372 "no format of these kinds knows its bytes or claims its extension: {}",
373 importers.kinds().join(", ")
374 );
375 let error = ContentError::new(input.file.clone(), message)
376 .with_code(shuyi_assets::error::codes::NO_FORMAT);
377 errors.push(Problem::from(&error));
378 limits.push(
379 "the importers of Tiled, LDtk and Aseprite files come with tickets 1123 and 1124"
380 .to_owned(),
381 );
382 }
383 let ok = !read_as.is_empty();
384 let output = ImportOutput {
385 file: input.file.clone(),
386 read_as,
387 errors,
388 };
389 Answer::new(Op::Import, ok, output, limits)
390}
391
392pub fn parse_args(args: &[String]) -> Result<ImportInput, String> {
394 let mut input = ImportInput::default();
395 let mut file = None;
396 let mut iter = args.iter();
397 while let Some(arg) = iter.next() {
398 match arg.as_str() {
399 "--print" => input.print = true,
400 "--kind" => {
401 input.kind = Some(iter.next().ok_or("--kind needs a value")?.clone());
402 }
403 flag if flag.starts_with("--") => return Err(format!("unknown option: {flag}")),
404 path if file.is_none() => file = Some(path.to_owned()),
405 extra => return Err(format!("one <file> only, not also {extra}")),
406 }
407 }
408 input.file = file.ok_or("missing <file>")?;
409 Ok(input)
410}
411
412pub fn text(output: &ImportOutput) -> String {
414 let mut out = String::new();
415 for read in &output.read_as {
416 let _ = writeln!(out, "import: {} is {}", output.file, read.summary);
418 if let Some(text) = &read.text {
419 out.push_str(text);
420 if !text.ends_with('\n') {
421 out.push('\n');
422 }
423 }
424 }
425 if !output.errors.is_empty() {
426 let _ = writeln!(out, "import: {} error(s)", output.errors.len());
427 for error in &output.errors {
428 let _ = writeln!(out, " {error}");
429 }
430 out.push_str(&codes::help_lines(&output.errors));
431 }
432 out
433}
434
435#[cfg(test)]
436mod tests;