rpfm_lib/files/animpack/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2024 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! AnimPacks are a container-type file, that usually contains anim-related files,
12//! such as [Anims Tables](crate::files::anims_table::AnimsTable),
13//! [Anim Fragments](crate::files::anim_fragment_battle::AnimFragmentBattle) and
14//! [Matched Combat Tables](crate::files::matched_combat::MatchedCombat).
15//!
16//! Support is complete for all known versions of the file.
17
18use serde_derive::{Serialize, Deserialize};
19
20use std::collections::HashMap;
21use std::path::PathBuf;
22use std::str::FromStr;
23
24use crate::binary::{ReadBytes, WriteBytes};
25use crate::error::Result;
26use crate::files::*;
27
28/// Extension used by AnimPacks.
29pub const EXTENSION: &str = ".animpack";
30
31#[cfg(test)] mod animpack_test;
32
33//---------------------------------------------------------------------------//
34//                              Enum & Structs
35//---------------------------------------------------------------------------//
36
37/// This holds an entire AnimPack file decoded in memory.
38///
39/// AnimPacks are a container-type file, that usually contains anim-related files, such as Anim Tables,
40/// Anim Fragments and Matched Combat Tables.
41///
42/// It's usually found in the `anim` folder of the game, under the extension `.animpack`, hence their name.
43///
44/// # AnimPack Structure
45///
46/// | Bytes          | Type                         | Data                                    |
47/// | -------------- | ---------------------------- | --------------------------------------- |
48/// | 4              | [u32]                        | File Count.                             |
49/// | X * File Count | [File](#file-structure) List | List of files inside the AnimPack File. |
50///
51///
52/// # File Structure
53///
54/// | Bytes       | Type           | Data                  |
55/// | ----------- | -------------- | --------------------- |
56/// | *           | Sized StringU8 | File Path.            |
57/// | 4           | [u32]          | File Length in bytes. |
58/// | File Lenght | &\[[u8]\]      | File Data.            |
59///
60#[derive(PartialEq, Clone, Debug, Default, Serialize, Deserialize)]
61pub struct AnimPack {
62
63    /// File Path on disk of this AnimPack.
64    disk_file_path: String,
65
66    /// Offset of this file in the disk file. If the file is not inside another file, it's 0.
67    disk_file_offset: u64,
68
69    /// Timestamp of the file. Needed for detecting edits on disk outside our control, in case
70    /// we use LazyLoading.
71    local_timestamp: u64,
72
73    /// List of file paths lowercased, with their casing counterparts. To quickly find files.
74    paths: HashMap<String, Vec<String>>,
75
76    /// List of files within this AnimPack.
77    files: HashMap<String, RFile>,
78}
79
80//---------------------------------------------------------------------------//
81//                           Implementation of AnimPack
82//---------------------------------------------------------------------------//
83
84impl Container for AnimPack {
85
86    /// This function returns a reference to the path on disk of this AnimPack.
87    /// If the AnimPack is not yet a file on disk, you may put an empty string.
88    ///
89    /// Just remember to update it once you save the file to disk.
90    fn disk_file_path(&self) -> &str {
91       &self.disk_file_path
92    }
93
94    /// This function returns a reference to the files inside this AnimPack.
95    fn files(&self) -> &HashMap<String, RFile> {
96        &self.files
97    }
98
99    /// This function returns a mutable reference to the files inside this AnimPack.
100    fn files_mut(&mut self) -> &mut HashMap<String, RFile> {
101        &mut self.files
102    }
103
104    /// This function returns the offset of this AnimPack on the corresponding file on disk.
105    ///
106    /// If the AnimPack hasn't yet be saved to disk or it's not within another file, this returns 0.
107    fn disk_file_offset(&self) -> u64 {
108       self.disk_file_offset
109    }
110
111    fn paths_cache(&self) -> &HashMap<String, Vec<String>> {
112        &self.paths
113    }
114
115    fn paths_cache_mut(&mut self) -> &mut HashMap<String, Vec<String>> {
116        &mut self.paths
117    }
118
119    /// This method returns the `Last modified date` the filesystem reports for the container file, in seconds.
120    fn local_timestamp(&self) -> u64 {
121        self.local_timestamp
122    }
123}
124
125impl Decodeable for AnimPack {
126
127    /// This function allow us to decode something implementing [ReadBytes], like a [File]
128    /// or a [Vec]<[u8]> into an structured AnimPack.
129    ///
130    /// About [extra_data](crate::files::DecodeableExtraData), this decode function requires the following fields:
131    /// - `lazy_load`: If we want to use Lazy-Loading. If the files within this AnimPack are encrypted, this is ignored.
132    /// - `is_encrypted`: If this AnimPack's data is encrypted. If it is, `lazy_load` is ignored.
133    /// - `disk_file_path`: If provided, it must correspond to a valid file on disk.
134    /// - `disk_file_offset`: If the file is within another file, it's the offset where this AnimPack's data starts. If not, it should be 0.
135    /// - `disk_file_size`: The size of the data belonging to this AnimPack.
136    /// - `timestamp`: `Last modified date` of this AnimPack, in seconds. If the AnimPack is not a disk file, it should be 0.
137    ///
138    /// ```rust
139    ///use std::fs::File;
140    ///use std::io::{BufReader, BufWriter, Write};
141    ///
142    ///use rpfm_lib::binary::ReadBytes;
143    ///use rpfm_lib::files::{*, animpack::AnimPack};
144    ///use rpfm_lib::utils::last_modified_time_from_file;
145    ///
146    ///let path = "../test_files/test_decode.animpack";
147    ///let mut reader = BufReader::new(File::open(path).unwrap());
148    ///
149    ///let mut decodeable_extra_data = DecodeableExtraData::default();
150    ///decodeable_extra_data.set_disk_file_path(Some(path));
151    ///decodeable_extra_data.set_data_size(reader.len().unwrap());
152    ///decodeable_extra_data.set_timestamp(last_modified_time_from_file(reader.get_ref()).unwrap());
153    ///
154    ///let data = AnimPack::decode(&mut reader, &Some(decodeable_extra_data)).unwrap();
155    /// ```
156    fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
157        let extra_data = extra_data.as_ref().ok_or(RLibError::DecodingMissingExtraData)?;
158
159        // If we're reading from a file on disk, we require a valid path.
160        // If we're reading from a file on memory, we don't need a valid path.
161        let disk_file_path = match extra_data.disk_file_path {
162            Some(path) => {
163                let file_path = PathBuf::from_str(path).map_err(|_|RLibError::DecodingMissingExtraDataField("disk_file_path".to_owned()))?;
164                if file_path.is_file() {
165                    path.to_owned()
166                } else {
167                    return Err(RLibError::DecodingMissingExtraData)
168                }
169            }
170            None => String::new()
171        };
172
173        let disk_file_offset = extra_data.disk_file_offset;
174        let disk_file_size = extra_data.data_size;
175        let local_timestamp = extra_data.timestamp;
176        let is_encrypted = extra_data.is_encrypted;
177
178        // If we don't have a path, or the file is encrypted, we can't lazy-load.
179        let lazy_load = !disk_file_path.is_empty() && !is_encrypted && extra_data.lazy_load;
180        let file_count = data.read_u32()?;
181
182        let mut anim_pack = Self {
183            disk_file_path,
184            disk_file_offset,
185            local_timestamp,
186            paths: HashMap::new(),
187            files: if file_count < 50_000 { HashMap::with_capacity(file_count as usize) } else { HashMap::new() },
188        };
189
190        for _ in 0..file_count {
191            let path_in_container = data.read_sized_string_u8()?.replace('\\', "/");
192            let size = data.read_u32()?;
193
194            // Encrypted files cannot be lazy-loaded. They must be read in-place.
195            if !lazy_load || is_encrypted {
196                let data = data.read_slice(size as usize, false)?;
197                let file = RFile {
198                    path: path_in_container.to_owned(),
199                    timestamp: None,
200                    file_type: FileType::AnimPack,
201                    container_name: None,
202                    data: RFileInnerData::Cached(data),
203                };
204
205                anim_pack.files.insert(path_in_container, file);
206            }
207
208            // Unencrypted and files are not read, but lazy-loaded, unless specified otherwise.
209            else {
210                let data_pos = data.stream_position()? - disk_file_offset;
211                let file = RFile::new_from_container(&anim_pack, size as u64, false, None, data_pos, local_timestamp, &path_in_container)?;
212                data.seek(SeekFrom::Current(size as i64))?;
213
214                anim_pack.files.insert(path_in_container, file);
215            }
216        }
217
218        anim_pack.paths_cache_generate();
219
220        anim_pack.files.par_iter_mut().map(|(_, file)| file.guess_file_type()).collect::<Result<()>>()?;
221
222        check_size_mismatch(data.stream_position()? as usize - anim_pack.disk_file_offset as usize, disk_file_size as usize)?;
223        Ok(anim_pack)
224    }
225}
226
227impl Encodeable for AnimPack {
228
229    /// This function allow us to encode an structured AnimPack into something implementing
230    /// [WriteBytes], like a [File] or a [Vec]<[u8]>.
231    ///
232    /// About [extra_data](crate::files::EncodeableExtraData), its not used in this implementation, so pass a [None].
233    ///
234    /// ```rust
235    ///use std::fs::File;
236    ///use std::io::{BufReader, BufWriter, Write};
237    ///
238    ///use rpfm_lib::binary::ReadBytes;
239    ///use rpfm_lib::files::{*, animpack::AnimPack};
240    ///
241    ///let mut data = AnimPack::default();
242    ///let mut encoded = vec![];
243    ///data.encode(&mut encoded, &None).unwrap();
244    ///
245    ///let path = "../test_files/test_encode.animpack";
246    ///let mut writer = BufWriter::new(File::create(path).unwrap());
247    ///writer.write_all(&encoded).unwrap();
248    /// ```
249    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
250        buffer.write_u32(self.files.len() as u32)?;
251
252        // NOTE: This has to use /, not \, because for some reason the animsets made by Assed break if we use \.
253        let mut sorted_files = self.files.iter_mut().collect::<Vec<(&String, &mut RFile)>>();
254        sorted_files.sort_unstable_by_key(|(path, _)| path.to_lowercase());
255
256        for (path, file) in sorted_files {
257            buffer.write_sized_string_u8(path)?;
258
259            let data = file.encode(extra_data, false, false, true)?.unwrap();
260
261            // Error on files too big for the AnimPack.
262            if data.len() > u32::MAX as usize {
263                return Err(RLibError::DataTooBigForContainer("AnimPack".to_owned(), u32::MAX as u64, data.len(), path.to_owned()));
264            }
265
266            buffer.write_u32(data.len() as u32)?;
267            buffer.write_all(&data)?;
268        }
269
270        Ok(())
271    }
272}