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
10pub 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 #[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 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;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 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 if test_path.exists() {
132 fs.search_path_dirs
133 .entry(search.clone())
134 .or_default()
135 .push(PathBuf::from(&path));
136
137 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 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 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 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 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}