ra2_mix/xcc_package/
writer.rs

1//! Writer module for RA2 MIX files
2
3use super::*;
4use std::{collections::VecDeque, fs::create_dir_all, path::PathBuf};
5
6impl MixPackage {
7    /// # Arguments
8    ///
9    /// * `output`:
10    ///
11    /// # Examples
12    ///
13    /// ```
14    /// ```
15    pub fn save(self, output: &Path) -> Result<usize, Ra2Error> {
16        if !output.is_file() {
17            return Err(Ra2Error::FileNotFound("must be a file".to_string()));
18        }
19        let data = self.encode()?;
20        std::fs::write(output, &data)?;
21        Ok(data.len())
22    }
23    pub fn dump(self, output: &Path) -> Result<usize, Ra2Error> {
24        if !output.exists() {
25            println!("Skipping file {}", output.display());
26            create_dir_all(output)?;
27        }
28        if !output.is_dir() {
29            return Err(Ra2Error::FileNotFound("file exists but not folder".to_string()));
30        }
31        unsafe { self.dump_unchecked(output) }
32    }
33    pub unsafe fn dump_unchecked(self, output: &Path) -> Result<usize, Ra2Error> {
34        for (filename, data) in self.files.iter() {
35            let mut file = File::create(output.join(&filename))?;
36            file.write_all(&data)?;
37        }
38        Ok(self.files.len())
39    }
40    /// # Arguments
41    ///
42    /// * `output`:
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// ```
48    pub fn encode(self) -> Result<Vec<u8>, Ra2Error> {
49        let file_map = coalesce_input_files(self.game, &self.files)?;
50
51        // Create file information list
52        let mut file_information_list: Vec<FileInfo> =
53            file_map.iter().map(|(filename, data)| FileInfo { file_id: ra2_crc(filename), data: data.clone() }).collect();
54
55        // Sort by file ID
56        file_information_list.sort_by_key(|file_info| file_info.file_id);
57
58        // Generate file entries and body data
59        let mut offset = 0u32;
60        let mut file_entry_data = Vec::new();
61        let mut body_data = Vec::new();
62
63        for file_info in &file_information_list {
64            let size = file_info.data.len() as u32;
65
66            // Write file entry
67            file_entry_data.write_u32::<LittleEndian>(file_info.file_id)?;
68            file_entry_data.write_u32::<LittleEndian>(offset)?;
69            file_entry_data.write_u32::<LittleEndian>(size)?;
70
71            // Write file data
72            body_data.extend_from_slice(&file_info.data);
73
74            offset += size;
75        }
76
77        // Combine all parts
78        let mut mix_data = create_mix_header(&file_map)?;
79        mix_data.extend_from_slice(&file_entry_data);
80        mix_data.extend_from_slice(&body_data);
81
82        Ok(mix_data)
83    }
84}
85
86/// Creates MIX database data
87fn get_mix_db_data(filenames: &[String], game: CncGame) -> Vec<u8> {
88    let num_files = filenames.len();
89    let db_size_in_bytes = XCC_HEADER_SIZE + filenames.iter().map(|filename| filename.len() + 1).sum::<usize>();
90
91    let mut bytes_data = Vec::with_capacity(db_size_in_bytes);
92
93    // Write XCC ID bytes
94    bytes_data.extend_from_slice(XCC_ID_BYTES);
95    // Pad to 32 bytes
96    bytes_data.resize(32, 0);
97
98    // Write header fields
99    bytes_data.write_u32::<LittleEndian>(db_size_in_bytes as u32).unwrap();
100    bytes_data.write_u32::<LittleEndian>(XCC_FILE_TYPE).unwrap();
101    bytes_data.write_u32::<LittleEndian>(XCC_FILE_VERSION).unwrap();
102    bytes_data.write_u32::<LittleEndian>(game as u32).unwrap();
103    bytes_data.write_u32::<LittleEndian>(num_files as u32).unwrap();
104
105    // Write filenames with null terminators
106    for filename in filenames {
107        bytes_data.extend_from_slice(filename.as_bytes());
108        bytes_data.push(0); // Null terminator
109    }
110
111    bytes_data
112}
113
114/// Processes input files and creates a file map
115pub fn coalesce_input_files(game: CncGame, file_map: &HashMap<String, Vec<u8>>) -> Result<HashMap<String, Vec<u8>>, Ra2Error> {
116    let mut extra_file_map = file_map.clone();
117    // Get filenames and create mix database
118    let mut filenames: Vec<String> = extra_file_map.keys().cloned().collect();
119    filenames.push(MIX_DB_FILENAME.to_string());
120    // Sort filenames
121    let db_data = get_mix_db_data(&filenames, game);
122    extra_file_map.insert(MIX_DB_FILENAME.to_string(), db_data);
123    Ok(extra_file_map)
124}
125
126/// Creates a MIX file header
127fn create_mix_header(file_map: &HashMap<String, Vec<u8>>) -> Result<Vec<u8>, Ra2Error> {
128    let flags = 0u32;
129    let file_count = file_map.len() as u16;
130    let data_size = file_map.values().map(|data| data.len() as u32).sum();
131
132    let mut header = Vec::with_capacity(HEADER_SIZE);
133    header.write_u32::<LittleEndian>(flags)?;
134    header.write_u16::<LittleEndian>(file_count)?;
135    header.write_u32::<LittleEndian>(data_size)?;
136
137    Ok(header)
138}
139
140/// Recursively unpack all mix files in the game directory
141/// 
142/// # Arguments 
143/// 
144/// * `folder`: The game directory path
145/// * `db`: The global mix database
146///
147/// # Examples 
148/// 
149/// ```no_run
150/// # use std::path::Path;
151/// # use ra2_mix::{decompress, MixDatabase, Ra2Error};
152/// fn main() -> Result<(), Ra2Error> {
153///     let db = MixDatabase::load(Path::new("C:\\Program Files (x86)\\XCC\\Utilities\\global mix database.dat"))?;
154///     decompress(Path::new("C:\\Red Alert 2 - Yuri's Revenge"), &db)?;
155///     Ok(())
156/// }
157/// ```
158pub fn decompress(folder: &Path, db: &MixDatabase) -> Result<(), Ra2Error> {
159    if !folder.exists() {
160        return Err(Ra2Error::FileNotFound("file not found".to_string()));
161    }
162    if !folder.is_dir() {
163        return Err(Ra2Error::FileNotFound("file exists but not folder".to_string()));
164    }
165    let mut queue = VecDeque::new();
166    task_append(&mut queue, folder)?;
167    while let Some(path) = queue.pop_front() {
168        let mix = MixPackage::load(&path, db)?;
169        let sub_folder = path.with_extension("");
170        create_dir_all(&sub_folder)?;
171        unsafe { mix.dump_unchecked(&sub_folder)? };
172        task_append(&mut queue, &sub_folder)?;
173    }
174    Ok(())
175}
176
177fn task_append(paths: &mut VecDeque<PathBuf>, folder: &Path) -> Result<(), Ra2Error> {
178    for entry in folder.read_dir()? {
179        let entry = entry?;
180        let path = entry.path();
181        match path.extension() {
182            Some(s) if s.eq("mix") => {
183                println!("Found mix file: {}", path.display());
184                paths.push_back(path);
185            }
186            _ => {}
187        }
188    }
189    Ok(())
190}