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