1use std::collections::HashMap;
5use std::fs;
6use std::fs::{DirEntry, ReadDir};
7use std::path::PathBuf;
8
9use tracing::{debug, warn};
10
11use crate::ByteBuffer;
12use crate::common::{Language, Platform, read_version};
13use crate::exd::EXD;
14use crate::exh::EXH;
15use crate::exl::EXL;
16use crate::patch::{PatchError, ZiPatch};
17use crate::repository::{Category, Repository, string_to_category};
18use crate::sqpack::{IndexEntry, SqPackData, SqPackIndex};
19
20pub struct GameData {
22 pub game_directory: String,
24
25 pub repositories: Vec<Repository>,
27
28 index_files: HashMap<String, SqPackIndex>,
29}
30
31fn is_valid(path: &str) -> bool {
32 let d = PathBuf::from(path);
33
34 if fs::metadata(d.as_path()).is_err() {
35 warn!("Game directory not found.");
36 return false;
37 }
38
39 true
40}
41
42#[derive(Debug)]
44pub enum RepairAction {
45 VersionFileMissing,
47 VersionFileCanRestore,
49}
50
51#[derive(Debug)]
52pub enum RepairError<'a> {
54 FailedRepair(&'a Repository),
56}
57
58impl GameData {
59 pub fn from_existing(platform: Platform, directory: &str) -> Option<GameData> {
72 debug!(directory, "Loading game directory");
73
74 match is_valid(directory) {
75 true => {
76 let mut data = Self {
77 game_directory: String::from(directory),
78 repositories: vec![],
79 index_files: HashMap::new(),
80 };
81 data.reload_repositories(platform);
82 Some(data)
83 }
84 false => {
85 warn!("Game data is not valid!");
86 None
87 }
88 }
89 }
90
91 fn reload_repositories(&mut self, platform: Platform) {
92 self.repositories.clear();
93
94 let mut d = PathBuf::from(self.game_directory.as_str());
95
96 if let Some(base_repository) =
98 Repository::from_existing_base(platform.clone(), d.to_str().unwrap())
99 {
100 self.repositories.push(base_repository);
101 }
102
103 d.push("sqpack");
105
106 if let Ok(repository_paths) = fs::read_dir(d.as_path()) {
107 let repository_paths: ReadDir = repository_paths;
108
109 let repository_paths: Vec<DirEntry> = repository_paths
110 .filter_map(Result::ok)
111 .filter(|s| s.file_type().unwrap().is_dir())
112 .collect();
113
114 for repository_path in repository_paths {
115 if let Some(expansion_repository) = Repository::from_existing_expansion(
116 platform.clone(),
117 repository_path.path().to_str().unwrap(),
118 ) {
119 self.repositories.push(expansion_repository);
120 }
121 }
122 }
123
124 self.repositories.sort();
125 }
126
127 fn get_dat_file(&self, path: &str, chunk: u8, data_file_id: u32) -> Option<SqPackData> {
128 let (repository, category) = self.parse_repository_category(path).unwrap();
129
130 let dat_path: PathBuf = [
131 self.game_directory.clone(),
132 "sqpack".to_string(),
133 repository.name.clone(),
134 repository.dat_filename(chunk, category, data_file_id),
135 ]
136 .iter()
137 .collect();
138
139 SqPackData::from_existing(dat_path.to_str()?)
140 }
141
142 pub fn exists(&mut self, path: &str) -> bool {
157 let Some(_) = self.get_index_filenames(path) else {
158 return false;
159 };
160
161 self.find_entry(path).is_some()
162 }
163
164 pub fn extract(&mut self, path: &str) -> Option<ByteBuffer> {
180 debug!(file = path, "Extracting file");
181
182 let slice = self.find_entry(path);
183 match slice {
184 Some((entry, chunk)) => {
185 let mut dat_file = self.get_dat_file(path, chunk, entry.data_file_id.into())?;
186
187 dat_file.read_from_offset(entry.offset)
188 }
189 None => None,
190 }
191 }
192
193 pub fn find_offset(&mut self, path: &str) -> Option<u64> {
195 let slice = self.find_entry(path);
196 slice.map(|(entry, _)| entry.offset)
197 }
198
199 fn parse_repository_category(&self, path: &str) -> Option<(&Repository, Category)> {
201 let tokens = path.split_once('/')?;
202
203 let repository_token = tokens.1;
204
205 for repository in &self.repositories {
206 if repository.name == repository_token {
207 return Some((repository, string_to_category(tokens.0)?));
208 }
209 }
210
211 Some((&self.repositories[0], string_to_category(tokens.0)?))
212 }
213
214 fn get_index_filenames(&self, path: &str) -> Option<Vec<(String, u8)>> {
215 let (repository, category) = self.parse_repository_category(path)?;
216
217 let mut index_filenames = vec![];
218
219 for chunk in 0..255 {
220 let index_path: PathBuf = [
221 &self.game_directory,
222 "sqpack",
223 &repository.name,
224 &repository.index_filename(chunk, category),
225 ]
226 .iter()
227 .collect();
228
229 index_filenames.push((index_path.into_os_string().into_string().unwrap(), chunk));
230
231 let index2_path: PathBuf = [
232 &self.game_directory,
233 "sqpack",
234 &repository.name,
235 &repository.index2_filename(chunk, category),
236 ]
237 .iter()
238 .collect();
239
240 index_filenames.push((index2_path.into_os_string().into_string().unwrap(), chunk));
241 }
242
243 Some(index_filenames)
244 }
245
246 pub fn read_excel_sheet_header(&mut self, name: &str) -> Option<EXH> {
248 let root_exl_file = self.extract("exd/root.exl")?;
249
250 let root_exl = EXL::from_existing(&root_exl_file)?;
251
252 for (row, _) in root_exl.entries {
253 if row == name {
254 let new_filename = name.to_lowercase();
255
256 let path = format!("exd/{new_filename}.exh");
257
258 return EXH::from_existing(&self.extract(&path)?);
259 }
260 }
261
262 None
263 }
264
265 pub fn get_all_sheet_names(&mut self) -> Option<Vec<String>> {
267 let root_exl_file = self.extract("exd/root.exl")?;
268
269 let root_exl = EXL::from_existing(&root_exl_file)?;
270
271 let mut names = vec![];
272 for (row, _) in root_exl.entries {
273 names.push(row);
274 }
275
276 Some(names)
277 }
278
279 pub fn read_excel_sheet(
281 &mut self,
282 name: &str,
283 exh: &EXH,
284 language: Language,
285 page: usize,
286 ) -> Option<EXD> {
287 let exd_path = format!(
288 "exd/{}",
289 EXD::calculate_filename(name, language, &exh.pages[page])
290 );
291
292 let exd_file = self.extract(&exd_path)?;
293
294 EXD::from_existing(&exd_file)
295 }
296
297 pub fn apply_patch(&self, patch_path: &str) -> Result<(), PatchError> {
299 ZiPatch::apply(&self.game_directory, patch_path)
300 }
301
302 pub fn needs_repair(&self) -> Option<Vec<(&Repository, RepairAction)>> {
306 let mut repositories: Vec<(&Repository, RepairAction)> = Vec::new();
307 for repository in &self.repositories {
308 if repository.version.is_none() {
309 let ver_bak_path: PathBuf = [
311 self.game_directory.clone(),
312 "sqpack".to_string(),
313 repository.name.clone(),
314 format!("{}.bck", repository.name),
315 ]
316 .iter()
317 .collect();
318
319 let repair_action = if read_version(&ver_bak_path).is_some() {
320 RepairAction::VersionFileCanRestore
321 } else {
322 RepairAction::VersionFileMissing
323 };
324
325 repositories.push((repository, repair_action));
326 }
327 }
328
329 if repositories.is_empty() {
330 None
331 } else {
332 Some(repositories)
333 }
334 }
335
336 pub fn perform_repair<'a>(
340 &self,
341 repositories: &Vec<(&'a Repository, RepairAction)>,
342 ) -> Result<(), RepairError<'a>> {
343 for (repository, action) in repositories {
344 let ver_path: PathBuf = [
345 self.game_directory.clone(),
346 "sqpack".to_string(),
347 repository.name.clone(),
348 format!("{}.ver", repository.name),
349 ]
350 .iter()
351 .collect();
352
353 let new_version: String = match action {
354 RepairAction::VersionFileMissing => {
355 let repo_path: PathBuf = [
356 self.game_directory.clone(),
357 "sqpack".to_string(),
358 repository.name.clone(),
359 ]
360 .iter()
361 .collect();
362
363 fs::remove_dir_all(&repo_path)
364 .ok()
365 .ok_or(RepairError::FailedRepair(repository))?;
366
367 fs::create_dir_all(&repo_path)
368 .ok()
369 .ok_or(RepairError::FailedRepair(repository))?;
370
371 "2012.01.01.0000.0000".to_string() }
373 RepairAction::VersionFileCanRestore => {
374 let ver_bak_path: PathBuf = [
375 self.game_directory.clone(),
376 "sqpack".to_string(),
377 repository.name.clone(),
378 format!("{}.bck", repository.name),
379 ]
380 .iter()
381 .collect();
382
383 read_version(&ver_bak_path).ok_or(RepairError::FailedRepair(repository))?
384 }
385 };
386
387 fs::write(ver_path, new_version)
388 .ok()
389 .ok_or(RepairError::FailedRepair(repository))?;
390 }
391
392 Ok(())
393 }
394
395 fn cache_index_file(&mut self, filename: &str) {
396 if !self.index_files.contains_key(filename) {
397 if let Some(index_file) = SqPackIndex::from_existing(filename) {
398 self.index_files.insert(filename.to_string(), index_file);
399 }
400 }
401 }
402
403 fn get_index_file(&self, filename: &str) -> Option<&SqPackIndex> {
404 self.index_files.get(filename)
405 }
406
407 fn find_entry(&mut self, path: &str) -> Option<(IndexEntry, u8)> {
408 let index_paths = self.get_index_filenames(path)?;
409
410 for (index_path, chunk) in index_paths {
411 self.cache_index_file(&index_path);
412
413 if let Some(index_file) = self.get_index_file(&index_path) {
414 if let Some(entry) = index_file.find_entry(path) {
415 return Some((entry, chunk));
416 }
417 }
418 }
419
420 None
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use crate::repository::Category::EXD;
427
428 use super::*;
429
430 fn common_setup_data() -> GameData {
431 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
432 d.push("resources/tests");
433 d.push("valid_sqpack");
434 d.push("game");
435
436 GameData::from_existing(Platform::Win32, d.to_str().unwrap()).unwrap()
437 }
438
439 #[test]
440 fn repository_ordering() {
441 let data = common_setup_data();
442
443 assert_eq!(data.repositories[0].name, "ffxiv");
444 assert_eq!(data.repositories[1].name, "ex1");
445 assert_eq!(data.repositories[2].name, "ex2");
446 }
447
448 #[test]
449 fn repository_and_category_parsing() {
450 let data = common_setup_data();
451
452 assert_eq!(
453 data.parse_repository_category("exd/root.exl").unwrap(),
454 (&data.repositories[0], EXD)
455 );
456 assert!(
457 data.parse_repository_category("what/some_font.dat")
458 .is_none()
459 );
460 }
461}