Skip to main content

source_fs/
providers.rs

1use std::path::{Path, PathBuf};
2
3use crate::{GameInfoProvider, PackFile};
4
5/// A dummy implementation that ignores VPK files.
6/// Forces the FileSystem to read exclusively from physical disk directories.
7#[derive(Debug, Clone, Copy)]
8pub struct DummyVpk;
9
10impl PackFile for DummyVpk {
11    fn open<P: AsRef<Path>>(_path: P) -> Option<Self> {
12        // Always return None to indicate no VPK was loaded.
13        None
14    }
15
16    fn has_entry(&self, _path: &str) -> bool {
17        false
18    }
19
20    fn read_entry(&self, _path: &str) -> Option<Vec<u8>> {
21        None
22    }
23}
24
25/// A basic VDF parser specifically for extracting SearchPaths from gameinfo.txt
26pub struct SimpleGameInfo;
27
28impl GameInfoProvider for SimpleGameInfo {
29    fn get_search_paths<P: AsRef<Path>>(path: P) -> Option<Vec<(String, String)>> {
30        let content = std::fs::read_to_string(&path).ok()?;
31        let path_ref = path.as_ref();
32        let mut paths = Vec::new();
33        let mut in_search_paths = false;
34
35
36        // TODO: EXRREMELY naive parser. use a `source-kv` parser after first release
37        // but it works, so it's good enough for now
38        for line in content.lines() {
39            let line = line.trim().to_lowercase();
40
41            if line.contains("\"searchpaths\"") || line.contains("searchpaths") {
42                in_search_paths = true;
43                continue;
44            }
45
46            if in_search_paths {
47                if line == "}" {
48                    break;
49                }
50
51                if line == "{" || line.is_empty() || line.starts_with("//") {
52                    continue;
53                }
54
55                let parts: Vec<&str> = line
56                    .trim()
57                    .splitn(2, char::is_whitespace)
58                    .map(|s| s.trim_start().trim_matches('"'))
59                    .collect();
60
61                if parts.len() >= 2 {
62                    let resolved = crate::utils::resolve_macro_path(parts[1], path_ref);
63                    paths.push((parts[0].to_string(), resolved.to_string_lossy().to_string()));
64                }
65            }
66        }
67
68        if paths.is_empty() {
69            None
70        } else {
71            Some(paths)
72        }
73    }
74}
75
76/// A GameInfoProvider that also parses `mount.cfg` for additional search paths,
77/// typically found in Source engine games like Garry's Mod or P2CE.
78pub struct SimpleWithMount;
79
80impl GameInfoProvider for SimpleWithMount {
81    fn get_search_paths<P: AsRef<Path>>(path: P) -> Option<Vec<(String, String)>> {
82        let path_ref = path.as_ref();
83        let main_game_dir = path_ref.parent()?;
84
85        let mut res_paths = SimpleGameInfo::get_search_paths(path_ref)?;
86
87        let mount_cfg_path = main_game_dir.join("cfg").join("mount.cfg");
88
89        if let Ok(mount_cfg_content) = std::fs::read_to_string(&mount_cfg_path) {
90            // Naive KV parser for mount.cfg
91            for line in mount_cfg_content.lines() {
92                let line = line.trim();
93                if line.starts_with("//") || line.is_empty() || line == "{" || line == "}" || line.contains("mountcfg") {
94                    continue;
95                }
96
97                let parts: Vec<&str> = line
98                    .splitn(2, |c: char| c.is_whitespace())
99                    .map(|s| s.trim().trim_matches('"'))
100                    .filter(|s| !s.is_empty())
101                    .collect();
102
103                if parts.len() >= 2 {
104                    let mount_path = parts[1];
105                    let mount_game_dir = if Path::new(mount_path).is_absolute() {
106                        PathBuf::from(mount_path)
107                    } else {
108                        main_game_dir.join(mount_path)
109                    };
110
111                    let mount_info_path = mount_game_dir.join("gameinfo.txt");
112                    if mount_info_path.exists() {
113                        if let Some(mounted_paths) = SimpleGameInfo::get_search_paths(&mount_info_path) {
114                            res_paths.extend(mounted_paths);
115                        }
116                    }
117                }
118            }
119        }
120
121        if res_paths.is_empty() {
122            None
123        } else {
124            Some(res_paths)
125        }
126    }
127}
128
129
130/// Portal 2 has a unique feature: DLC folders.
131/// They aren't added to SearchPaths;
132/// the game automatically mounts the content if it exists, incrementing the DLC number
133pub struct P2GameInfo;
134
135impl GameInfoProvider for P2GameInfo {
136    fn get_search_paths<P: AsRef<Path>>(path: P) -> Option<Vec<(String, String)>> {
137
138        // only for portal 2:
139        let path_ref = path.as_ref();
140        let game_path = path_ref.ancestors().nth(2).unwrap_or_else(|| Path::new(""));
141
142        let mut paths: Vec<_> = (1..)
143            .map(|idx| format!("portal2_dlc{}/", idx))
144            .take_while(|dlc_name| game_path.join(dlc_name).exists())
145            .map(|dlc_name| ("game".to_string(), dlc_name))
146            .collect();
147
148        paths.reverse();
149        paths.extend(SimpleGameInfo::get_search_paths(&path)?);
150
151        Some(paths)
152    }
153}
154
155// TODO: Add other unique GameInfoProviders here