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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
use std::fs;
use std::path::Path;
use binrw::{Endian, binrw};
use strum_macros::{Display, FromRepr};
#[binrw]
#[brw(repr(u8))]
#[repr(u8)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
/// Language the game data is written for.
///
/// Keep in mind that the selection of languages vary depending on the client's region.
pub enum Language {
/// Used for data that is language-agnostic.
None,
/// Japanese language. Only available in the Global client.
Japanese,
/// English language. Only available in the Global client.
English,
/// German language. Only available in the Global client.
German,
/// French language. Only available in the Global client.
French,
/// Chinese (Simplified) language. Only available in the Chinese client.
ChineseSimplified,
/// Chinese (Traditional) language. Only available in the Chinese client.
ChineseTraditional,
/// Korean language. Only available in the Korean client.
Korean,
/// (Traditional) Chinese language. Only available in the Taiwanese client.
TraditionalChinese,
}
impl Language {
/// Returns the shorthand language code for `language`.
///
/// For example, English becomes "en".
pub fn shortname(&self) -> &'static str {
match self {
Language::None => "",
Language::Japanese => "ja",
Language::English => "en",
Language::German => "de",
Language::French => "fr",
Language::ChineseSimplified => "chs",
Language::ChineseTraditional => "cht",
Language::Korean => "ko",
Language::TraditionalChinese => "tc",
}
}
}
/// Reads a version file. It intentionally reads whitespace as the game reads those characters too.
// TODO: use version type
pub fn read_version(p: &Path) -> Option<String> {
fs::read_to_string(p).ok()
}
/// Platform used for game data.
#[binrw]
#[brw(repr = u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum Platform {
/// Windows and macOS.
Win32 = 0x0,
/// Playstation 3.
PS3 = 0x1,
/// Playstation 4.
PS4 = 0x2,
/// Playstation 5.
PS5 = 0x3,
/// Xbox One.
Xbox = 0x4,
}
/// Used internally for automatic SqPack detection. Please update when adding new platforms!
pub(crate) const PLATFORM_LIST: [Platform; 5] = [
Platform::Win32,
Platform::PS3,
Platform::PS4,
Platform::PS5,
Platform::Xbox,
];
impl Platform {
/// Returns the short-hand codename for this platform.
///
/// For example, `Platform::Win32` becomes "win32".
pub fn shortname(&self) -> &'static str {
match self {
Platform::Win32 => "win32",
Platform::PS3 => "ps3",
Platform::PS4 => "ps4",
Platform::PS5 => "ps5",
Platform::Xbox => "lys",
}
}
/// Returns the endianness for this platform.
pub(crate) fn endianness(&self) -> Endian {
match self {
Platform::PS3 => Endian::Big,
_ => Endian::Little,
}
}
}
use std::{
cmp::Ordering,
fmt::{self, Display, Formatter},
};
/// Represents a game version, e.g. "2025.02.27.0000.0000".
///
/// Unlike a normal string, this can sort itself in a sensible way.
#[derive(PartialEq, Eq, PartialOrd)]
pub struct Version<'a>(pub &'a str);
#[derive(PartialEq, Eq, Ord, PartialOrd)]
struct VersionParts {
year: i32,
month: i32,
day: i32,
patch1: i32,
patch2: i32,
}
impl VersionParts {
fn new(version: &str) -> Self {
let parts: Vec<&str> = version.split('.').collect();
Self {
year: parts[0].parse::<i32>().unwrap(),
month: parts[1].parse::<i32>().unwrap(),
day: parts[2].parse::<i32>().unwrap(),
patch1: parts[3].parse::<i32>().unwrap(),
patch2: parts[4].parse::<i32>().unwrap(),
}
}
}
impl Display for Version<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Ord for Version<'_> {
fn cmp(&self, other: &Self) -> Ordering {
let our_version_parts = VersionParts::new(self.0);
let their_version_parts = VersionParts::new(other.0);
our_version_parts.cmp(&their_version_parts)
}
}
/// A file that can be parsed from its serialized byte form.
///
/// This should be implemented for all types readable from SqPack.
pub trait ReadableFile: Sized {
/// Read an existing file.
fn from_existing(platform: Platform, buffer: ByteSpan) -> Option<Self>;
}
/// A file that can be written back to its serialized byte form.
///
/// This should be implemented for all types readable from SqPack, on a best-effort basis.
pub trait WritableFile: Sized {
/// Writes data back to a buffer.
fn write_to_buffer(&self, platform: Platform) -> Option<ByteBuffer>;
}
/// Used for basic sanity checking tests in other modules.
#[cfg(test)]
pub fn pass_random_invalid<T: ReadableFile>() {
let mut d = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("resources/tests");
d.push("random");
// Feeding it invalid data should not panic
// Note that we don't check the Option currently, because some types like Hwc return Some regardless.
T::from_existing(
Platform::Win32,
&std::fs::read(d).expect("Could not read random test file"),
);
}
/// A continuous block of memory which is not owned, and comes either from an in-memory location or from a file.
pub type ByteSpan<'a> = &'a [u8];
/// A continuous block of memory which is owned.
pub type ByteBuffer = Vec<u8>;
/// Names for rows in the Excel sheet of the same name.
/// See <https://github.com/aers/FFXIVClientStructs/blob/main/FFXIVClientStructs/FFXIV/Client/Enums/TerritoryIntendedUse.cs>.
#[repr(u8)]
#[derive(FromRepr, Display, Clone, Copy, PartialEq)]
pub enum TerritoryIntendedUse {
/// Towns such as Limsa Lominsa.
Town = 0,
/// Open world zones such as everything out of towns.
OpenWorld = 1,
/// Inn rooms.
Inn = 2,
/// Dungeon zones and other misc duties like Air Force One.
Dungeon = 3,
/// Variant dungeons like The Sil'dihn Subterrane.
VariantDungeon = 4,
/// Jail zones like Mordion Gaol.
Jail = 5,
/// Copies of Towns that are only during the opening.
OpeningArea = 6,
/// Rarely seen "lobby zones", such as the Phantom Village for Occult Crescent.
LobbyArea = 7,
/// Zones used in Alliance Raids.
AllianceRaid = 8,
/// Used for (pre-Endwalker?) quest battles.
OpenWorldInstanceBattle = 9,
/// Trial battles.
Trial = 10,
Unk100 = 11,
Unk110 = 12,
HousingOutdoor = 13,
HousingIndoor = 14,
SoloOverworldInstance = 15,
/// Fighting arenas for raids like.
Raid1 = 16,
/// Seen in at least AAC Heavyweight M1 (Savage)
Raid2 = 17,
/// Zones used for Frontline PvP.
Frontline = 18,
Unk120 = 19,
ChocoboRacing = 20,
/// Used for the only Ishgard Restoration zone, the Firamament.
IshgardRestoration = 21,
/// The Sanctum of the Twelve zone used for weddings.
Wedding = 22,
/// Gold Saucer zones.
GoldSaucer = 23,
/// ???
ExploratoryMissions = 26,
/// Used for the Hall of Novice tutorials.
HallOfTheNovice = 27,
/// Zones used for Crystalline Conflict PvP.
CrystallineConflict = 28,
/// Used for events like Solo Duties.
SoloDuty = 29,
/// The barracks zones of grand companies.
FreeCompanyGarrison = 30,
/// Zones used for Deep Dugeons, e.g. Palace of the Dead.
DeepDungeon = 31,
/// Used for zones only accessible seasonally, like Starlight Halls.
Seasonal = 32,
/// Treasure dungeons like Vault Oneiron.
TreasureDungeon = 33,
/// ???
SeasonalInstancedArea = 34,
/// ???
TripleTriadBattleHall = 35,
/// Used for raids like The Cloud of Darkness (Chaotic).
ChaoticRaid = 36,
/// ???
CrystallineConflictCustomMatch = 37,
/// Used in Rival Wings content.
RivalWings = 39,
/// Also used for Starlight Halls(?)
PrivateEventArea = 40,
/// Eureka zones.
Eureka = 41,
Unk2 = 42,
Unk3 = 43,
/// Leap of Faith zones.
LeapOfFaith = 44,
/// ???
MaskedCarnival = 45,
/// Zones used for Ocean Fishing.
OceanFishing = 46,
Unk7 = 47,
Unk8 = 48,
/// Island Sanctuary zones.
IslandSanctuary = 49,
Unk10 = 50,
/// Used in the Triple Triad Invitational Parlor duty.
TripleTriadInvitationalParlor = 51,
Unk12 = 52,
Unk13 = 53,
Unk14 = 54,
Unk15 = 55,
Elysion = 56,
/// Criterion Dungeons zones.
CriterionDungeon = 57,
/// Savage Criterion Dungeons zones.
SavageCriterionDungeon = 58,
/// Bean containment zones.
Blunderville = 59,
/// Cosmic Exploration zones.
CosmicExploration = 60,
/// Occult Crescent zones.
OccultCrescent = 61,
Unk22 = 62,
SprigCleaning = 63, // Lilyswim (Hatching-tide 2026)
Unknown64 = 64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eq() {
assert!(Version("2025.02.27.0000.0000") == Version("2025.02.27.0000.0000"));
assert!(Version("2025.01.20.0000.0000") != Version("2025.02.27.0000.0000"));
}
#[test]
fn test_ordering() {
// year
assert!(Version("2025.02.27.0000.0000") > Version("2024.02.27.0000.0000"));
// month
assert!(Version("2025.03.27.0000.0000") > Version("2025.02.27.0000.0000"));
// day
assert!(Version("2025.02.28.0000.0000") > Version("2025.02.27.0000.0000"));
// patch1
assert!(Version("2025.02.27.1000.0000") > Version("2025.02.27.0000.0000"));
// patch2
assert!(Version("2025.02.27.0000.1000") > Version("2025.02.27.0000.0000"));
}
#[test]
fn test_version() {
let mut dir = std::env::temp_dir();
dir.push("test.ver");
if dir.exists() {
std::fs::remove_file(&dir).unwrap();
}
assert_eq!(read_version(&dir), None);
std::fs::write(&dir, "2023.09.15.0000.0000").unwrap();
assert_eq!(read_version(&dir), Some("2023.09.15.0000.0000".to_string()));
std::fs::write(&dir, "2023.09.15.0000.0000\r\n").unwrap();
assert_eq!(
read_version(&dir),
Some("2023.09.15.0000.0000\r\n".to_string())
);
}
}