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