infinite_rs/module/loader.rs
1//! Main abstraction file for modules.
2
3use byteorder::{LE, ReadBytesExt};
4use std::{
5 fs::File,
6 io::{BufReader, Seek, SeekFrom},
7 path::Path,
8 ptr::eq,
9};
10
11use super::{
12 block::ModuleBlockEntry,
13 file::{DataOffsetType, ModuleFileEntry},
14 header::{ModuleHeader, ModuleVersion},
15};
16use crate::Result;
17use crate::{
18 Error,
19 common::{errors::TagError, extensions::BufReaderExt},
20};
21
22#[derive(Default, Debug)]
23/// Module structure which contains the layout of the entire module file.
24pub struct ModuleFile {
25 /// Information relating to how the other fields should be read.
26 pub header: ModuleHeader,
27 /// Metadata regarding compression and layout of files (tags).
28 pub files: Vec<ModuleFileEntry>,
29 /// Indices of resource files present in the module.
30 pub resource_indices: Vec<u32>,
31 /// Uncompressed/compressed blocks making up a file.
32 blocks: Vec<ModuleBlockEntry>,
33 /// Offset in [`BufReader`] where file data starts.
34 file_data_offset: u64,
35 /// Reference to the module file buffer.
36 module_file: Option<BufReader<File>>,
37 /// Reference to HD1 buffer if it exists.
38 hd1_file: Option<BufReader<File>>,
39 /// Whether to use the HD1 module or not.
40 pub use_hd1: bool,
41}
42
43impl ModuleFile {
44 /// Instantiates a [`ModuleFile`] object from the given file path.
45 pub fn from_path<T: AsRef<Path>>(file_path: T) -> Result<Self> {
46 let mut module = Self::default();
47 module.read(file_path)?;
48 Ok(module)
49 }
50
51 /// Reads the module file from the given file path.
52 /// This function reads the entire structure of the module file.
53 /// It also calculates and stores important offsets within the file.
54 ///
55 /// # Arguments
56 ///
57 /// * `file_path` - A reference to a type that implements [`Path`] that holds the path to the module file.
58 ///
59 /// # Errors
60 /// - If the reader fails to read the exact number of bytes [`ReadError`](`crate::Error::ReadError`)
61 /// - If the string table has invalid UTF-8 [`Utf8ReadingError`](`crate::Error::Utf8ReadingError`)
62 pub fn read<T: AsRef<Path>>(&mut self, file_path: T) -> Result<()> {
63 let file = File::open(&file_path)?;
64 let mut reader = BufReader::new(file);
65
66 self.header.read(&mut reader)?;
67 self.open_hd1(file_path)?;
68
69 for _ in 0..self.header.file_count {
70 let mut file = ModuleFileEntry::default();
71 file.read(&mut reader, &self.header.version)?;
72 self.files.push(file);
73 }
74
75 let strings_offset = reader.stream_position()?;
76 reader.seek(SeekFrom::Start(
77 strings_offset + u64::from(self.header.strings_size),
78 ))?;
79 self.resource_indices = (0..self.header.resource_count)
80 .map(|_| -> Result<u32> { Ok(reader.read_u32::<LE>()?) })
81 .collect::<Result<Vec<_>>>()?;
82 let post_resource_offset = reader.stream_position()?;
83
84 // Read strings contained in the file. A stringlist only exists in files before Season 3.
85 // Each entry is separated by a null terminator, and files specify their offset themselves
86 // in no particular order, so we cannot pre-read and just index into them.
87 //
88 // For files from modules that do not contain strings, we get it from the `get_tag_path` function.
89 reader.seek(SeekFrom::Start(strings_offset))?;
90 if self.header.version <= ModuleVersion::CampaignFlight {
91 for file in &mut self.files {
92 reader.seek(SeekFrom::Start(
93 strings_offset + u64::from(file.name_offset),
94 ))?;
95 file.tag_name = reader.read_null_terminated_string()?;
96 }
97 } else {
98 let tag_paths: Vec<String> = (0..self.files.len())
99 .map(|i| self.get_tag_path(i, 0))
100 .collect::<Result<Vec<_>>>()?;
101
102 for (file, tag_path) in self.files.iter_mut().zip(tag_paths) {
103 file.tag_name = tag_path;
104 }
105 }
106
107 reader.seek(SeekFrom::Start(post_resource_offset))?;
108 self.blocks =
109 reader.read_enumerable::<ModuleBlockEntry>(u64::from(self.header.block_count))?;
110
111 // Align to 0x?????000
112 let stream_position = reader.stream_position()?;
113 reader.seek(SeekFrom::Start((stream_position / 0x1000 + 1) * 0x1000))?;
114 self.file_data_offset = reader.stream_position()?;
115 self.module_file = Some(reader);
116 Ok(())
117 }
118
119 /// Opens the HD1 file if it exists.
120 fn open_hd1<T: AsRef<Path>>(&mut self, file_path: T) -> Result<()> {
121 if self.header.hd1_delta != 0 {
122 let hd1 = file_path.as_ref().with_extension("module_hd1");
123 if hd1.exists() {
124 self.use_hd1 = true;
125 let file = File::open(hd1)?;
126 self.hd1_file = Some(BufReader::new(file));
127 }
128 }
129 Ok(())
130 }
131
132 /// Gets the tag path of a file entry.
133 ///
134 /// This function returns the tag path of a file entry based on the provided index.
135 /// For file entries that have a parent, the function recursively gets the tag path of the parent and appends the child index to the path.
136 ///
137 /// # Arguments
138 /// * `index` - The index of the file entry to get the tag path from.
139 /// * `depth` - The depth of the recursion. This is used to prevent infinite recursion.
140 ///
141 /// # Returns
142 /// Returns the tag path of the file entry if the operation is successful.
143 fn get_tag_path(&self, index: usize, depth: usize) -> Result<String> {
144 if depth > 3 {
145 return Err(Error::TagError(TagError::RecursionDepth));
146 }
147 let file = &self.files[index];
148 if file.tag_id == -1 && file.parent_index != -1 {
149 let parent = &self.files[usize::try_from(file.parent_index)?];
150 let mut parent_name: String = String::new();
151 let child_index = self.resource_indices[usize::try_from(parent.resource_index)?
152 ..usize::try_from(parent.resource_index)?
153 + usize::try_from(parent.resource_count)?]
154 .iter()
155 .map(|&i| &self.files[i as usize])
156 .take_while(|&item| !eq(item, file))
157 .count();
158 if parent.tag_name.is_empty() {
159 parent_name = self.get_tag_path(usize::try_from(file.parent_index)?, depth + 1)?;
160 }
161 if parent.tag_id == -1 {
162 parent_name = self.get_tag_path(usize::try_from(file.parent_index)?, depth + 1)?;
163 Ok(format!("{parent_name}[{child_index}:block]"))
164 } else {
165 Ok(format!("{parent_name}[{child_index}:resource]"))
166 }
167 } else {
168 Ok(format!(
169 "{}/{}.{}",
170 file.tag_group, file.tag_id, file.tag_group
171 ))
172 }
173 }
174
175 /// Reads a specific tag from the module file.
176 ///
177 /// This function reads a specific tag from the module file based on the provided index.
178 /// It also utilizes the HD1 stream if the file entry has the flag set for it and the stream is loaded, and returns `None` if the tag offset is invalid.
179 ///
180 /// # Arguments
181 ///
182 /// * `index` - The index of the file entry to read the tag from. This index corresponds to
183 /// the position of the file entry in the [`files`](`ModuleFile::files`) vector.
184 ///
185 /// # Returns
186 ///
187 /// Returns a mutable reference to the file if the read operation is successful, or an [`Error`](`crate::Error`), a [`None`] if the file was not read (if tag offset is specified as invalid) or the containing the I/O error if any reading operation fails.
188 pub fn read_tag(&mut self, index: u32) -> Result<Option<&mut ModuleFileEntry>> {
189 let file = &mut self.files[index as usize];
190 if file.data_offset_flags.contains(DataOffsetType::DEBUG) {
191 return Ok(None); // Currently not reading debug modules because we don't have an
192 // example.
193 }
194
195 let mut offset = self.header.hd1_delta;
196 if file.data_offset_flags.contains(DataOffsetType::USE_HD1) {
197 if let Some(ref mut module_file) = self.hd1_file {
198 if self.header.version <= ModuleVersion::CampaignFlight {
199 offset += self.header.hd1_delta;
200 }
201 file.read_tag(
202 module_file,
203 offset,
204 &self.blocks,
205 &self.header.version,
206 true,
207 )?;
208 } else {
209 return Ok(None);
210 }
211 } else if let Some(ref mut module_file) = self.module_file {
212 file.read_tag(
213 module_file,
214 self.file_data_offset,
215 &self.blocks,
216 &self.header.version,
217 false,
218 )?;
219 }
220 Ok(Some(file))
221 }
222
223 /// Searches for the index of the tag given the `global_id`.
224 ///
225 /// This function searches for the index of a tag in the [`files`](`ModuleFile::files`) vector using the provided
226 /// `global_id`. If the tag is found, it reads the tag using the [`read_tag`](`ModuleFile::read_tag`) function and
227 /// stores it in the index.
228 ///
229 /// # Arguments
230 ///
231 /// * `global_id` - The global tag ID of the file to find. This ID is used to identify the
232 /// specific tag within the module file.
233 ///
234 /// # Returns
235 ///
236 /// Returns a mutable reference to the file if successful. If the tag is not
237 /// found or couldn't be read, it returns [`None`]. Any I/O error encountered during the operation is also returned
238 /// if it occurs.
239 pub fn read_tag_from_id(&mut self, global_id: i32) -> Result<Option<&mut ModuleFileEntry>> {
240 if let Some(index) = self.files.iter().position(|file| file.tag_id == global_id) {
241 let has_read = self.read_tag(u32::try_from(index)?)?;
242 if let Some(tag) = has_read {
243 Ok(Some(tag))
244 } else {
245 Ok(None)
246 }
247 } else {
248 Ok(None)
249 }
250 }
251}