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 = path.split_once('/')?;
203
204 let repository_token = tokens.1;
205
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)?))
213 }
214
215 fn get_index_filenames(&self, path: &str) -> Option<Vec<(String, u8)>> {
216 let (repository, category) = self.parse_repository_category(path)?;
217
218 let mut index_filenames = vec![];
219
220 for chunk in 0..255 {
221 let index_path: PathBuf = [
222 &self.game_directory,
223 "sqpack",
224 &repository.name,
225 &repository.index_filename(chunk, category),
226 ]
227 .iter()
228 .collect();
229
230 index_filenames.push((index_path.into_os_string().into_string().unwrap(), chunk));
231
232 let index2_path: PathBuf = [
233 &self.game_directory,
234 "sqpack",
235 &repository.name,
236 &repository.index2_filename(chunk, category),
237 ]
238 .iter()
239 .collect();
240
241 index_filenames.push((index2_path.into_os_string().into_string().unwrap(), chunk));
242 }
243
244 Some(index_filenames)
245 }
246
247 pub fn read_excel_sheet_header(&mut self, name: &str) -> Option<EXH> {
249 let root_exl_file = self.extract("exd/root.exl")?;
250
251 let root_exl = EXL::from_existing(&root_exl_file)?;
252
253 for (row, _) in root_exl.entries {
254 if row == name {
255 let new_filename = name.to_lowercase();
256
257 let path = format!("exd/{new_filename}.exh");
258
259 return EXH::from_existing(&self.extract(&path)?);
260 }
261 }
262
263 None
264 }
265
266 pub fn get_all_sheet_names(&mut self) -> Option<Vec<String>> {
268 let root_exl_file = self.extract("exd/root.exl")?;
269
270 let root_exl = EXL::from_existing(&root_exl_file)?;
271
272 let mut names = vec![];
273 for (row, _) in root_exl.entries {
274 names.push(row);
275 }
276
277 Some(names)
278 }
279
280 pub fn read_excel_sheet(
282 &mut self,
283 name: &str,
284 exh: &EXH,
285 language: Language,
286 page: usize,
287 ) -> Option<EXD> {
288 let exd_path = format!(
289 "exd/{}",
290 EXD::calculate_filename(name, language, &exh.pages[page])
291 );
292
293 let exd_file = self.extract(&exd_path)?;
294
295 EXD::from_existing(&exh, &exd_file)
296 }
297
298 pub fn apply_patch(&self, patch_path: &str) -> Result<(), PatchError> {
300 ZiPatch::apply(&self.game_directory, patch_path)
301 }
302
303 pub fn needs_repair(&self) -> Option<Vec<(&Repository, RepairAction)>> {
307 let mut repositories: Vec<(&Repository, RepairAction)> = Vec::new();
308 for repository in &self.repositories {
309 if repository.version.is_none() {
310 let ver_bak_path: PathBuf = [
312 self.game_directory.clone(),
313 "sqpack".to_string(),
314 repository.name.clone(),
315 format!("{}.bck", repository.name),
316 ]
317 .iter()
318 .collect();
319
320 let repair_action = if read_version(&ver_bak_path).is_some() {
321 RepairAction::VersionFileCanRestore
322 } else {
323 RepairAction::VersionFileMissing
324 };
325
326 repositories.push((repository, repair_action));
327 }
328 }
329
330 if repositories.is_empty() {
331 None
332 } else {
333 Some(repositories)
334 }
335 }
336
337 pub fn perform_repair<'a>(
341 &self,
342 repositories: &Vec<(&'a Repository, RepairAction)>,
343 ) -> Result<(), RepairError<'a>> {
344 for (repository, action) in repositories {
345 let ver_path: PathBuf = [
346 self.game_directory.clone(),
347 "sqpack".to_string(),
348 repository.name.clone(),
349 format!("{}.ver", repository.name),
350 ]
351 .iter()
352 .collect();
353
354 let new_version: String = match action {
355 RepairAction::VersionFileMissing => {
356 let repo_path: PathBuf = [
357 self.game_directory.clone(),
358 "sqpack".to_string(),
359 repository.name.clone(),
360 ]
361 .iter()
362 .collect();
363
364 fs::remove_dir_all(&repo_path)
365 .ok()
366 .ok_or(RepairError::FailedRepair(repository))?;
367
368 fs::create_dir_all(&repo_path)
369 .ok()
370 .ok_or(RepairError::FailedRepair(repository))?;
371
372 "2012.01.01.0000.0000".to_string() }
374 RepairAction::VersionFileCanRestore => {
375 let ver_bak_path: PathBuf = [
376 self.game_directory.clone(),
377 "sqpack".to_string(),
378 repository.name.clone(),
379 format!("{}.bck", repository.name),
380 ]
381 .iter()
382 .collect();
383
384 read_version(&ver_bak_path).ok_or(RepairError::FailedRepair(repository))?
385 }
386 };
387
388 fs::write(ver_path, new_version)
389 .ok()
390 .ok_or(RepairError::FailedRepair(repository))?;
391 }
392
393 Ok(())
394 }
395
396 fn cache_index_file(&mut self, filename: &str) {
397 if !self.index_files.contains_key(filename) {
398 if let Some(index_file) = SqPackIndex::from_existing(filename) {
399 self.index_files.insert(filename.to_string(), index_file);
400 }
401 }
402 }
403
404 fn get_index_file(&self, filename: &str) -> Option<&SqPackIndex> {
405 self.index_files.get(filename)
406 }
407
408 fn find_entry(&mut self, path: &str) -> Option<(IndexEntry, u8)> {
409 let index_paths = self.get_index_filenames(path)?;
410
411 for (index_path, chunk) in index_paths {
412 self.cache_index_file(&index_path);
413
414 if let Some(index_file) = self.get_index_file(&index_path) {
415 if let Some(entry) = index_file.find_entry(path) {
416 return Some((entry, chunk));
417 }
418 }
419 }
420
421 None
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use crate::repository::Category::EXD;
428
429 use super::*;
430
431 fn common_setup_data() -> GameData {
432 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
433 d.push("resources/tests");
434 d.push("valid_sqpack");
435 d.push("game");
436
437 GameData::from_existing(Platform::Win32, d.to_str().unwrap())
438 }
439
440 #[test]
441 fn repository_ordering() {
442 let data = common_setup_data();
443
444 assert_eq!(data.repositories[0].name, "ffxiv");
445 assert_eq!(data.repositories[1].name, "ex1");
446 assert_eq!(data.repositories[2].name, "ex2");
447 }
448
449 #[test]
450 fn repository_and_category_parsing() {
451 let data = common_setup_data();
452
453 assert_eq!(
454 data.parse_repository_category("exd/root.exl").unwrap(),
455 (&data.repositories[0], EXD)
456 );
457 assert!(
458 data.parse_repository_category("what/some_font.dat")
459 .is_none()
460 );
461 }
462}