Skip to main content

game_detector/
steam.rs

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