proton_launch/
steam.rs

1use std::{fs::read_to_string, path::PathBuf};
2
3use keyvalues_parser::Vdf;
4
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum SteamDataError {
9    #[error("Could not locate Steam installation directory")]
10    NoSteamDir,
11    #[error("Could not read libraryfolders.vdf")]
12    NoLibraryFolders,
13    #[error("IO error: {0}")]
14    IOError(#[from] std::io::Error),
15    #[error("VDF parse error: {0}")]
16    KVParser(#[from] Box<keyvalues_parser::error::Error>),
17}
18
19type SteamResult<T> = Result<T, SteamDataError>;
20
21pub struct SteamData {
22    library_folders: LibraryFolders,
23    pub path: PathBuf,
24}
25
26impl SteamData {
27    pub fn new() -> SteamResult<Self> {
28        let dir = Self::locate()?;
29        Self::new_with_path(dir)
30    }
31
32    pub fn new_with_path(path: PathBuf) -> SteamResult<Self> {
33        let mut sd = Self {
34            library_folders: LibraryFolders::EMPTY,
35            path,
36        };
37        sd.init_library_paths()?;
38
39        Ok(sd)
40    }
41
42    fn init_library_paths(&mut self) -> SteamResult<()> {
43        let library_folders_path = self.path.join("steamapps/libraryfolders.vdf");
44        if library_folders_path.is_file() {
45            let content = read_to_string(library_folders_path)?;
46            self.library_folders = LibraryFolders::from_vdf(&content)?;
47            Ok(())
48        } else {
49            Err(SteamDataError::NoLibraryFolders)
50        }
51    }
52
53    /// Locates the Steam installation directory on the filesystem and initializes a `SteamDir` (Linux)
54    ///
55    /// Returns `None` if no Steam installation can be located.
56    fn locate() -> SteamResult<PathBuf> {
57        let home_dir = dirs::home_dir().ok_or(SteamDataError::NoSteamDir)?;
58
59        // Steam's installation location is pretty easy to find on Linux, too, thanks to the symlink in $USER
60
61        // Check for Flatpak steam install
62        let steam_flatpak_path = home_dir.join(".var/app/com.valvesoftware.Steam");
63        if steam_flatpak_path.is_dir() {
64            let steam_flatpak_install_path = steam_flatpak_path.join(".steam/steam");
65            if steam_flatpak_install_path.is_dir() {
66                return Ok(steam_flatpak_install_path);
67            }
68        }
69
70        // Check for Standard steam install
71        let standard_path = home_dir.join(".steam/steam");
72        if standard_path.is_dir() {
73            return Ok(standard_path);
74        }
75
76        Err(SteamDataError::NoSteamDir)
77    }
78
79    pub fn has_app(&self, app_id: u64) -> bool {
80        self.library_folders.has_app(app_id)
81    }
82
83    pub fn get_app_dir(&self, app_id: u64) -> Option<PathBuf> {
84        self.library_folders.get_app_dir(app_id)
85    }
86}
87
88#[derive(Debug, Clone)]
89pub struct LibraryFolders(Vec<LibraryFolder>);
90
91impl LibraryFolders {
92    const EMPTY: Self = Self(Vec::new());
93
94    pub fn from_vdf(vdf: &str) -> SteamResult<Self> {
95        let vdf = Vdf::parse(vdf).map_err(Box::new)?.value;
96        let obj = vdf.get_obj().ok_or(SteamDataError::NoLibraryFolders)?;
97
98        let folders: Vec<_> = obj
99            .iter()
100            .filter(|(key, values)| key.parse::<u32>().is_ok() && values.len() == 1)
101            .filter_map(|(_, values)| {
102                let lfo = values.get(0)?.get_obj()?;
103                let library_folder_string = lfo.get("path")?.get(0)?.get_str()?.to_string();
104                let apps = lfo
105                    .get("apps")?
106                    .iter()
107                    .flat_map(|v| v.get_obj())
108                    .flat_map(|o| o.keys())
109                    .filter_map(|k| k.parse::<u64>().ok())
110                    .collect::<Vec<_>>();
111                let library_folder = PathBuf::from(library_folder_string).join("steamapps");
112                Some(LibraryFolder {
113                    path: library_folder,
114                    apps,
115                })
116            })
117            .collect();
118
119        Ok(Self(folders))
120    }
121
122    pub fn has_app(&self, appid: u64) -> bool {
123        self.0.iter().any(|lf| lf.has_game(appid))
124    }
125
126    pub fn get_app_dir(&self, app_id: u64) -> Option<PathBuf> {
127        let library = self.0.iter().find(|lf| lf.has_game(app_id))?;
128        let manifest_location = library.path.join(format!("appmanifest_{}.acf", app_id));
129        if manifest_location.is_file() {
130            let manifest = read_to_string(manifest_location).ok()?;
131            let vdf = Vdf::parse(&manifest).ok()?;
132            let obj = vdf.value.get_obj()?;
133            let install_dir = obj.get("installdir")?.get(0)?.get_str()?;
134            return Some(library.path.join("common").join(install_dir));
135        }
136
137        None
138    }
139}
140
141#[derive(Debug, Clone)]
142pub struct LibraryFolder {
143    path: PathBuf,
144    apps: Vec<u64>,
145}
146
147impl LibraryFolder {
148    fn has_game(&self, appid: u64) -> bool {
149        self.apps.contains(&appid)
150    }
151}