Skip to main content

source_fs/
fs.rs

1use std::{collections::HashMap, path::{Path, PathBuf}, sync::Arc};
2
3use crate::{GameInfoProvider, PackFile, utils};
4
5#[derive(Debug, Clone, Default)]
6pub struct FileSystemOptions {
7    pub bin_platform: Option<String>,
8}
9
10// todo: add custom error type for FileSystem later
11
12/// Core FileSystem representation holding physical directories and loaded pack files.
13pub struct FileSystem<P: PackFile> {
14    root_path: PathBuf,
15    search_path_dirs: HashMap<String, Vec<PathBuf>>,
16    search_path_vpks: HashMap<String, Vec<Arc<P>>>,
17}
18
19impl<P: PackFile> FileSystem<P> {
20    /// Locates the game using the local Steam installation and loads the filesystem.
21    #[cfg(feature = "steam")]
22    pub fn load_from_app_id<G: GameInfoProvider>(
23        app_id: u32,
24        game_name: &str,
25        options: &FileSystemOptions,
26    ) -> Option<Self> {
27        let steamdir = steamlocate::locate().ok()?;
28        let (app, library) = steamdir.find_app(app_id).ok()??;
29        let game_path = library.resolve_app_dir(&app).join(&game_name);
30
31        Self::load_from_path::<G>(&game_path, options)
32    }
33
34    /// Loads the filesystem from a specific game directory (where `gameinfo.txt` resides).
35    pub fn load_from_path<G: GameInfoProvider>(
36        game_path: &Path,
37        options: &FileSystemOptions,
38    ) -> Option<Self> {
39        let gameinfo_path = game_path.join("gameinfo.txt");
40        if !gameinfo_path.is_file() {
41            return None;
42        }
43
44        let root_path = game_path.parent()?.to_path_buf();
45        let game_id = game_path.file_name()?.to_string_lossy().to_string();
46
47        let mut fs = Self {
48            root_path,
49            search_path_dirs: HashMap::new(),
50            search_path_vpks: HashMap::new(),
51        };
52
53        let search_paths = G::get_search_paths(&gameinfo_path)?;
54        if search_paths.is_empty() {
55            return Some(fs);
56        }
57
58        for (i, (key, value)) in search_paths.into_iter().enumerate() {
59            let searches: Vec<String> = key.to_lowercase()
60                .split('+')
61                .map(|s| s.to_string())
62                .collect();
63
64            let mut path = value;// .to_lowercase(); // todo: case insensitive!
65
66            let all_source_engine_paths = "|all_source_engine_paths|";
67            let gameinfo_path_macro = "|gameinfo_path|";
68
69            if path.starts_with(all_source_engine_paths) {
70                path = path[all_source_engine_paths.len()..].to_string();
71            } else if path.starts_with(gameinfo_path_macro) {
72                path = format!("{}/{}", game_id, &path[gameinfo_path_macro.len()..]);
73            }
74
75            if path.ends_with('.') && !path.ends_with("..") {
76                path.pop();
77            }
78            let path = utils::normalize_slashes(&path, false, false);
79
80            if path.ends_with(".vpk") {
81                let mut full_path = fs.root_path.join(&path);
82
83                if !full_path.exists() {
84                    // Try to fallback to the `_dir.vpk` naming convention
85                    if let Some(stem) = full_path.file_stem() {
86                        let parent = full_path.parent().unwrap_or_else(|| Path::new(""));
87                        let dir_vpk = parent.join(format!("{}_dir.vpk", stem.to_string_lossy()));
88                        if dir_vpk.exists() {
89                            full_path = dir_vpk;
90                        } else {
91                            continue;
92                        }
93                    } else {
94                        continue;
95                    }
96                }
97
98                if let Some(pack) = P::open(&full_path).map(Arc::new) {
99                    for search in &searches {
100                        fs.search_path_vpks
101                            .entry(search.clone())
102                            .or_default()
103                            .push(Arc::clone(&pack));
104                    }
105                }
106            } else {
107                for search in &searches {
108                    if path.ends_with("/*") {
109                        let glob_parent_path = fs.root_path.join(&path[..path.len() - 2]);
110                        if glob_parent_path.is_dir() {
111                            if let Ok(entries) = std::fs::read_dir(&glob_parent_path) {
112                                for entry in entries.flatten() {
113                                    if let Ok(rel_path) = entry.path().strip_prefix(&fs.root_path) {
114                                        let glob_child_path = utils::normalize_slashes(
115                                            &rel_path.to_string_lossy(),
116                                            false,
117                                            false,
118                                        );
119                                        fs.search_path_dirs
120                                            .entry(search.clone())
121                                            .or_default()
122                                            .push(PathBuf::from(glob_child_path));
123                                    }
124                                }
125                            }
126                        }
127                    } else {
128                        let test_path = fs.root_path.join(&path);
129                        // dbg!(&fs.root_path, &path, &test_path); // todo: debug
130                        // dbg!(&test_path);
131                        if test_path.exists() {
132                            fs.search_path_dirs
133                                .entry(search.clone())
134                                .or_default()
135                                .push(PathBuf::from(&path));
136
137                            // Automatically populate `gamebin` and `mod` depending on context
138                            if search == "game" {
139                                fs.search_path_dirs
140                                    .entry("gamebin".to_string())
141                                    .or_default()
142                                    .push(PathBuf::from(format!("{}/bin", path)));
143
144                                if i == 0 {
145                                    fs.search_path_dirs
146                                        .entry("mod".to_string())
147                                        .or_default()
148                                        .push(PathBuf::from(&path));
149                                }
150                            }
151                        }
152                    }
153                }
154            }
155        }
156
157        // Setup default path overrides
158        let exec_paths = fs.search_path_dirs.entry("executable_path".to_string()).or_default();
159        if let Some(plat) = &options.bin_platform {
160            let plat_path = fs.root_path.join("bin").join(plat);
161            if plat_path.exists() {
162                exec_paths.push(PathBuf::from(format!("bin/{}", plat)));
163            }
164        }
165        exec_paths.push(PathBuf::from("bin"));
166        exec_paths.push(PathBuf::from(""));
167
168        fs.search_path_dirs
169            .entry("platform".to_string())
170            .or_insert_with(|| vec![PathBuf::from("platform")]);
171
172        if let Some(game_paths) = fs.search_path_dirs.get_mut("game") {
173            let platform_buf = PathBuf::from("platform");
174            if !game_paths.contains(&platform_buf) {
175                game_paths.push(platform_buf);
176            }
177        }
178
179        fs.search_path_dirs
180            .entry("default_write_path".to_string())
181            .or_insert_with(|| vec![PathBuf::from(&game_id)]);
182
183        fs.search_path_dirs
184            .entry("logdir".to_string())
185            .or_insert_with(|| vec![PathBuf::from(&game_id)]);
186
187        fs.search_path_dirs
188            .entry("config".to_string())
189            .or_insert_with(|| vec![PathBuf::from("platform/config")]);
190
191        Some(fs)
192    }
193
194    pub fn search_path_dirs(&self) -> &HashMap<String, Vec<PathBuf>> {
195        &self.search_path_dirs
196    }
197
198    pub fn search_path_dirs_mut(&mut self) -> &mut HashMap<String, Vec<PathBuf>> {
199        &mut self.search_path_dirs
200    }
201
202    pub fn search_path_vpks(&self) -> &HashMap<String, Vec<Arc<P>>> {
203        &self.search_path_vpks
204    }
205
206    pub fn search_path_vpks_mut(&mut self) -> &mut HashMap<String, Vec<Arc<P>>> {
207        &mut self.search_path_vpks
208    }
209
210    pub fn find_file(&self, file_path: &str, search_path: &str) -> Option<PathBuf> {
211        let file_path_str = utils::normalize_slashes(&file_path.to_lowercase(), true, false);
212        let search_path_str = search_path.to_lowercase();
213
214        if let Some(dirs) = self.search_path_dirs.get(&search_path_str) {
215            for base_path in dirs {
216                let base_dir = self.root_path.join(base_path);
217                if let Some(resolved_path) = utils::resolve_path_case_insensitive(&base_dir, &file_path_str) {
218                    return Some(resolved_path);
219                }
220            }
221        }
222
223        None
224    }
225
226    /// Reads data from the internal mounted paths using standard Source Engine priorities.
227    pub fn read(&self, file_path: &str, search_path: &str, prioritize_vpks: bool) -> Option<Vec<u8>> {
228        let file_path_str = utils::normalize_slashes(&file_path.to_lowercase(), true, false);
229        let search_path_str = search_path.to_lowercase();
230
231        if prioritize_vpks {
232            if let Some(data) = self.check_vpks(&search_path_str, &file_path_str) {
233                return Some(data);
234            }
235        }
236
237        if let Some(resolved_path) = self.find_file(&file_path_str, &search_path_str) {
238            if let Ok(data) = std::fs::read(resolved_path) {
239                return Some(data);
240            }
241        }
242
243        if !prioritize_vpks {
244            return self.check_vpks(&search_path_str, &file_path_str);
245        }
246
247        None
248    }
249
250    /// Same as `read`, but takes an optional active map pack file which gets highest priority.
251    pub fn read_for_map(
252        &self,
253        map_pack: Option<&P>,
254        file_path: &str,
255        search_path: &str,
256        prioritize_vpks: bool,
257    ) -> Option<Vec<u8>> {
258        if let Some(map) = map_pack {
259            if map.has_entry(file_path) {
260                return map.read_entry(file_path);
261            }
262        }
263        self.read(file_path, search_path, prioritize_vpks)
264    }
265
266    pub fn read_str(&self, file_path: &str, search_path: &str, prioritize_vpks: bool) -> Option<String> {
267        let data = self.read(file_path, search_path, prioritize_vpks)?;
268        // Some(String::from_utf8_lossy(&data).to_string())
269        String::from_utf8(data).ok()
270    }
271
272    fn find_in_vpks(&self, search_path: &str, file_path: &str) -> Option<PathBuf> {
273        if let Some(vpks) = self.search_path_vpks.get(search_path) {
274            for vpk in vpks {
275                if vpk.has_entry(file_path) {
276                    return Some(PathBuf::from(file_path));
277                }
278            }
279        }
280        None
281    }
282
283    fn check_vpks(&self, search_path: &str, file_path: &str) -> Option<Vec<u8>> {
284        if let Some(vpks) = self.search_path_vpks.get(search_path) {
285            for vpk in vpks {
286                if vpk.has_entry(file_path) {
287                    return vpk.read_entry(file_path);
288                }
289            }
290        }
291        None
292    }
293}