ck3_history_extractor/
steam.rs1use std::{
2 env, error,
3 fmt::{self, Debug, Display},
4 fs::{read_dir, read_to_string},
5 path::{Path, PathBuf},
6};
7
8use derive_more::Display;
9use keyvalues_parser::{Value, Vdf};
10
11const CK3_ID: &str = "1158310";
14
15const APPS_PATH: &str = "steamapps";
17
18#[cfg(target_os = "linux")]
19const DEFAULT_STEAM_PATH: [&str; 2] = [
20 ".local/share/Steam/",
21 ".var/app/com.valvesoftware.Steam/.local/share/Steam/",
22];
23#[cfg(target_os = "windows")]
24const DEFAULT_STEAM_PATH: [&str; 1] = ["C:\\Program Files (x86)\\Steam\\"];
25#[cfg(target_os = "macos")]
26const DEFAULT_STEAM_PATH: [&str; 1] = ["Library/Application Support/Steam/"];
27
28const DEFAULT_VDF_PATH: &str = "libraryfolders.vdf";
30
31const MOD_PATH: &str = "workshop/content/";
32
33pub const CK3_PATH: &str = "common/Crusader Kings III/game";
35
36#[derive(Debug, Display)]
37pub enum SteamError {
38 #[display("steam directory not found")]
40 SteamDirNotFound,
41 #[display("VDF file not found")]
43 VdfNotFound,
44 #[display("library error parsing VDF file: {_0}")]
46 VdfParseError(keyvalues_parser::error::Error),
47 #[display("error processing VDF file: {_0}")]
49 VdfProcessingError(&'static str),
50 #[display("CK3 directory not found")]
52 Ck3NotFound,
53 #[display("CK3 missing from library")]
55 CK3Missing,
56}
57
58impl error::Error for SteamError {
59 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
60 match self {
61 SteamError::VdfParseError(e) => Some(e),
62 _ => None,
63 }
64 }
65}
66
67fn get_steam_path() -> Result<PathBuf, SteamError> {
70 for path in DEFAULT_STEAM_PATH.iter() {
71 let mut steam_path = if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
72 #[allow(deprecated)]
73 env::home_dir().unwrap().join(path)
75 } else {
76 Path::new(path).to_path_buf()
77 };
78
79 steam_path.push(APPS_PATH);
80
81 if steam_path.is_dir() {
82 return Ok(steam_path.to_path_buf());
83 }
84 }
85 Err(SteamError::SteamDirNotFound)
86}
87
88pub fn get_library_path() -> Result<PathBuf, SteamError> {
100 let vdf_path = get_steam_path()?.join(DEFAULT_VDF_PATH);
101 if !vdf_path.exists() {
102 return Err(SteamError::VdfNotFound);
103 }
104 let mut library_path = None;
105 let vdf_contents = read_to_string(&vdf_path).unwrap();
106 match Vdf::parse(&vdf_contents) {
107 Ok(vdf) => {
108 if let Value::Obj(folders) = vdf.value {
110 for folder_objs in folders.values() {
112 for folder in folder_objs {
114 if let Value::Obj(folder) = folder {
116 if let Some(apps_objs) = folder.get("apps") {
118 for app in apps_objs {
120 if let Value::Obj(app) = app {
122 if app.keys().any(|k| k == CK3_ID) {
124 if let Some(path) = folder.get("path") {
126 let path = path.get(0).unwrap();
127 if let Value::Str(path) = path {
128 library_path = Some(path.to_owned());
129 break;
130 } else {
131 return Err(SteamError::VdfProcessingError(
132 "Path is not a string",
133 ));
134 }
135 }
136 }
137 }
138 }
139 if library_path.is_some() {
140 break;
141 }
142 } else {
143 continue;
145 }
146 } else {
147 continue;
148 }
149 }
150 if library_path.is_some() {
151 break;
152 }
153 }
154 } else {
155 return Err(SteamError::VdfProcessingError(
156 "Root of VDF file is not an object",
157 ));
158 }
159 }
160 Err(e) => {
161 return Err(SteamError::VdfParseError(e));
162 }
163 }
164 if let Some(library_path) = library_path {
165 let lib_path = Path::new(library_path.as_ref()).join(APPS_PATH);
166 if lib_path.exists() {
167 Ok(lib_path)
168 } else {
169 Err(SteamError::Ck3NotFound)
170 }
171 } else {
172 return Err(SteamError::CK3Missing);
173 }
174}
175
176pub fn get_game_path(library_path: &PathBuf) -> Result<PathBuf, SteamError> {
177 let ck3_path = library_path.join(CK3_PATH);
178 if ck3_path.exists() {
179 Ok(ck3_path)
180 } else {
181 Err(SteamError::Ck3NotFound)
182 }
183}
184
185pub struct ModDescriptor {
186 name: String,
187 path: PathBuf,
188}
189
190impl Display for ModDescriptor {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 write!(f, "\"{}\" at {}", self.name, self.path.display())
193 }
194}
195
196impl ModDescriptor {
197 fn new(name: String, path: PathBuf) -> Self {
198 ModDescriptor { name, path }
199 }
200}
201
202impl AsRef<PathBuf> for ModDescriptor {
203 fn as_ref(&self) -> &PathBuf {
204 &self.path
205 }
206}
207
208pub fn get_mod_paths(
209 library_path: &PathBuf,
210 out: &mut Vec<ModDescriptor>,
211) -> Result<(), SteamError> {
212 let mut mods_path = library_path.join(MOD_PATH);
213 mods_path.push(CK3_ID);
214 if mods_path.exists() {
215 if let Ok(dir) = read_dir(&mods_path) {
216 for mod_folder in dir {
217 if let Ok(mod_folder) = mod_folder {
218 if !mod_folder.file_type().unwrap().is_dir() {
219 continue;
220 }
221 let mod_path = mod_folder.path();
222 if let Ok(descriptor_contents) = read_to_string(mod_path.join("descriptor.mod"))
223 {
224 for line in descriptor_contents.lines() {
225 if line.starts_with("name") {
226 let name = line.split('=').nth(1).unwrap().trim();
227 let name = name.trim_matches('"').to_string();
228 out.push(ModDescriptor::new(name, mod_path));
229 break;
230 }
231 }
232 }
233 }
234 }
235 }
236 } else {
237 return Err(SteamError::Ck3NotFound);
238 }
239 Ok(())
240}