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}