Skip to main content

casc_lib/root/
flags.rs

1//! Locale and content flag bitmask types.
2//!
3//! Root file entries carry a pair of bitmasks - `LocaleFlags` and
4//! `ContentFlags` - that describe which client locale and platform
5//! configuration an entry applies to. During extraction the locale filter is
6//! compared against each entry's flags to select the correct variant.
7
8use std::fmt;
9
10/// Locale flag constants (bitmask).
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct LocaleFlags(pub u32);
13
14impl LocaleFlags {
15    /// No locale selected (acts as "no filter" in [`matches`](Self::matches)).
16    pub const NONE: Self = Self(0);
17    /// English (United States).
18    pub const EN_US: Self = Self(0x2);
19    /// Korean (South Korea).
20    pub const KO_KR: Self = Self(0x4);
21    /// French (France).
22    pub const FR_FR: Self = Self(0x10);
23    /// German (Germany).
24    pub const DE_DE: Self = Self(0x20);
25    /// Chinese (Simplified, China).
26    pub const ZH_CN: Self = Self(0x40);
27    /// Spanish (Spain).
28    pub const ES_ES: Self = Self(0x80);
29    /// Chinese (Traditional, Taiwan).
30    pub const ZH_TW: Self = Self(0x100);
31    /// English (Great Britain).
32    pub const EN_GB: Self = Self(0x200);
33    /// English (China region).
34    pub const EN_CN: Self = Self(0x400);
35    /// English (Taiwan region).
36    pub const EN_TW: Self = Self(0x800);
37    /// Spanish (Mexico / Latin America).
38    pub const ES_MX: Self = Self(0x1000);
39    /// Russian (Russia).
40    pub const RU_RU: Self = Self(0x2000);
41    /// Portuguese (Brazil).
42    pub const PT_BR: Self = Self(0x4000);
43    /// Italian (Italy).
44    pub const IT_IT: Self = Self(0x8000);
45    /// Portuguese (Portugal).
46    pub const PT_PT: Self = Self(0x10000);
47    /// All locale bits set - matches every locale.
48    pub const ALL: Self = Self(0xFFFFFFFF);
49
50    /// Returns true if any of the bits in `other` are set in `self`.
51    pub fn contains(self, other: Self) -> bool {
52        (self.0 & other.0) != 0
53    }
54
55    /// Returns true if this locale matches the given filter.
56    /// A zero/NONE filter means "no filter" and matches everything.
57    /// ALL (0xFFFFFFFF) also matches everything.
58    pub fn matches(self, filter: Self) -> bool {
59        filter.0 == 0 || (self.0 & filter.0) != 0
60    }
61}
62
63impl fmt::Display for LocaleFlags {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        if self.0 == 0 {
66            return write!(f, "None");
67        }
68        if self.0 == 0xFFFFFFFF {
69            return write!(f, "All");
70        }
71
72        const NAMES: &[(u32, &str)] = &[
73            (0x2, "enUS"),
74            (0x4, "koKR"),
75            (0x10, "frFR"),
76            (0x20, "deDE"),
77            (0x40, "zhCN"),
78            (0x80, "esES"),
79            (0x100, "zhTW"),
80            (0x200, "enGB"),
81            (0x400, "enCN"),
82            (0x800, "enTW"),
83            (0x1000, "esMX"),
84            (0x2000, "ruRU"),
85            (0x4000, "ptBR"),
86            (0x8000, "itIT"),
87            (0x10000, "ptPT"),
88        ];
89
90        let mut first = true;
91        for &(bit, name) in NAMES {
92            if (self.0 & bit) != 0 {
93                if !first {
94                    write!(f, "|")?;
95                }
96                write!(f, "{}", name)?;
97                first = false;
98            }
99        }
100        if first {
101            write!(f, "0x{:X}", self.0)?;
102        }
103        Ok(())
104    }
105}
106
107/// Content flag constants (bitmask).
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct ContentFlags(pub u32);
110
111impl ContentFlags {
112    /// No content flags set.
113    pub const NONE: Self = Self(0);
114    /// High-resolution texture variant.
115    pub const HIGH_RES_TEXTURE: Self = Self(0x1);
116    /// File is part of the install manifest.
117    pub const INSTALL: Self = Self(0x4);
118    /// Load this file on Windows clients.
119    pub const LOAD_ON_WINDOWS: Self = Self(0x8);
120    /// Load this file on macOS clients.
121    pub const LOAD_ON_MACOS: Self = Self(0x10);
122    /// 32-bit x86 binary.
123    pub const X86_32: Self = Self(0x20);
124    /// 64-bit x86 binary.
125    pub const X86_64: Self = Self(0x40);
126    /// Low-violence regional variant.
127    pub const LOW_VIOLENCE: Self = Self(0x80);
128    /// Do not load this file during normal operation.
129    pub const DO_NOT_LOAD: Self = Self(0x100);
130    /// File belongs to the update plugin.
131    pub const UPDATE_PLUGIN: Self = Self(0x800);
132    /// ARM64 binary.
133    pub const ARM64: Self = Self(0x8000);
134    /// File data is encrypted with a TACT key.
135    pub const ENCRYPTED: Self = Self(0x8000000);
136    /// Block does not contain name hashes (FDID-only entries).
137    pub const NO_NAME_HASH: Self = Self(0x10000000);
138    /// Uncommon resolution texture variant.
139    pub const UNCOMMON_RES: Self = Self(0x20000000);
140    /// File is part of a bundle.
141    pub const BUNDLE: Self = Self(0x40000000);
142    /// File data is stored without compression.
143    pub const NO_COMPRESSION: Self = Self(0x80000000);
144
145    /// Returns true if the given flag bit(s) are set.
146    pub fn has(self, flag: Self) -> bool {
147        (self.0 & flag.0) != 0
148    }
149
150    /// Convenience: checks if the NoNameHash flag is set.
151    pub fn has_no_name_hash(self) -> bool {
152        self.has(Self::NO_NAME_HASH)
153    }
154}
155
156impl fmt::Display for ContentFlags {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        if self.0 == 0 {
159            return write!(f, "None");
160        }
161
162        const NAMES: &[(u32, &str)] = &[
163            (0x1, "HighResTexture"),
164            (0x4, "Install"),
165            (0x8, "LoadOnWindows"),
166            (0x10, "LoadOnMacOS"),
167            (0x20, "x86_32"),
168            (0x40, "x86_64"),
169            (0x80, "LowViolence"),
170            (0x100, "DoNotLoad"),
171            (0x800, "UpdatePlugin"),
172            (0x8000, "ARM64"),
173            (0x8000000, "Encrypted"),
174            (0x10000000, "NoNameHash"),
175            (0x20000000, "UncommonRes"),
176            (0x40000000, "Bundle"),
177            (0x80000000, "NoCompression"),
178        ];
179
180        let mut first = true;
181        for &(bit, name) in NAMES {
182            if (self.0 & bit) != 0 {
183                if !first {
184                    write!(f, "|")?;
185                }
186                write!(f, "{}", name)?;
187                first = false;
188            }
189        }
190        if first {
191            write!(f, "0x{:X}", self.0)?;
192        }
193        Ok(())
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn locale_enus() {
203        assert_eq!(LocaleFlags::EN_US.0, 0x2);
204    }
205
206    #[test]
207    fn locale_dede() {
208        assert_eq!(LocaleFlags::DE_DE.0, 0x20);
209    }
210
211    #[test]
212    fn locale_contains() {
213        let flags = LocaleFlags(0x2 | 0x200); // enUS + enGB
214        assert!(flags.contains(LocaleFlags::EN_US));
215        assert!(flags.contains(LocaleFlags::EN_GB));
216        assert!(!flags.contains(LocaleFlags::DE_DE));
217    }
218
219    #[test]
220    fn locale_matches_filter() {
221        let flags = LocaleFlags(0x2); // enUS
222        assert!(flags.matches(LocaleFlags::EN_US));
223        assert!(!flags.matches(LocaleFlags::DE_DE));
224        assert!(flags.matches(LocaleFlags::ALL)); // ALL matches everything
225    }
226
227    #[test]
228    fn locale_matches_none_filter() {
229        // A NONE filter (0) means "no filter applied" - matches everything
230        let flags = LocaleFlags(0x2);
231        assert!(flags.matches(LocaleFlags::NONE));
232    }
233
234    #[test]
235    fn content_no_name_hash() {
236        let flags = ContentFlags(0x10000000);
237        assert!(flags.has_no_name_hash());
238    }
239
240    #[test]
241    fn content_flags_combined() {
242        let flags = ContentFlags(0x8 | 0x40); // Windows + x86_64
243        assert!(flags.has(ContentFlags::LOAD_ON_WINDOWS));
244        assert!(flags.has(ContentFlags::X86_64));
245        assert!(!flags.has(ContentFlags::LOAD_ON_MACOS));
246    }
247
248    #[test]
249    fn content_flags_none_has_nothing() {
250        let flags = ContentFlags::NONE;
251        assert!(!flags.has(ContentFlags::LOAD_ON_WINDOWS));
252        assert!(!flags.has_no_name_hash());
253    }
254
255    #[test]
256    fn locale_display_single() {
257        assert_eq!(format!("{}", LocaleFlags::EN_US), "enUS");
258    }
259
260    #[test]
261    fn locale_display_combined() {
262        let flags = LocaleFlags(0x2 | 0x20); // enUS + deDE
263        assert_eq!(format!("{}", flags), "enUS|deDE");
264    }
265
266    #[test]
267    fn locale_display_none() {
268        assert_eq!(format!("{}", LocaleFlags::NONE), "None");
269    }
270
271    #[test]
272    fn locale_display_all() {
273        assert_eq!(format!("{}", LocaleFlags::ALL), "All");
274    }
275
276    #[test]
277    fn content_display_single() {
278        assert_eq!(
279            format!("{}", ContentFlags::LOAD_ON_WINDOWS),
280            "LoadOnWindows"
281        );
282    }
283
284    #[test]
285    fn content_display_combined() {
286        let flags = ContentFlags(0x8 | 0x10000000); // Windows + NoNameHash
287        assert_eq!(format!("{}", flags), "LoadOnWindows|NoNameHash");
288    }
289
290    #[test]
291    fn content_display_none() {
292        assert_eq!(format!("{}", ContentFlags::NONE), "None");
293    }
294}