game_detector/
steam.rs

1use fs_err::File;
2use log::{error, warn};
3use serde::Deserialize;
4use std::collections::HashMap;
5use std::path::Path;
6
7#[derive(Debug, Clone, Deserialize)]
8#[serde(rename = "libraryfolders")]
9pub struct LibraryFolders(pub HashMap<usize, LibraryFolder>);
10
11#[derive(Debug, Clone, Deserialize)]
12pub struct LibraryFolder {
13    pub path: String,
14    pub label: String,
15    pub contentid: i64,
16    pub totalsize: usize,
17    pub update_clean_bytes_tally: Option<usize>,
18    pub time_last_update_corruption: Option<u64>,
19    pub apps: HashMap<u32, usize>,
20}
21
22// cohae: Yikes Valve.
23#[derive(Debug, Clone, Deserialize)]
24#[allow(non_snake_case)]
25pub struct AppState {
26    pub appid: u64,
27    // pub universe: u64,
28    pub LauncherPath: Option<String>,
29    pub name: String,
30    pub StateFlags: u64,
31    pub installdir: String,
32    pub LastUpdated: Option<u64>,
33    pub SizeOnDisk: usize,
34    pub StagingSize: Option<usize>,
35    pub buildid: u64,
36    pub LastOwner: Option<u64>,
37    pub UpdateResult: Option<u64>,
38    pub BytesToDownload: Option<usize>,
39    pub BytesDownloaded: Option<usize>,
40    pub BytesToStage: Option<usize>,
41    pub BytesStaged: Option<usize>,
42    pub TargetBuildID: Option<usize>,
43    pub AutoUpdateBehavior: Option<u64>,
44    // pub AllowOtherDownloadsWhileRunning: bool,
45    pub ScheduledAutoUpdate: Option<u64>,
46    pub InstalledDepots: HashMap<u64, InstalledDepot>,
47    pub SharedDepots: Option<HashMap<u64, u64>>,
48    // pub UserConfig: AppConfig,
49    // pub MountedConfig: AppConfig,
50    /// Base library path (eg. D:/Steam/)
51    #[serde(skip)]
52    pub library_path: String,
53
54    /// Full game path (eg. D:/Steam/steamapps/common/Team Fortress 2/
55    #[serde(skip)]
56    pub game_path: String,
57}
58
59#[derive(Debug, Clone, Deserialize)]
60pub struct InstalledDepot {
61    pub manifest: u64,
62    pub size: usize,
63}
64
65// #[derive(Debug, Clone, Deserialize)]
66// pub struct AppConfig {
67//     pub language: String,
68// }
69
70#[cfg(not(any(windows, unix)))]
71fn get_steam_path() -> anyhow::Result<String> {
72    anyhow::bail!("Not supported on this platform")
73}
74
75#[cfg(unix)]
76fn get_steam_path() -> anyhow::Result<String> {
77    use anyhow::Context;
78
79    let home = std::env::var("HOME").context("HOME environment variable not set")?;
80    Ok(Path::new(&home)
81        .join(Path::new(".steam/steam"))
82        .to_str()
83        .unwrap()
84        .to_string())
85}
86
87#[cfg(windows)]
88fn get_steam_path_reg(root: winreg::HKEY) -> anyhow::Result<String> {
89    const STEAM_REGKEY_PATH: &str = "SOFTWARE\\Valve\\Steam";
90
91    let hkcu = winreg::RegKey::predef(root);
92    let steam_key = hkcu.open_subkey(STEAM_REGKEY_PATH)?;
93    let path: String = steam_key.get_value("SteamPath")?;
94    Ok(path)
95}
96
97#[cfg(windows)]
98fn get_steam_path() -> anyhow::Result<String> {
99    get_steam_path(winreg::enums::HKEY_CURRENT_USER)
100        .or_else(|_| get_steam_path(winreg::enums::HKEY_LOCAL_MACHINE))
101}
102
103pub fn get_all_apps() -> anyhow::Result<Vec<AppState>> {
104    use anyhow::Context;
105    use log::debug;
106
107    let steam_path = get_steam_path().context("Failed to find Steam installation path")?;
108
109    debug!("Using Steam path: {steam_path}");
110
111    let vdf_path = Path::new(&steam_path).join("config/libraryfolders.vdf");
112
113    let mut apps = vec![];
114    let folders: LibraryFolders = keyvalues_serde::from_reader(File::open(vdf_path)?)?;
115    for f in folders.0.values() {
116        let steamapps_path = Path::new(&f.path).join("steamapps");
117        for &app_id in f.apps.keys() {
118            let appmanifest_path = steamapps_path.join(format!("appmanifest_{app_id}.acf"));
119            match File::open(&appmanifest_path).map(keyvalues_serde::from_reader::<_, AppState>) {
120                Ok(a) => match a {
121                    Ok(mut a) => {
122                        a.library_path = f.path.clone();
123                        a.game_path = steamapps_path
124                            .join("common")
125                            .join(&a.installdir)
126                            .to_string_lossy()
127                            .to_string();
128                        apps.push(a);
129                    }
130                    Err(e) => {
131                        error!(
132                            "Failed to read appmanifest {}: {e}",
133                            appmanifest_path.display()
134                        );
135                    }
136                },
137                Err(e) => {
138                    // cohae: Sometimes happens after uninstalling an app, so doesn't have to be an error
139                    warn!(
140                        "Failed to open appmanifest {}: {e}",
141                        appmanifest_path.display()
142                    );
143                }
144            }
145        }
146    }
147
148    Ok(apps)
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_appstate_serde() {
157        const MANIFEST_DATA: &str = r#"
158"AppState"
159{
160	"appid"		"70"
161	"universe"		"1"
162	"LauncherPath"		"C:\\Program Files (x86)\\Steam\\steam.exe"
163	"name"		"Half-Life"
164	"StateFlags"		"6"
165	"installdir"		"Half-Life"
166	"LastUpdated"		"1703587250"
167	"SizeOnDisk"		"589449723"
168	"StagingSize"		"0"
169	"buildid"		"13032868"
170	"LastOwner"		"76561198166639473"
171	"UpdateResult"		"0"
172	"BytesToDownload"		"42478352"
173	"BytesDownloaded"		"0"
174	"BytesToStage"		"127625842"
175	"BytesStaged"		"0"
176	"TargetBuildID"		"13032868"
177	"AutoUpdateBehavior"		"0"
178	"AllowOtherDownloadsWhileRunning"		"0"
179	"ScheduledAutoUpdate"		"1706853353"
180	"InstalledDepots"
181	{
182		"1"
183		{
184			"manifest"		"6665583105370934040"
185			"size"		"513399487"
186		}
187		"3"
188		{
189			"manifest"		"6081070194444336449"
190			"size"		"893958"
191		}
192		"71"
193		{
194			"manifest"		"5133329123964362030"
195			"size"		"16416909"
196		}
197		"96"
198		{
199			"manifest"		"6298465564582633871"
200			"size"		"9067684"
201		}
202		"2"
203		{
204			"manifest"		"3124227209284380614"
205			"size"		"49671685"
206		}
207	}
208	"SharedDepots"
209	{
210		"228988"		"228980"
211	}
212	"UserConfig"
213	{
214		"language"		"english"
215	}
216	"MountedConfig"
217	{
218		"language"		"english"
219	}
220}
221"#;
222
223        let app_state: AppState =
224            keyvalues_serde::from_str(MANIFEST_DATA).expect("Failed to parse app manifest data");
225
226        assert_eq!(app_state.appid, 70);
227        // assert_eq!(app_state.universe, 1);
228        assert_eq!(
229            app_state.LauncherPath,
230            Some("C:\\Program Files (x86)\\Steam\\steam.exe".to_string())
231        );
232        assert_eq!(app_state.name, "Half-Life");
233        assert_eq!(app_state.StateFlags, 6);
234        assert_eq!(app_state.installdir, "Half-Life");
235        assert_eq!(app_state.LastUpdated, Some(1703587250));
236        assert_eq!(app_state.SizeOnDisk, 589449723);
237        assert_eq!(app_state.StagingSize, Some(0));
238        assert_eq!(app_state.buildid, 13032868);
239        assert_eq!(app_state.LastOwner, Some(76561198166639473));
240        assert_eq!(app_state.UpdateResult, Some(0));
241        assert_eq!(app_state.BytesToDownload, Some(42478352));
242        assert_eq!(app_state.BytesDownloaded, Some(0));
243        assert_eq!(app_state.BytesToStage, Some(127625842));
244        assert_eq!(app_state.BytesStaged, Some(0));
245        assert_eq!(app_state.TargetBuildID, Some(13032868));
246        assert_eq!(app_state.AutoUpdateBehavior, Some(0));
247        // assert!(!app_state.AllowOtherDownloadsWhileRunning);
248        assert_eq!(app_state.ScheduledAutoUpdate, Some(1706853353));
249
250        assert_eq!(app_state.InstalledDepots.len(), 5);
251        // assert_eq!(app_state.UserConfig.language, "english");
252    }
253
254    #[test]
255    fn test_libraryfolders_serde() {
256        const MANIFEST_DATA: &str = r#"
257"libraryfolders"
258{
259	"0"
260	{
261		"path"		"C:\\Program Files (x86)\\Steam"
262		"label"		""
263		"contentid"		"3328371409298419016"
264		"totalsize"		"0"
265		"update_clean_bytes_tally"		"131786642906"
266		"time_last_update_corruption"		"0"
267		"apps"
268		{
269			"228980"		"747619496"
270			"250820"		"5464658003"
271			"365670"		"1174137444"
272			"629730"		"9384044754"
273			"992490"		"85670250"
274			"1009850"		"79179350"
275			"1068820"		"825086515"
276			"1826330"		"274110"
277		}
278	}
279	"1"
280	{
281		"path"		"D:\\Steam"
282		"label"		""
283		"contentid"		"1039182383252157525"
284		"totalsize"		"2000397791232"
285		"update_clean_bytes_tally"		"133903725466"
286		"time_last_update_corruption"		"0"
287		"apps"
288		{
289			"70"		"589449723"
290			"240"		"4628887753"
291			"440"		"28179155955"
292			"620"		"12753876784"
293		}
294	}
295}
296"#;
297
298        let folders: LibraryFolders =
299            keyvalues_serde::from_str(MANIFEST_DATA).expect("Failed to parse app manifest data");
300
301        assert_eq!(folders.0.len(), 2);
302    }
303}