physis/
repository.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::cmp::Ordering;
5use std::cmp::Ordering::{Greater, Less};
6use std::path::{Path, PathBuf};
7
8use crate::common::{Platform, get_platform_string, read_version};
9use crate::repository::RepositoryType::{Base, Expansion};
10
11/// The type of repository, discerning game data from expansion data.
12#[derive(Debug, PartialEq, Eq, Copy, Clone)]
13#[repr(C)]
14pub enum RepositoryType {
15    /// The base game directory, like "ffxiv".
16    Base,
17    /// An expansion directory, like "ex1".
18    Expansion {
19        /// The expansion number starting at 1.
20        number: i32,
21    },
22}
23
24/// Encapsulates a directory of game data, such as "ex1". This data is also versioned.
25/// This handles calculating the correct dat and index filenames, mainly for `GameData`.
26#[derive(Debug, Clone, Eq)]
27pub struct Repository {
28    /// The folder name, such as "ex1".
29    pub name: String,
30    /// The platform this repository is designed for.
31    pub platform: Platform,
32    /// The type of repository, such as "base game" or "expansion".
33    pub repo_type: RepositoryType,
34    /// The version of the game data.
35    pub version: Option<String>,
36}
37
38impl PartialEq for Repository {
39    fn eq(&self, other: &Self) -> bool {
40        self.name == other.name
41    }
42}
43
44impl Ord for Repository {
45    fn cmp(&self, other: &Self) -> Ordering {
46        // This ensures that the ordering of the repositories is always ffxiv, ex1, ex2 and so on.
47        match self.repo_type {
48            Base => Less,
49            Expansion { number } => {
50                let super_number = number;
51                match other.repo_type {
52                    Base => Greater,
53                    Expansion { number } => super_number.cmp(&number),
54                }
55            }
56        }
57    }
58}
59
60impl PartialOrd for Repository {
61    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
62        Some(self.cmp(other))
63    }
64}
65
66/// This refers to the specific root directory a file is located in.
67/// This is a fixed list of directories, and all of them are known.
68#[derive(Debug, PartialEq, Eq, Copy, Clone)]
69pub enum Category {
70    /// Common files such as game fonts, and other data that doesn't really fit anywhere else.
71    Common = 0x00,
72    /// Shared data between game maps.
73    BackgroundCommon = 0x01,
74    /// Game map data such as models, textures, and so on.
75    Background = 0x02,
76    /// Cutscene content such as animations.
77    Cutscene = 0x03,
78    /// Character model files and more.
79    Character = 0x04,
80    /// Compiled shaders used by the retail client.
81    Shader = 0x05,
82    /// UI layouts and textures.
83    UI = 0x06,
84    /// Sound effects, basically anything not under `Music`.
85    Sound = 0x07,
86    /// This "VFX" means "visual effects", and contains textures and definitions for stuff like battle effects.
87    VFX = 0x08,
88    /// A leftover from 1.0, where the UI was primarily driven by LUA scripts.
89    UIScript = 0x09,
90    /// Excel data.
91    EXD = 0x0A,
92    /// Many game events are driven by LUA scripts, such as cutscenes.
93    GameScript = 0x0B,
94    /// Music!
95    Music = 0x0C,
96    /// Unknown purpose, most likely to test SqPack functionality.
97    SqPackTest = 0x12,
98    /// Unknown purpose, most likely debug files.
99    Debug = 0x13,
100}
101
102pub fn string_to_category(string: &str) -> Option<Category> {
103    use crate::repository::Category::*;
104
105    match string {
106        "common" => Some(Common),
107        "bgcommon" => Some(BackgroundCommon),
108        "bg" => Some(Background),
109        "cut" => Some(Cutscene),
110        "chara" => Some(Character),
111        "shader" => Some(Shader),
112        "ui" => Some(UI),
113        "sound" => Some(Sound),
114        "vfx" => Some(VFX),
115        "ui_script" => Some(UIScript),
116        "exd" => Some(EXD),
117        "game_script" => Some(GameScript),
118        "music" => Some(Music),
119        "sqpack_test" => Some(SqPackTest),
120        "debug" => Some(Debug),
121        _ => None,
122    }
123}
124
125impl Repository {
126    /// Creates a new base `Repository`, from an existing directory. This may return `None` if
127    /// the directory is invalid, e.g. a version file is missing.
128    pub fn from_existing_base(platform: Platform, dir: &str) -> Option<Repository> {
129        let path = Path::new(dir);
130        if path.metadata().is_err() {
131            return None;
132        }
133
134        let mut d = PathBuf::from(dir);
135        d.push("ffxivgame.ver");
136
137        let version = read_version(d.as_path());
138        Some(Repository {
139            name: "ffxiv".to_string(),
140            platform,
141            repo_type: Base,
142            version,
143        })
144    }
145
146    /// Creates a new expansion `Repository`, from an existing directory. This may return `None` if
147    /// the directory is invalid, e.g. a version file is missing.
148    pub fn from_existing_expansion(platform: Platform, dir: &str) -> Option<Repository> {
149        let path = Path::new(dir);
150        if path.metadata().is_err() {
151            return None;
152        }
153
154        let name = String::from(path.file_stem()?.to_str()?);
155        let expansion_number = name[2..3].parse().ok()?;
156
157        let mut d = PathBuf::from(dir);
158        d.push(format!("{name}.ver"));
159
160        Some(Repository {
161            name,
162            platform,
163            repo_type: Expansion {
164                number: expansion_number,
165            },
166            version: read_version(d.as_path()),
167        })
168    }
169
170    /// Calculate an index filename for a specific category, like _"0a0000.win32.index"_.
171    pub fn index_filename(&self, chunk: u8, category: Category) -> String {
172        format!(
173            "{:02x}{:02}{:02}.{}.index",
174            category as i32,
175            self.expansion(),
176            chunk,
177            get_platform_string(&self.platform)
178        )
179    }
180
181    /// Calculate an index2 filename for a specific category, like _"0a0000.win32.index"_.
182    pub fn index2_filename(&self, chunk: u8, category: Category) -> String {
183        format!("{}2", self.index_filename(chunk, category))
184    }
185
186    /// Calculate a dat filename given a category and a data file id, returns something like _"0a0000.win32.dat0"_.
187    pub fn dat_filename(&self, chunk: u8, category: Category, data_file_id: u32) -> String {
188        let expansion = self.expansion();
189        let platform = get_platform_string(&self.platform);
190
191        format!(
192            "{:02x}{expansion:02}{chunk:02}.{platform}.dat{data_file_id}",
193            category as u32
194        )
195    }
196
197    fn expansion(&self) -> i32 {
198        match self.repo_type {
199            Base => 0,
200            Expansion { number } => number,
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use crate::common::Platform;
208    use std::path::PathBuf;
209
210    use super::*;
211
212    #[test]
213    fn test_base() {
214        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
215        d.push("resources/tests");
216        d.push("ffxiv");
217
218        let repository = Repository::from_existing_base(Platform::Win32, d.to_str().unwrap());
219        assert!(repository.is_some());
220        assert_eq!(repository.unwrap().version.unwrap(), "2012.01.01.0000.0000");
221    }
222
223    #[test]
224    fn test_expansion() {
225        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
226        d.push("resources/tests");
227        d.push("ex1");
228
229        let repository = Repository::from_existing_expansion(Platform::Win32, d.to_str().unwrap());
230        assert!(repository.is_some());
231        assert_eq!(repository.unwrap().version.unwrap(), "2012.01.01.0000.0000");
232    }
233
234    #[test]
235    fn test_win32_filenames() {
236        let repo = Repository {
237            name: "ffxiv".to_string(),
238            platform: Platform::Win32,
239            repo_type: RepositoryType::Base,
240            version: None,
241        };
242
243        assert_eq!(
244            repo.index_filename(0, Category::Music),
245            "0c0000.win32.index"
246        );
247        assert_eq!(
248            repo.index2_filename(0, Category::Music),
249            "0c0000.win32.index2"
250        );
251        assert_eq!(
252            repo.dat_filename(0, Category::GameScript, 1),
253            "0b0000.win32.dat1"
254        );
255    }
256
257    #[test]
258    fn test_ps3_filenames() {
259        let repo = Repository {
260            name: "ffxiv".to_string(),
261            platform: Platform::PS3,
262            repo_type: RepositoryType::Base,
263            version: None,
264        };
265
266        assert_eq!(repo.index_filename(0, Category::Music), "0c0000.ps3.index");
267        assert_eq!(
268            repo.index2_filename(0, Category::Music),
269            "0c0000.ps3.index2"
270        );
271        assert_eq!(
272            repo.dat_filename(0, Category::GameScript, 1),
273            "0b0000.ps3.dat1"
274        );
275    }
276
277    #[test]
278    fn test_ps4_filenames() {
279        let repo = Repository {
280            name: "ffxiv".to_string(),
281            platform: Platform::PS4,
282            repo_type: RepositoryType::Base,
283            version: None,
284        };
285
286        assert_eq!(repo.index_filename(0, Category::Music), "0c0000.ps4.index");
287        assert_eq!(
288            repo.index2_filename(0, Category::Music),
289            "0c0000.ps4.index2"
290        );
291        assert_eq!(
292            repo.dat_filename(0, Category::GameScript, 1),
293            "0b0000.ps4.dat1"
294        );
295    }
296}