ck3_history_extractor/
steam.rs

1use 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
11/// The Steam ID for Crusader Kings III.
12/// Source: [SteamDB](https://steamdb.info/app/1158310/)
13const CK3_ID: &str = "1158310";
14
15// constant literal, but I dont like repeated data
16const 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
28/// The default path from the Steam directory to the libraryfolders.vdf file.
29const DEFAULT_VDF_PATH: &str = "libraryfolders.vdf";
30
31const MOD_PATH: &str = "workshop/content/";
32
33/// The default path from the library to the CK3 directory.
34pub const CK3_PATH: &str = "common/Crusader Kings III/game";
35
36#[derive(Debug, Display)]
37pub enum SteamError {
38    /// The Steam directory was not found.
39    #[display("steam directory not found")]
40    SteamDirNotFound,
41    /// The VDF file was not found.
42    #[display("VDF file not found")]
43    VdfNotFound,
44    /// An error occurred while parsing the VDF file.
45    #[display("library error parsing VDF file: {_0}")]
46    VdfParseError(keyvalues_parser::error::Error),
47    /// An error occurred while processing the VDF file.
48    #[display("error processing VDF file: {_0}")]
49    VdfProcessingError(&'static str),
50    /// The CK3 directory was not found. So it was in the manifest, but not in the library.
51    #[display("CK3 directory not found")]
52    Ck3NotFound,
53    /// According to the internal manifest, there is no CK3 installation.
54    #[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
67/// Returns the path to the Steam directory.
68/// If the Steam directory is not found, it will return an error.
69fn 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            // home_dir is deprecated, because Windows is bad, but we don't care since we are only using it for Linux
74            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
88/// Get the path to the Steam library that has CK3 installed.
89///
90/// # Errors
91///
92/// This function shouldn't panic, but it can return an error.
93/// Generally speaking error checking regarding VDF expected format is not performed.
94/// So if the VDF file is weird, then [SteamError::CK3Missing] will be returned.
95///
96/// # Returns
97///
98/// The path to the Steam library that has CK3 installed.
99pub 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            // vdf was parsed successfully
109            if let Value::Obj(folders) = vdf.value {
110                // root of the VDF file is an object
111                for folder_objs in folders.values() {
112                    // foreach value set in the root object
113                    for folder in folder_objs {
114                        // foreach value in the value set
115                        if let Value::Obj(folder) = folder {
116                            // if the value is an object
117                            if let Some(apps_objs) = folder.get("apps") {
118                                // if the object has an "apps" key
119                                for app in apps_objs {
120                                    // foreach value in the "apps" object
121                                    if let Value::Obj(app) = app {
122                                        // if the value is an object
123                                        if app.keys().any(|k| k == CK3_ID) {
124                                            // if the object has a key with the CK3 ID
125                                            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                                // we could error here, but what's the point?
144                                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}