Skip to main content

shuyi_assets/pack/
bake.rs

1//! Baking: which assets a game bakes ([`Bake`]), the pack made of its
2//! bundle ([`Bake::bake`]), and the proof that every asset read from the
3//! pack equals the one read from its source ([`Bake::check`]). An asset
4//! is baked in the pack's binary form, or in a format of its own
5//! ([`Bake::under_as`]): a one-bit target's pictures, as one-bit picture
6//! files.
7use crate::prelude::*;
8
9use alloc::collections::{BTreeMap, BTreeSet};
10use core::any::TypeId;
11use core::fmt;
12use core::marker::PhantomData;
13
14use serde::Serialize;
15use serde::de::DeserializeOwned;
16
17use super::{Content, PackInfo, Packed, write};
18use crate::bundle::Bundle;
19use crate::error::{ContentError, codes};
20use crate::format::{AssetKind, Encode, Formats, RegisterError};
21
22/// A kind that can be baked: it has a binary form (serde, both ways), and
23/// two of it can be compared, for [`Bake::check`].
24pub trait Bakeable: AssetKind + Serialize + DeserializeOwned + PartialEq + fmt::Debug {}
25
26impl<K: AssetKind + Serialize + DeserializeOwned + PartialEq + fmt::Debug> Bakeable for K {}
27
28/// Which files of a kind a [`Bake`] bakes.
29#[derive(Debug, Clone, PartialEq, Eq)]
30enum Which {
31    /// The asset with this id ([`Formats::find`]).
32    Id(String),
33    /// Every file under this directory that a format of the kind claims
34    /// ([`Formats::load_under`]).
35    Under(String),
36}
37
38/// A kind of a [`Bake`], whatever its type.
39trait BakeKind: Send + Sync {
40    /// The kind's name.
41    fn name(&self) -> &'static str;
42
43    /// The kind's type.
44    fn type_id(&self) -> TypeId;
45
46    /// The paths of the files `which` names in `bundle`.
47    fn paths(
48        &self,
49        formats: &Formats,
50        bundle: &Bundle,
51        which: &Which,
52    ) -> Result<Vec<String>, Vec<ContentError>>;
53
54    /// The binary form of the asset read from the file at `path`.
55    fn encode(
56        &self,
57        formats: &Formats,
58        bundle: &Bundle,
59        path: &str,
60    ) -> Result<Vec<u8>, Vec<ContentError>>;
61
62    /// Whether the asset at `path` reads the same from `pack` as from
63    /// `source`; the error that says how it doesn't otherwise.
64    fn same(
65        &self,
66        formats: &Formats,
67        source: &Bundle,
68        pack: &Bundle,
69        path: &str,
70    ) -> Result<(), Vec<ContentError>>;
71
72    /// Registers the kind's [`Packed`] format, unless it is there.
73    fn register(&self, formats: &mut Formats) -> Result<(), RegisterError>;
74
75    /// Reads the asset at `path` of `bundle`, and drops it.
76    fn load(&self, formats: &Formats, bundle: &Bundle, path: &str)
77    -> Result<(), Vec<ContentError>>;
78
79    /// Whether [`encode`](Self::encode) gives the file's bytes in a format
80    /// of the kind ([`Bake::under_as`]), not the pack's binary form.
81    fn own_format(&self) -> bool {
82        false
83    }
84}
85
86/// The paths of the files of kind `K` that `which` names in `bundle`.
87fn paths_of<K: AssetKind>(
88    formats: &Formats,
89    bundle: &Bundle,
90    which: &Which,
91) -> Result<Vec<String>, Vec<ContentError>> {
92    match which {
93        Which::Id(id) => formats
94            .find::<K>(bundle, id)
95            .map(|path| vec![path])
96            .map_err(|e| vec![e]),
97        Which::Under(dir) => Ok(bundle
98            .files_under(dir)
99            .into_iter()
100            .filter(|path| formats.claims::<K>(path))
101            .map(ToOwned::to_owned)
102            .collect()),
103    }
104}
105
106/// Whether the asset of kind `K` at `path` reads the same from `pack` as
107/// from `source` with `formats`, once a format `id` reads it from the
108/// pack ([`BakeKind::same`]).
109fn same_in<K: AssetKind + PartialEq>(
110    formats: &Formats,
111    (source, pack): (&Bundle, &Bundle),
112    path: &str,
113    id: &str,
114) -> Result<(), Vec<ContentError>> {
115    let shown = pack.display_path(path);
116    if !formats.ids::<K>().contains(&id) {
117        let message = format!(
118            "no format reads a baked {}: register the pack's formats (Bake::register)",
119            K::NAME
120        );
121        return Err(vec![ContentError::new(shown, message)]);
122    }
123    let from_source = formats.load::<K>(source, path)?;
124    let from_pack = formats.load::<K>(pack, path)?;
125    if from_source == from_pack {
126        return Ok(());
127    }
128    let message = format!(
129        "the {} read from the pack differs from the one read from its source",
130        K::NAME
131    );
132    Err(vec![
133        ContentError::new(shown, message).with_code(codes::PACK_DIFFERS),
134    ])
135}
136
137/// Kind `K`.
138struct KindOf<K>(PhantomData<fn() -> K>);
139
140impl<K: Bakeable> BakeKind for KindOf<K> {
141    fn name(&self) -> &'static str {
142        K::NAME
143    }
144
145    fn type_id(&self) -> TypeId {
146        TypeId::of::<K>()
147    }
148
149    fn paths(
150        &self,
151        formats: &Formats,
152        bundle: &Bundle,
153        which: &Which,
154    ) -> Result<Vec<String>, Vec<ContentError>> {
155        paths_of::<K>(formats, bundle, which)
156    }
157
158    fn encode(
159        &self,
160        formats: &Formats,
161        bundle: &Bundle,
162        path: &str,
163    ) -> Result<Vec<u8>, Vec<ContentError>> {
164        let asset = formats.load::<K>(bundle, path)?;
165        postcard::to_allocvec(&asset).map_err(|e| {
166            let message = format!("its {} has no binary form: {e}", K::NAME);
167            vec![ContentError::new(bundle.display_path(path), message)]
168        })
169    }
170
171    fn same(
172        &self,
173        formats: &Formats,
174        source: &Bundle,
175        pack: &Bundle,
176        path: &str,
177    ) -> Result<(), Vec<ContentError>> {
178        same_in::<K>(formats, (source, pack), path, Packed::<K>::ID)
179    }
180
181    fn register(&self, formats: &mut Formats) -> Result<(), RegisterError> {
182        if formats.ids::<K>().contains(&Packed::<K>::ID) {
183            return Ok(());
184        }
185        formats.register(Packed::<K>::new())
186    }
187
188    fn load(
189        &self,
190        formats: &Formats,
191        bundle: &Bundle,
192        path: &str,
193    ) -> Result<(), Vec<ContentError>> {
194        formats.load::<K>(bundle, path).map(drop)
195    }
196}
197
198/// The assets of format `F`'s kind, read with a bake's formats and kept
199/// in the pack in `F`'s own bytes ([`Bake::under_as`]).
200struct InFormat<F>(F);
201
202impl<F> BakeKind for InFormat<F>
203where
204    F: Encode + Clone,
205    F::Asset: PartialEq,
206{
207    fn name(&self) -> &'static str {
208        <F::Asset as AssetKind>::NAME
209    }
210
211    fn type_id(&self) -> TypeId {
212        TypeId::of::<F::Asset>()
213    }
214
215    fn paths(
216        &self,
217        formats: &Formats,
218        bundle: &Bundle,
219        which: &Which,
220    ) -> Result<Vec<String>, Vec<ContentError>> {
221        paths_of::<F::Asset>(formats, bundle, which)
222    }
223
224    fn encode(
225        &self,
226        formats: &Formats,
227        bundle: &Bundle,
228        path: &str,
229    ) -> Result<Vec<u8>, Vec<ContentError>> {
230        let asset = formats.load::<F::Asset>(bundle, path)?;
231        Ok(self.0.encode(&asset))
232    }
233
234    fn same(
235        &self,
236        formats: &Formats,
237        source: &Bundle,
238        pack: &Bundle,
239        path: &str,
240    ) -> Result<(), Vec<ContentError>> {
241        same_in::<F::Asset>(formats, (source, pack), path, self.0.id())
242    }
243
244    fn register(&self, formats: &mut Formats) -> Result<(), RegisterError> {
245        if formats.ids::<F::Asset>().contains(&self.0.id()) {
246            return Ok(());
247        }
248        formats.register(self.0.clone())
249    }
250
251    fn load(
252        &self,
253        formats: &Formats,
254        bundle: &Bundle,
255        path: &str,
256    ) -> Result<(), Vec<ContentError>> {
257        formats.load::<F::Asset>(bundle, path).map(drop)
258    }
259
260    fn own_format(&self) -> bool {
261        true
262    }
263}
264
265/// What a game bakes into a pack: which assets of which kinds, the same
266/// way its loaders ask for them ([`asset`](Self::asset) as
267/// [`Formats::load`], [`under`](Self::under) as [`Formats::load_under`]),
268/// and its stamp. Every other file of the bundle goes into the pack as its
269/// own bytes.
270///
271/// A file whose bytes something other than its kind's format reads (a
272/// picture the platform draws, a sound it plays, a file another format
273/// reads beside its own) stays as it is, or is baked in a format that
274/// reader knows ([`under_as`](Self::under_as)): bake only kinds whose
275/// files nothing else reads.
276pub struct Bake {
277    stamp: String,
278    plan: Vec<(Box<dyn BakeKind>, Which)>,
279}
280
281impl fmt::Debug for Bake {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        let plan: Vec<(&str, &Which)> = self
284            .plan
285            .iter()
286            .map(|(kind, which)| (kind.name(), which))
287            .collect();
288        f.debug_struct("Bake")
289            .field("stamp", &self.stamp)
290            .field("plan", &plan)
291            .finish()
292    }
293}
294
295/// What [`Bake::bake`] made: the pack, and what went into it.
296#[derive(Debug, Clone, PartialEq, Eq)]
297pub struct Baked {
298    /// The pack's bytes.
299    pub bytes: Vec<u8>,
300    /// Every file in it, in path order.
301    pub files: Vec<BakedFile>,
302}
303
304/// One file of a [`Baked`] pack.
305#[derive(Debug, Clone, PartialEq, Eq)]
306#[non_exhaustive]
307pub struct BakedFile {
308    /// Its path.
309    pub path: String,
310    /// Its source as messages show it ([`Bundle::display_path`]), under
311    /// the layer it was read from (`assets@playdate/hero.png`).
312    pub shown: String,
313    /// The kinds it was baked as; none if it went in as its own bytes.
314    pub kinds: Vec<&'static str>,
315    /// Its source's bytes.
316    pub source: usize,
317}
318
319impl Baked {
320    /// The bytes of every file's source, added up.
321    pub fn source_bytes(&self) -> usize {
322        self.files.iter().map(|file| file.source).sum()
323    }
324}
325
326impl Bake {
327    /// A bake of nothing yet, with `stamp`: the game's word for the shapes
328    /// of its own kinds (its version, say), which [`Pack::open`] then asks
329    /// for.
330    ///
331    /// [`Pack::open`]: super::Pack::open
332    pub fn new(stamp: impl Into<String>) -> Self {
333        Self {
334            stamp: stamp.into(),
335            plan: Vec::new(),
336        }
337    }
338
339    /// The stamp.
340    pub fn stamp(&self) -> &str {
341        &self.stamp
342    }
343
344    /// This bake, baking the asset of kind `K` with id `id` (`data/palette`
345    /// or `data/palette.ron`).
346    #[must_use]
347    pub fn asset<K: Bakeable>(mut self, id: &str) -> Self {
348        self.plan
349            .push((Box::new(KindOf::<K>(PhantomData)), Which::Id(id.to_owned())));
350        self
351    }
352
353    /// This bake, baking every file of kind `K` under directory `dir` (`""`
354    /// for the whole bundle) whose extension a format of the kind claims.
355    #[must_use]
356    pub fn under<K: Bakeable>(mut self, dir: &str) -> Self {
357        self.plan.push((
358            Box::new(KindOf::<K>(PhantomData)),
359            Which::Under(dir.to_owned()),
360        ));
361        self
362    }
363
364    /// This bake, baking every file of `format`'s kind under directory
365    /// `dir` (`""` for the whole bundle) whose extension a format of the
366    /// kind claims, read with the bake's formats, and kept in the pack in
367    /// `format`'s own bytes ([`Encode`]) instead of the pack's binary
368    /// form: an optimized format derived from its source (ADR-0007 ยง4),
369    /// which whatever reads the file on the target reads as it is. A
370    /// one-bit target's pictures go in as one-bit picture files so, each
371    /// at its PNG's path:
372    ///
373    /// ```
374    /// use shuyi_assets::bitmap::OneBitFile;
375    /// use shuyi_assets::pack::Bake;
376    ///
377    /// let bake = Bake::new("my-game 1").under_as("", OneBitFile);
378    /// assert_eq!(bake.kinds(), ["one-bit picture"]);
379    /// ```
380    ///
381    /// [`register`](Self::register) registers `format` where its kind
382    /// lacks it, and [`check`](Self::check) proves each such file of the
383    /// pack, read by it, equal to its source read with the bake's
384    /// formats. A file baked so is baked as no other kind.
385    #[must_use]
386    pub fn under_as<F>(mut self, dir: &str, format: F) -> Self
387    where
388        F: Encode + Clone,
389        F::Asset: PartialEq,
390    {
391        self.plan
392            .push((Box::new(InFormat(format)), Which::Under(dir.to_owned())));
393        self
394    }
395
396    /// The names of the kinds it bakes, each once, in the order first
397    /// named.
398    pub fn kinds(&self) -> Vec<&'static str> {
399        let mut kinds = Vec::new();
400        for (kind, _) in &self.plan {
401            if !kinds.contains(&kind.name()) {
402                kinds.push(kind.name());
403            }
404        }
405        kinds
406    }
407
408    /// Registers in `formats` the format of a baked file of every kind it
409    /// bakes ([`Packed`]), where it isn't already: what a game that loads
410    /// a pack does at start-up.
411    pub fn register(&self, formats: &mut Formats) -> Result<(), RegisterError> {
412        self.plan
413            .iter()
414            .try_for_each(|(kind, _)| kind.register(formats))
415    }
416
417    /// The baked files: each path with the kinds it is baked as (in the
418    /// order named), and every problem finding them.
419    fn planned(
420        &self,
421        formats: &Formats,
422        bundle: &Bundle,
423    ) -> (BTreeMap<String, Vec<&dyn BakeKind>>, Vec<ContentError>) {
424        let mut planned: BTreeMap<String, Vec<&dyn BakeKind>> = BTreeMap::new();
425        let mut errors = Vec::new();
426        let mut types: BTreeMap<&str, TypeId> = BTreeMap::new();
427        for (kind, which) in &self.plan {
428            let first = *types.entry(kind.name()).or_insert(kind.type_id());
429            if first != kind.type_id() {
430                let message = format!(
431                    "the bake has two kinds called \"{}\": a pack tells kinds by name",
432                    kind.name()
433                );
434                errors.push(ContentError::new("bake", message));
435                continue;
436            }
437            match kind.paths(formats, bundle, which) {
438                Ok(paths) => {
439                    for path in paths {
440                        let kinds = planned.entry(path).or_default();
441                        if !kinds.iter().any(|k| k.type_id() == kind.type_id()) {
442                            kinds.push(&**kind);
443                        }
444                    }
445                }
446                Err(e) => errors.extend(e),
447            }
448        }
449        for (path, kinds) in &planned {
450            if kinds.len() > 1 && kinds.iter().any(|kind| kind.own_format()) {
451                let names: Vec<&str> = kinds.iter().map(|kind| kind.name()).collect();
452                let message = format!(
453                    "the bake keeps it in a format of its own and bakes it as {}; a file kept \
454                     in its own format is baked as one kind",
455                    names.join(" and ")
456                );
457                errors.push(ContentError::new(bundle.display_path(path), message));
458            }
459        }
460        (planned, errors)
461    }
462
463    /// The pack of `bundle`, baked for `target`: each file this bake names
464    /// holds its assets read with `formats` (or, baked
465    /// [`under_as`](Self::under_as) a format, that format's bytes), every
466    /// other file its own bytes, and its index holds `bundle`'s fingerprint, which the pack
467    /// then stands for. Import settings files go in as their own bytes
468    /// too, once they check ([`Formats::check_import_settings`]). Refuses
469    /// a source with errors: every one of them is returned.
470    pub fn bake(
471        &self,
472        formats: &Formats,
473        bundle: &Bundle,
474        target: Option<&str>,
475    ) -> Result<Baked, Vec<ContentError>> {
476        let (planned, mut errors) = self.planned(formats, bundle);
477        errors.extend(formats.check_import_settings(bundle));
478        let mut contents = BTreeMap::new();
479        let mut files = Vec::new();
480        for path in bundle.files_under("") {
481            let source = bundle.bytes(path).unwrap_or_default();
482            let kinds = planned.get(path).map(Vec::as_slice).unwrap_or_default();
483            let content = if kinds.is_empty() {
484                Content::Raw(source.to_vec())
485            } else if let [kind] = kinds
486                && kind.own_format()
487            {
488                let bytes = kind.encode(formats, bundle, path);
489                Content::Raw(bytes.map_err(|e| errors.extend(e)).unwrap_or_default())
490            } else {
491                let mut sections = BTreeMap::new();
492                for kind in kinds {
493                    match kind.encode(formats, bundle, path) {
494                        Ok(data) => {
495                            sections.insert(kind.name(), data);
496                        }
497                        Err(e) => errors.extend(e),
498                    }
499                }
500                Content::Baked(sections)
501            };
502            files.push(BakedFile {
503                path: path.to_owned(),
504                shown: bundle.display_path(path),
505                kinds: kinds.iter().map(|kind| kind.name()).collect(),
506                source: source.len(),
507            });
508            contents.insert(path.to_owned(), content);
509        }
510        if !errors.is_empty() {
511            return Err(errors);
512        }
513        let info = PackInfo {
514            stamp: self.stamp.clone(),
515            target: target.map(ToOwned::to_owned),
516            content: bundle.fingerprint(),
517        };
518        Ok(Baked {
519            bytes: write(&info, &contents),
520            files,
521        })
522    }
523
524    /// Reads every asset this bake names from `bundle` with `formats`, as
525    /// the game's loaders would, and drops them: what `shuyi bake` times,
526    /// from the sources and from the pack. Every error found.
527    pub fn load(&self, formats: &Formats, bundle: &Bundle) -> Vec<ContentError> {
528        let (planned, mut errors) = self.planned(formats, bundle);
529        for (path, kinds) in &planned {
530            for kind in kinds {
531                if let Err(e) = kind.load(formats, bundle, path) {
532                    errors.extend(e);
533                }
534            }
535        }
536        errors
537    }
538
539    /// Every way `pack` (a bundle of a baked pack) differs from `source`,
540    /// the bundle it was baked from: an asset this bake names that reads
541    /// differently from the two with `formats` (which must have the pack's
542    /// formats: [`register`](Self::register)), a file the pack lacks or
543    /// adds, or one kept as its own bytes that changed. None if the pack
544    /// is true to its sources.
545    pub fn check(&self, formats: &Formats, source: &Bundle, pack: &Bundle) -> Vec<ContentError> {
546        let (planned, mut errors) = self.planned(formats, source);
547        let sources = source.files_under("");
548        for path in &sources {
549            let shown = pack.display_path(path);
550            let Some(bytes) = pack.bytes(path) else {
551                errors.push(
552                    ContentError::new(shown, "the pack lacks this file")
553                        .with_code(codes::PACK_DIFFERS),
554                );
555                continue;
556            };
557            match planned.get(*path) {
558                Some(kinds) => {
559                    for kind in kinds {
560                        if let Err(e) = kind.same(formats, source, pack, path) {
561                            errors.extend(e);
562                        }
563                    }
564                }
565                None if Some(bytes) != source.bytes(path) => {
566                    errors.push(
567                        ContentError::new(shown, "the pack's copy differs from the file")
568                            .with_code(codes::PACK_DIFFERS),
569                    );
570                }
571                None => {}
572            }
573        }
574        let sources: BTreeSet<&str> = sources.into_iter().collect();
575        for path in pack.files_under("") {
576            if !sources.contains(path) {
577                let message = "the pack has this file and the sources don't";
578                errors.push(
579                    ContentError::new(pack.display_path(path), message)
580                        .with_code(codes::PACK_DIFFERS),
581                );
582            }
583        }
584        errors
585    }
586}