1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later

use std::cmp::Ordering;
use std::cmp::Ordering::{Greater, Less};
use std::path::{Path, PathBuf};

use crate::common::{get_platform_string, read_version, Platform};
use crate::repository::RepositoryType::{Base, Expansion};

/// The type of repository, discerning game data from expansion data.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[repr(C)]
pub enum RepositoryType {
    /// The base game directory, like "ffxiv".
    Base,
    /// An expansion directory, like "ex1".
    Expansion {
        /// The expansion number starting at 1.
        number: i32,
    },
}

/// Encapsulates a directory of game data, such as "ex1". This data is also versioned.
/// This handles calculating the correct dat and index filenames, mainly for `GameData`.
#[derive(Debug, Clone)]
pub struct Repository {
    /// The folder name, such as "ex1".
    pub name: String,
    /// The platform this repository is designed for.
    pub platform: Platform,
    /// The type of repository, such as "base game" or "expansion".
    pub repo_type: RepositoryType,
    /// The version of the game data.
    pub version: Option<String>,
}

impl Eq for Repository {}

impl PartialEq for Repository {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl Ord for Repository {
    fn cmp(&self, other: &Self) -> Ordering {
        // This ensures that the ordering of the repositories is always ffxiv, ex1, ex2 and so on.
        match self.repo_type {
            Base => Less,
            Expansion { number } => {
                let super_number = number;
                match other.repo_type {
                    Base => Greater,
                    Expansion { number } => super_number.cmp(&number),
                }
            }
        }
    }
}

impl PartialOrd for Repository {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

/// This refers to the specific root directory a file is located in.
/// This is a fixed list of directories, and all of them are known.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Category {
    /// Common files such as game fonts, and other data that doesn't really fit anywhere else.
    Common,
    /// Shared data between game maps.
    BackgroundCommon,
    /// Game map data such as models, textures, and so on.
    Background,
    /// Cutscene content such as animations.
    Cutscene,
    /// Character model files and more.
    Character,
    /// Compiled shaders used by the retail client.
    Shader,
    /// UI layouts and textures.
    UI,
    /// Sound effects, basically anything not under `Music`.
    Sound,
    /// This "VFX" means "visual effects", and contains textures and definitions for stuff like battle effects.
    VFX,
    /// A leftover from 1.0, where the UI was primarily driven by LUA scripts.
    UIScript,
    /// Excel data.
    EXD,
    /// Many game events are driven by LUA scripts, such as cutscenes.
    GameScript,
    /// Music!
    Music,
    /// Unknown purpose, most likely to test SqPack functionality.
    SqPackTest,
    /// Unknown purpose, most likely debug files.
    Debug,
}

pub fn string_to_category(string: &str) -> Option<Category> {
    use crate::repository::Category::*;

    match string {
        "common" => Some(Common),
        "bgcommon" => Some(BackgroundCommon),
        "bg" => Some(Background),
        "cut" => Some(Cutscene),
        "chara" => Some(Character),
        "shader" => Some(Shader),
        "ui" => Some(UI),
        "sound" => Some(Sound),
        "vfx" => Some(VFX),
        "ui_script" => Some(UIScript),
        "exd" => Some(EXD),
        "game_script" => Some(GameScript),
        "music" => Some(Music),
        "sqpack_test" => Some(SqPackTest),
        "debug" => Some(Debug),
        _ => None,
    }
}

impl Repository {
    /// Creates a new base `Repository`, from an existing directory. This may return `None` if
    /// the directory is invalid, e.g. a version file is missing.
    pub fn from_existing_base(platform: Platform, dir: &str) -> Option<Repository> {
        let path = Path::new(dir);
        if path.metadata().is_err() {
            return None;
        }

        let mut d = PathBuf::from(dir);
        d.push("ffxivgame.ver");

        let version = read_version(d.as_path());
        Some(Repository {
            name: "ffxiv".to_string(),
            platform,
            repo_type: Base,
            version,
        })
    }

