caco 0.5.33

library for dealing with doom [et al] wad files
Documentation
use crate::lump::DoomLump;
use crate::agnostic::{FlatClod, FlatClodMut};
use crate::error::Error as WadError;

use std::{fs::File, io::{Read, Seek, Write}};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use anyhow::{Error, Result};

/// The "flavor" of a wad file, being either an IWAD intended to hold main game files
/// or a PWAD intended to hold modded/additional content.
/// 
/// As far as I know, literally nothing within the Doom engine ecosystem looks at or
/// even glances in the general direction of this value. But it would be good to have
/// here for completeness I suppose. I don't know. I'm so tired man.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DoomWadFlavor {
    /// A wad intended to function as the game's main resources.
    InternalWad,

    /// A wad intended to function as an addon or patch.
    PatchWad,
}

/// A wad file. Contains game data for games in the doom engine family.
#[derive(Clone, Debug)]
pub struct DoomWad {
    /// The intended function of the wad.
    /// (See [`DoomWadFlavor`] for more details.)
    pub flavor: DoomWadFlavor,

    /// The series of lumps (named blocks of binary data) contained
    /// in the file.
    pub lumps: Vec<DoomLump>,
}

impl DoomWad {
    /// Create a new, empty iwad.
    pub fn blank_i() -> Self {
        Self { flavor: DoomWadFlavor::InternalWad, lumps: vec!() }
    }

    /// Create a new, empty pwad.
    pub fn blank_p() -> Self {
        Self { flavor: DoomWadFlavor::PatchWad, lumps: vec!() }
    }

    /// Create a new wad of the given flavor.
    pub fn blank(flavor: DoomWadFlavor) -> Self {
        Self { flavor, lumps: vec!() }
    }

    /// Try to load and parse a wad file.
    /// 
    /// # Example
    /// ```no_run
    /// use std::fs::File;
    /// use caco::wad::Wad;
    /// 
    /// // open the file
    /// let mut f = File::open("my_wad.wad").unwrap();
    /// 
    /// // parse it into a wad file
    /// let wad = Wad::load(&mut f).unwrap();
    /// ```
    pub fn load(f: &mut File) -> Result<DoomWad> {
        f.seek(std::io::SeekFrom::Start(0))?;

        let mut lumps = vec!();
        
        let mut wad_sig = [0u8; 4];
        f.read_exact(&mut wad_sig)?;

        let flavor = if wad_sig == *b"IWAD" {
            DoomWadFlavor::InternalWad
        } else if wad_sig == *b"PWAD" {
            DoomWadFlavor::PatchWad
        } else { return Err(Error::new(WadError::IncorrectMagicInHeader)); };

        let lump_count = f.read_u32::<LittleEndian>()? as u64;
        let directory_location = f.read_u32::<LittleEndian>()? as u64;

        for n in 0..lump_count {
            f.seek(std::io::SeekFrom::Start(directory_location + 16 * n))?;

            let filepos = f.read_u32::<LittleEndian>()? as u64;
            let filesize = f.read_u32::<LittleEndian>()? as usize;
            
            let mut lumpname = "".to_owned();
            let mut null_seen = false;

            for _ in 0..8 {
                let nextchar = f.read_u8()? as char;
                if nextchar == '\0' { null_seen = true; }
                if !null_seen { lumpname.push(nextchar); }
            }

            f.seek(std::io::SeekFrom::Start(filepos))?;
            let mut filebuf = vec![0; filesize];
            f.read_exact(&mut filebuf)?;
            
            lumps.push(DoomLump {
                name: lumpname,
                data: filebuf.into_boxed_slice(),
            })
        }

        Ok(Self { flavor, lumps })
    }

    /// Save the wad out to a file on disk.
    /// 
    /// If a wad is loaded from a file and then saved back, the result may not be identical even if
    /// no changes were made to lumps in the wad. This is because the save method will always insert
    /// the lump dictionary at the end of the wad, with the lumps coming in the middle. Some older
    /// wads also contain padding between entry data that this crate will not accurately replicate.
    /// These differences shouldn't make a difference in how other programs in the Doom ecosystem
    /// interpret the file.
    /// 
    /// # Example
    /// ```no_run
    /// use std::fs::File;
    /// use caco::wad::Wad;
    /// 
    /// // a new blank (i)wad, no entries
    /// let wad = Wad::blank_i();
    /// 
    /// // lets save it to disk
    /// let mut f = File::create("my_new_iwad.wad").unwrap();
    /// wad.save(&mut f);
    /// ```
    pub fn save(&self, f: &mut File) -> Result<()> {
        f.seek(std::io::SeekFrom::Start(0))?;

        f.write(if self.flavor == DoomWadFlavor::InternalWad { b"IWAD" } else { b"PWAD" })?;
        f.write_u32::<LittleEndian>(self.lumps.len() as u32)?;

        f.seek(std::io::SeekFrom::Start(12))?;

        struct DictionaryEntry {
            name: String,
            size: u32,
            loc: u32,
        }

        let mut dict = vec!();

        for l in &self.lumps {
            dict.push(DictionaryEntry {
                name: l.name.clone(),
                size: l.data.len() as u32,
                loc: f.stream_position()? as u32,
            });

            f.write(&l.data)?;
        }

        let dictpos = f.stream_position()?;

        for d in dict {
            f.write_u32::<LittleEndian>(d.loc)?;
            f.write_u32::<LittleEndian>(d.size)?;
            
            let mut lname = d.name.clone();
            lname.truncate(8);
            while lname.len() < 8 { lname.push('\0'); }
            f.write(lname.as_bytes())?;
        }

        f.seek(std::io::SeekFrom::Start(8))?;
        f.write_u32::<LittleEndian>(dictpos as u32)?;

        Ok(())
    }
}

impl FlatClod<DoomLump> for DoomWad {
    fn clump_amt(&self) -> usize {
        self.lumps.len()
    }

    fn nth_clump(&self, n: usize) -> &DoomLump {
        &self.lumps[n]
    }

    fn a_sub_nth_clumps(&self, a: impl Iterator<Item = usize>) -> Vec<&DoomLump> {
        let a: Vec<usize> = a.collect();

        (&self.lumps[..])
            .iter()
            .enumerate()
            .filter(|(k, _)| a.contains(k))
            .map(|(_, l)| l)
            .collect()
    }
}

impl FlatClodMut<DoomLump> for DoomWad {
    fn nth_clump_mut(&mut self, n: usize) -> &mut DoomLump {
        &mut self.lumps[n]
    }

    fn a_sub_nth_clumps_mut(&mut self, a: impl Iterator<Item = usize>) -> Vec<&mut DoomLump> {
        let a: Vec<usize> = a.collect();
        (&mut self.lumps[..])
            .iter_mut()
            .enumerate()
            .filter(|(k, _)| a.contains(k))
            .map(|(_, l)| l)
            .collect()
    }

    fn insert_clump(&mut self, l: DoomLump, n: usize) {
        self.lumps.insert(n, l);
    }

    fn remove_clump(&mut self, n: usize) -> DoomLump {
        self.lumps.remove(n)
    }
}