pak 0.7.1

An easy-to-use data pak format for games.
Documentation
use {
    super::{
        Canonicalize, Writer,
        bitmap::{BitmapAsset, BitmapSwizzle},
        file_key, re_run_if_changed,
    },
    crate::{
        BitmapFontId, BlobId,
        bitmap::{Bitmap, BitmapColor, BitmapFormat},
        bitmap_font::BitmapFont,
    },
    bmfont::{BMFont, OrdinateOrientation},
    log::info,
    parking_lot::Mutex,
    serde::Deserialize,
    std::{
        fs::File,
        fs::read_to_string,
        io::{Cursor, Read},
        path::{Path, PathBuf},
        sync::Arc,
    },
};

/// Holds a description of any generic file.
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq)]
pub struct BlobAsset {
    /// The file source.
    src: Option<PathBuf>,
}

impl BlobAsset {
    pub fn new(src: impl AsRef<Path>) -> Self {
        let src = src.as_ref().to_path_buf();

        Self { src: Some(src) }
    }

    /// Reads and processes arbitrary binary source files into an existing `.pak` file buffer.
    pub fn bake(
        &self,
        writer: &Arc<Mutex<Writer>>,
        project_dir: impl AsRef<Path>,
    ) -> anyhow::Result<BlobId> {
        let Some(src) = self.src() else {
            return Err(anyhow::Error::msg("unspecified bitmap source"));
        };

        let asset = self.clone().into();

        // Early-out if we have already baked this blob
        if let Some(id) = writer.lock().ctx.get(&asset) {
            return Ok(id.as_blob().unwrap());
        }

        let key = file_key(&project_dir, src);

        info!("Baking blob: {}", key);

        re_run_if_changed(src);

        let mut file = File::open(src).unwrap();
        let mut value = vec![];
        file.read_to_end(&mut value).unwrap();

        let mut writer = writer.lock();
        if let Some(id) = writer.ctx.get(&asset) {
            return Ok(id.as_blob().unwrap());
        }

        let id = writer.push_blob(value, Some(key));
        writer.ctx.insert(asset, id.into());

        Ok(id)
    }

    /// Reads and processes bitmapped font source files into an existing `.pak` file buffer.
    pub(super) fn bake_bitmap_font(
        &self,
        writer: &Arc<Mutex<Writer>>,
        project_dir: impl AsRef<Path>,
        path: impl AsRef<Path>,
    ) -> anyhow::Result<BitmapFontId> {
        let Some(src) = self.src() else {
            return Err(anyhow::Error::msg("unspecified bitmap source"));
        };

        let asset = self.clone().into();

        // Early-out if we have already baked this blob
        if let Some(id) = writer.lock().ctx.get(&asset) {
            return Ok(id.as_bitmap_font().unwrap());
        }

        let key = file_key(&project_dir, &path);

        info!("Baking bitmap font: {}", key);

        re_run_if_changed(src);

        // Get the fs objects for this asset
        let def_parent = src.parent().unwrap();
        let def_file = read_to_string(src).unwrap();
        let def = BMFont::new(Cursor::new(&def_file), OrdinateOrientation::TopToBottom).unwrap();
        let pages = def
            .pages()
            .flat_map(|page| {
                let path = def_parent.join(page);

                // Bake the pixels
                BitmapAsset::read_pixels(path, Some(BitmapSwizzle::RGBA), None)
            })
            .map(|(_, width, pixels)| {
                // TODO: Handle format correctly!
                let mut better_pixels = Vec::with_capacity(pixels.len());
                for y in 0..pixels.len() / 4 / width as usize {
                    for x in 0..width as usize {
                        let g = pixels[y * width as usize * 4 + x * 4 + 1];
                        let r = pixels[y * width as usize * 4 + x * 4 + 3];
                        if 0xff == r {
                            better_pixels.push(0xff);
                            better_pixels.push(0x00);
                        } else if 0xff == g {
                            better_pixels.push(0x00);
                            better_pixels.push(0xff);
                        } else {
                            better_pixels.push(0x00);
                            better_pixels.push(0x00);
                        }
                        better_pixels.push(0x00);
                    }
                }

                (width, better_pixels)
            })
            .collect::<Vec<_>>();

        // Panic if any page is a different size (the format says they should all be the same)
        let mut page_size = None;
        for (page_width, page_pixels) in &pages {
            let page_height = page_pixels.len() as u32 / 3 / page_width;
            if page_size.is_none() {
                page_size = Some((*page_width, page_height));
            } else if let Some((width, height)) = page_size
                && (*page_width != width || page_height != height)
            {
                panic!("Unexpected page size");
            }
        }

        let (width, _) = page_size.unwrap();

        let page_bufs = pages
            .into_iter()
            .map(|(_, pixels)| {
                Bitmap::new(BitmapColor::Linear, BitmapFormat::Rgb, width, 1, pixels)
            })
            .collect();

        let mut writer = writer.lock();
        if let Some(id) = writer.ctx.get(&asset) {
            return Ok(id.as_bitmap_font().unwrap());
        }

        let id = writer.push_bitmap_font(BitmapFont::new(def_file, page_bufs), Some(key));
        writer.ctx.insert(asset, id.into());

        Ok(id)
    }

    /// Sets the blob file source.
    pub fn set_src(&mut self, src: impl AsRef<Path>) {
        self.src = Some(src.as_ref().to_path_buf());
    }

    pub fn src(&self) -> Option<&Path> {
        self.src.as_deref()
    }
}

impl Canonicalize for BlobAsset {
    fn canonicalize(&mut self, project_dir: impl AsRef<Path>, src_dir: impl AsRef<Path>) {
        if let Some(src) = self.src() {
            self.src = Some(Self::canonicalize_project_path(project_dir, src_dir, src));
        }
    }
}