    /// Creates a new expansion `Repository`, from an existing directory. This may return `None` if
    /// the directory is invalid, e.g. a version file is missing.
    pub fn from_existing_expansion(platform: Platform, dir: &str) -> Option<Repository> {
        let path = Path::new(dir);
        if path.metadata().is_err() {
            return None;
        }

        let name = String::from(path.file_stem()?.to_str()?);
        let expansion_number = name[2..3].parse().ok()?;

        let mut d = PathBuf::from(dir);
        d.push(format!("{name}.ver"));

        Some(Repository {
            name,
            platform,
            repo_type: Expansion {
                number: expansion_number,
            },
            version: read_version(d.as_path()),
        })
    }

    /// Calculate an index filename for a specific category, like _"0a0000.win32.index"_.
    pub fn index_filename(&self, category: Category) -> String {
        format!(
            "{:02x}{:02}{:02}.{}.index",
            category as i32,
            self.expansion(),
            0,
            get_platform_string(&self.platform)
        )
    }

    /// Calculate an index2 filename for a specific category, like _"0a0000.win32.index"_.
    pub fn index2_filename(&self, category: Category) -> String {
        format!("{}2", self.index_filename(category))
    }

    /// Calculate a dat filename given a category and a data file id, returns something like _"0a0000.win32.dat0"_.
    pub fn dat_filename(&self, category: Category, data_file_id: u32) -> String {
        let expansion = self.expansion();
        let chunk = 0;
        let platform = get_platform_string(&self.platform);

        format!(
            "{:02x}{expansion:02}{chunk:02}.{platform}.dat{data_file_id}",
            category as u32
        )
    }

    fn expansion(&self) -> i32 {
        match self.repo_type {
            Base => 0,
            Expansion { number } => number,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::common::Platform;
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn test_base() {
        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        d.push("resources/tests");
        d.push("ffxiv");

        let repository = Repository::from_existing_base(Platform::Win32, d.to_str().unwrap());
        assert!(repository.is_some());
        assert_eq!(repository.unwrap().version.unwrap(), "2012.01.01.0000.0000");
    }

    #[test]
    fn test_expansion() {
        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        d.push("resources/tests");
        d.push("ex1");

        let repository = Repository::from_existing_expansion(Platform::Win32, d.to_str().unwrap());
        assert!(repository.is_some());
        assert_eq!(repository.unwrap().version.unwrap(), "2012.01.01.0000.0000");
    }

    #[test]
    fn test_win32_filenames() {
        let repo = Repository {
            name: "ffxiv".to_string(),
            platform: Platform::Win32,
            repo_type: RepositoryType::Base,
            version: None,
        };

        assert_eq!(repo.index_filename(Category::Music), "0c0000.win32.index");
        assert_eq!(repo.index2_filename(Category::Music), "0c0000.win32.index2");
        assert_eq!(
            repo.dat_filename(Category::GameScript, 1),
            "0b0000.win32.dat1"
        );
    }

    // TODO: We need to check if these console filenames are actually correct
    #[test]
    fn test_ps3_filenames() {
        let repo = Repository {
            name: "ffxiv".to_string(),
            platform: Platform::PS3,
            repo_type: RepositoryType::Base,
            version: None,
        };

        assert_eq!(repo.index_filename(Category::Music), "0c0000.ps3.index");
        assert_eq!(repo.index2_filename(Category::Music), "0c0000.ps3.index2");
        assert_eq!(
            repo.dat_filename(Category::GameScript, 1),
            "0b0000.ps3.dat1"
        );
    }

    #[test]
    fn test_ps4_filenames() {
        let repo = Repository {
            name: "ffxiv".to_string(),
            platform: Platform::PS4,
            repo_type: RepositoryType::Base,
            version: None,
        };

        assert_eq!(repo.index_filename(Category::Music), "0c0000.ps4.index");
        assert_eq!(repo.index2_filename(Category::Music), "0c0000.ps4.index2");
        assert_eq!(
            repo.dat_filename(Category::GameScript, 1),
            "0b0000.ps4.dat1"
        );
    }
}