1use std::{collections::HashMap, path::{Path, PathBuf}, sync::Arc};
2
3use crate::{FileSystemError, GameInfoProvider, PackFile, utils};
4
5#[derive(Debug, Clone, Default)]
6pub struct FileSystemOptions {
7 pub bin_platform: Option<String>,
8}
9
10#[derive(Debug)]
12pub struct FileSystem<P: PackFile> {
13 root_path: PathBuf,
14 search_path_dirs: HashMap<String, Vec<PathBuf>>,
15 search_path_vpks: HashMap<String, Vec<Arc<P>>>,
16}
17
18impl<P: PackFile> Clone for FileSystem<P> {
19 fn clone(&self) -> Self {
20 Self {
21 root_path: self.root_path.clone(),
22 search_path_dirs: self.search_path_dirs.clone(),
23 search_path_vpks: self.search_path_vpks.clone(),
24 }
25 }
26}
27
28impl<P: PackFile> FileSystem<P> {
29 #[cfg(feature = "steam")]
31 pub fn load_from_app_id<G: GameInfoProvider>(
32 app_id: u32,
33 game_name: &str,
34 options: &FileSystemOptions,
35 ) -> Result<Self, FileSystemError> {
36 let steamdir = steamlocate::SteamDir::locate().map_err(|_| FileSystemError::SteamNotFound)?;
37 let (app, library) = steamdir
38 .find_app(app_id)?
39 .ok_or(FileSystemError::SteamAppNotFound(app_id))?;
40 let game_path = library.resolve_app_dir(&app).join(&game_name);
41
42 Self::load_from_path::<G>(&game_path, options)
43 }
44
45 pub fn load_from_path<G: GameInfoProvider>(
47 game_path: &Path,
48 options: &FileSystemOptions,
49 ) -> Result<Self, FileSystemError> {
50 let gameinfo_path = game_path.join("gameinfo.txt");
51 if !gameinfo_path.is_file() {
52 return Err(FileSystemError::GameInfoNotFound(gameinfo_path));
53 }
54
55 let root_path = game_path.parent()
56 .ok_or_else(|| FileSystemError::InvalidGamePath(game_path.to_path_buf()))?
57 .to_path_buf();
58 let game_id = game_path.file_name()
59 .ok_or_else(|| FileSystemError::InvalidGamePath(game_path.to_path_buf()))?
60 .to_string_lossy()
61 .to_string();
62
63 let mut fs = Self {
64 root_path,
65 search_path_dirs: HashMap::new(),
66 search_path_vpks: HashMap::new(),
67 };
68
69 let search_paths = G::get_search_paths(&gameinfo_path)
70 .ok_or(FileSystemError::GameInfoParseError)?;
71 if search_paths.is_empty() {
72 return Ok(fs);
73 }
74
75 for (i, (key, value)) in search_paths.into_iter().enumerate() {
76 let searches: Vec<String> = key.to_lowercase()
77 .split('+')
78 .map(|s| s.to_string())
79 .collect();
80
81 let mut path = value;if path.ends_with('.') && !path.ends_with("..") {
84 path.pop();
85 }
86 let path = utils::normalize_slashes(&path, false, false);
87
88 if path.ends_with(".vpk") {
89 let mut full_path = fs.root_path.join(&path);
90
91 if !full_path.exists() {
92 if let Some(stem) = full_path.file_stem() {
94 let parent = full_path.parent().unwrap_or_else(|| Path::new(""));
95 let dir_vpk = parent.join(format!("{}_dir.vpk", stem.to_string_lossy()));
96 if dir_vpk.exists() {
97 full_path = dir_vpk;
98 } else {
99 continue;
100 }
101 } else {
102 continue;
103 }
104 }
105
106 if let Some(pack) = P::open(&full_path).map(Arc::new) {
107 for search in &searches {
108 fs.search_path_vpks
109 .entry(search.clone())
110 .or_default()
111 .push(Arc::clone(&pack));
112 }
113 }
114 } else {
115 for search in &searches {
116 if path.ends_with("/*") {
117 let glob_parent_path = fs.root_path.join(&path[..path.len() - 2]);
118 if glob_parent_path.is_dir() {
119 if let Ok(entries) = std::fs::read_dir(&glob_parent_path) {
120 for entry in entries.flatten() {
121 let glob_child_path = utils::normalize_slashes(
122 &entry.path().to_string_lossy(),
123 false,
124 false,
125 );
126 fs.search_path_dirs
127 .entry(search.clone())
128 .or_default()
129 .push(PathBuf::from(glob_child_path));
130 }
131 }
132 }
133 } else {
134 let test_path = fs.root_path.join(&path);
135 if test_path.exists() {
138 fs.search_path_dirs
139 .entry(search.clone())
140 .or_default()
141 .push(PathBuf::from(&path));
142
143 if search == "game" {
145 fs.search_path_dirs
146 .entry("gamebin".to_string())
147 .or_default()
148 .push(PathBuf::from(format!("{}/bin", path)));
149
150 if i == 0 {
151 fs.search_path_dirs
152 .entry("mod".to_string())
153 .or_default()
154 .push(PathBuf::from(&path));
155 }
156 }
157 }
158 }
159 }
160 }
161 }
162
163 let exec_paths = fs.search_path_dirs.entry("executable_path".to_string()).or_default();
165 if let Some(plat) = &options.bin_platform {
166 let plat_path = fs.root_path.join("bin").join(plat);
167 if plat_path.exists() {
168 exec_paths.push(PathBuf::from(format!("bin/{}", plat)));
169 }
170 }
171 exec_paths.push(PathBuf::from("bin"));
172 exec_paths.push(PathBuf::from(""));
173
174 fs.search_path_dirs
175 .entry("platform".to_string())
176 .or_insert_with(|| vec![PathBuf::from("platform")]);
177
178 if let Some(game_paths) = fs.search_path_dirs.get_mut("game") {
179 let platform_buf = PathBuf::from("platform");
180 if !game_paths.contains(&platform_buf) {
181 game_paths.push(platform_buf);
182 }
183 }
184
185 fs.search_path_dirs
186 .entry("default_write_path".to_string())
187 .or_insert_with(|| vec![PathBuf::from(&game_id)]);
188
189 fs.search_path_dirs
190 .entry("logdir".to_string())
191 .or_insert_with(|| vec![PathBuf::from(&game_id)]);
192
193 fs.search_path_dirs
194 .entry("config".to_string())
195 .or_insert_with(|| vec![PathBuf::from("platform/config")]);
196
197 Ok(fs)
198 }
199
200 pub fn root_path(&self) -> &PathBuf {
201 &self.root_path
202 }
203
204 pub fn search_path_dirs(&self) -> &HashMap<String, Vec<PathBuf>> {
205 &self.search_path_dirs
206 }
207
208 pub fn search_path_dirs_mut(&mut self) -> &mut HashMap<String, Vec<PathBuf>> {
209 &mut self.search_path_dirs
210 }
211
212 pub fn search_path_vpks(&self) -> &HashMap<String, Vec<Arc<P>>> {
213 &self.search_path_vpks
214 }
215
216 pub fn search_path_vpks_mut(&mut self) -> &mut HashMap<String, Vec<Arc<P>>> {
217 &mut self.search_path_vpks
218 }
219
220 fn format_asset_path(name: &str, prefix: &str, suffix: &str) -> String {
222 let mut path = String::with_capacity(name.len() + prefix.len() + suffix.len());
223 if !prefix.is_empty() && !name.starts_with(prefix) {
224 path.push_str(prefix);
225 }
226 path.push_str(name);
227 if !suffix.is_empty() && !name.ends_with(suffix) {
228 path.push_str(suffix);
229 }
230
231 path
232 }
233
234 pub fn find_file(&self, file_path: &str, search_path: &str) -> Option<PathBuf> {
235 let file_path_str = utils::normalize_slashes(&file_path.to_lowercase(), true, false);
236 let search_path_str = search_path.to_lowercase();
237
238 if let Some(dirs) = self.search_path_dirs.get(&search_path_str) {
239 for base_path in dirs {
240 let base_dir = self.root_path.join(base_path);
241 if let Some(resolved_path) = utils::resolve_path_case_insensitive(&base_dir, &file_path_str) {
242 return Some(resolved_path);
243 }
244 }
245 }
246
247 None
248 }
249
250 pub fn read(&self, file_path: &str, search_path: &str, prioritize_vpks: bool) -> Option<Vec<u8>> {
252 let file_path_str = utils::normalize_slashes(&file_path.to_lowercase(), true, false);
253 let search_path_str = search_path.to_lowercase();
254
255 if prioritize_vpks {
256 if let Some(data) = self.check_vpks(&search_path_str, &file_path_str) {
257 return Some(data);
258 }
259 }
260
261 if let Some(resolved_path) = self.find_file(&file_path_str, &search_path_str) {
262 if let Ok(data) = std::fs::read(resolved_path) {
263 return Some(data);
264 }
265 }
266
267 if !prioritize_vpks {
268 return self.check_vpks(&search_path_str, &file_path_str);
269 }
270
271 None
272 }
273
274 pub fn read_for_map(
276 &self,
277 map_pack: Option<&P>,
278 file_path: &str,
279 search_path: &str,
280 prioritize_vpks: bool,
281 ) -> Option<Vec<u8>> {
282 if let Some(map) = map_pack {
283 if map.has_entry(file_path) {
284 return map.read_entry(file_path);
285 }
286 }
287 self.read(file_path, search_path, prioritize_vpks)
288 }
289
290 pub fn read_str(&self, file_path: &str, search_path: &str, prioritize_vpks: bool) -> Option<String> {
291 let data = self.read(file_path, search_path, prioritize_vpks)?;
292 String::from_utf8(data).ok()
294 }
295
296 pub fn find_asset(&self, name: &str, prefix: &str, suffix: &str, search_path: &str) -> Option<PathBuf> {
298 let path = Self::format_asset_path(name, prefix, suffix);
299 self.find_file(&path, search_path)
300 }
301
302 pub fn read_asset(&self, name: &str, prefix: &str, suffix: &str, search_path: &str, prioritize_vpks: bool) -> Option<Vec<u8>> {
304 let path = Self::format_asset_path(name, prefix, suffix);
305 self.read(&path, search_path, prioritize_vpks)
306 }
307
308 pub fn read_asset_str(&self, name: &str, prefix: &str, suffix: &str, search_path: &str, prioritize_vpks: bool) -> Option<String> {
310 self.read_asset(name, prefix, suffix, search_path, prioritize_vpks)
311 .and_then(|data| String::from_utf8(data).ok())
312 }
313
314 pub fn read_material(&self, name: &str, search_path: &str, prioritize_vpks: bool) -> Option<Vec<u8>> {
316 self.read_asset(name, "materials/", ".vmt", search_path, prioritize_vpks)
317 }
318
319 pub fn read_material_str(&self, name: &str, search_path: &str, prioritize_vpks: bool) -> Option<String> {
321 self.read_asset_str(name, "materials/", ".vmt", search_path, prioritize_vpks)
322 }
323
324 pub fn read_model(&self, name: &str, search_path: &str, prioritize_vpks: bool) -> Option<Vec<u8>> {
326 self.read_asset(name, "models/", ".mdl", search_path, prioritize_vpks)
327 }
328
329 pub fn read_model_str(&self, name: &str, search_path: &str, prioritize_vpks: bool) -> Option<String> {
331 self.read_asset_str(name, "models/", ".mdl", search_path, prioritize_vpks)
332 }
333
334 pub fn read_sound_str(&self, name: &str, search_path: &str, prioritize_vpks: bool) -> Option<String> {
336 if let Some(data) = self.read_asset_str(name, "sound/", ".wav", search_path, prioritize_vpks) {
338 return Some(data);
339 }
340
341 let clean_name = name.strip_suffix(".wav").unwrap_or(name);
343 self.read_asset_str(clean_name, "sound/", ".mp3", search_path, prioritize_vpks)
344 }
345
346 fn find_in_vpks(&self, search_path: &str, file_path: &str) -> Option<PathBuf> {
347 if let Some(vpks) = self.search_path_vpks.get(search_path) {
348 for vpk in vpks {
349 if vpk.has_entry(file_path) {
350 return Some(PathBuf::from(file_path));
351 }
352 }
353 }
354 None
355 }
356
357 fn check_vpks(&self, search_path: &str, file_path: &str) -> Option<Vec<u8>> {
358 if let Some(vpks) = self.search_path_vpks.get(search_path) {
359 for vpk in vpks {
360 if vpk.has_entry(file_path) {
361 return vpk.read_entry(file_path);
362 }
363 }
364 }
365 None
366 }
367}