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 fn locate() -> SteamResult<PathBuf> {
57 let home_dir = dirs::home_dir().ok_or(SteamDataError::NoSteamDir)?;
58
59 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 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}