1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
//! Campaign files store multiple scenario files in one easily distributable chunk.
//!
//! genie-cpx can read and write campaign files using the Campaign and CampaignWriter structs,
//! respectively.

#![deny(future_incompatible)]
#![deny(nonstandard_style)]
#![deny(rust_2018_idioms)]
#![deny(unsafe_code)]
#![warn(missing_docs)]
#![warn(unused)]

use std::io::{Read, Seek, Write};

mod read;
mod write;

pub use read::{Campaign, ReadCampaignError};
pub use write::{CampaignWriter, WriteCampaignError};

/// Version identifier for the campaign file format.
///
/// The only existing version is `b"1.00"`.
pub type CPXVersion = [u8; 4];

/// Campaign header.
#[derive(Debug, Clone)]
pub(crate) struct CampaignHeader {
    /// File format version.
    pub(crate) version: CPXVersion,
    /// Name of the campaign.
    pub(crate) name: String,
    /// Amount of scenario files in this campaign.
    pub(crate) num_scenarios: usize,
}

impl CampaignHeader {
    pub(crate) fn new(name: &str) -> Self {
        Self {
            version: *b"1.00",
            name: name.to_string(),
            num_scenarios: 0,
        }
    }
}

/// Data about a scenario in the campaign file.
#[derive(Debug, Clone)]
pub struct ScenarioMeta {
    /// Size in bytes of the scenario file.
    pub size: usize,
    pub(crate) offset: usize,
    /// Name of the scenario.
    pub name: String,
    /// File name of the scenario.
    pub filename: String,
}

impl<R> Campaign<R>
where
    R: Read + Seek,
{
    /// Write the scenario file to an output stream.
    pub fn write_to<W: Write>(&mut self, output: &mut W) -> Result<(), WriteCampaignError> {
        let mut writer = CampaignWriter::new(self.name(), output);

        for i in 0..self.len() {
            let bytes = self
                .by_index_raw(i)
                .map_err(|_| WriteCampaignError::NotFoundError(i))?;
            match (self.get_name(i), self.get_filename(i)) {
                (Some(name), Some(filename)) => {
                    writer.add_raw(name, filename, bytes);
                }
                _ => return Err(WriteCampaignError::NotFoundError(i)),
            }
        }

        let _output = writer.flush()?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::Cursor;

    #[test]
    fn rebuild_cpx() {
        let instream = File::open("./test/campaigns/Armies at War A Combat Showcase.cpn").unwrap();
        let mut outstream = vec![];
        let mut incpx = Campaign::from(instream).unwrap();
        incpx.write_to(&mut outstream).unwrap();

        let mut written_cpx = Campaign::from(Cursor::new(outstream)).unwrap();
        assert_eq!(written_cpx.name(), incpx.name());
        assert_eq!(written_cpx.len(), incpx.len());
        assert_eq!(
            written_cpx.by_index_raw(0).unwrap(),
            incpx.by_index_raw(0).unwrap()
        );
    }
}