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.
7pub struct DummyVpk;
8
9impl PackFile for DummyVpk {
10    fn open<P: AsRef<Path>>(_path: P) -> Option<Self> {
11        // Always return None to indicate no VPK was loaded.
12        None
13    }
14
15    fn has_entry(&self, _path: &str) -> bool {
16        false
17    }
18
19    fn read_entry(&self, _path: &str) -> Option<Vec<u8>> {
20        None
21    }
22}
23
24/// A basic VDF parser specifically for extracting SearchPaths from gameinfo.txt
25pub struct SimpleGameInfo;
26
27impl GameInfoProvider for SimpleGameInfo {
28    fn get_search_paths<P: AsRef<Path>>(path: P) -> Option<Vec<(String, String)>> {
29        let content = std::fs::read_to_string(&path).ok()?;
30        let mut paths = Vec::new();
31        let mut in_search_paths = false;
32
33        // TODO: EXRREMELY naive parser. use a `source-kv` parser after first release
34        // but it works, so it's good enough for now
35        for line in content.lines() {
36            let line = line.trim().to_lowercase();
37
38            if line.contains("\"searchpaths\"") || line.contains("searchpaths") {
39                in_search_paths = true;
40                continue;
41            }
42
43            if in_search_paths {
44                if line == "}" {
45                    break;
46                }
47
48                if line == "{" || line.is_empty() || line.starts_with("//") {
49                    continue;
50                }
51
52                let parts: Vec<&str> = line
53                    .trim()
54                    .splitn(2, char::is_whitespace)
55                    .map(|s| s.trim_start().trim_matches('"'))
56                    .collect();
57
58                if parts.len() >= 2 {
59                    paths.push((parts[0].to_string(), parts[1].to_string()));
60                }
61            }
62        }
63
64        if paths.is_empty() {
65            None
66        } else {
67            Some(paths)
68        }
69    }
70}
71
72/// Portal 2 has a unique feature: DLC folders.
73/// They aren't added to SearchPaths;
74/// the game automatically mounts the content if it exists, incrementing the DLC number
75pub struct P2GameInfo;
76
77impl GameInfoProvider for P2GameInfo {
78    fn get_search_paths<P: AsRef<Path>>(path: P) -> Option<Vec<(String, String)>> {
79        let mut paths = vec![];
80
81        // only for portal 2:
82        let game_path_str = path.as_ref()
83            .ancestors()
84            .nth(2)
85            .map(|p| p.to_string_lossy().into_owned())
86            .unwrap_or_default();
87        let game_path = PathBuf::from(&game_path_str);
88
89        for idx in 1..99 {
90            let dlc_name = format!("portal2_dlc{}/", idx);
91            let dlc_path = game_path.join(&dlc_name);
92            if !dlc_path.exists() {
93                break
94            }
95            // paths.push(("game".to_string(), dlc_path.to_string_lossy().into_owned()));
96            paths.push(("game".to_string(), dlc_name));
97        }
98
99        paths.reverse();
100        paths.extend(SimpleGameInfo::get_search_paths(&path)?);
101
102        Some(paths)
103    }
104}
105
106// TODO: Add other unique GameInfoProviders here