From a0964415e34ca9816861a01891962f94e8f4537f Mon Sep 17 00:00:00 2001 From: CosmicHorror Date: Sat, 16 Dec 2023 11:27:13 -0700 Subject: [PATCH] Prepare the v2.0 beta release (#56) * `SteamDir::from_steam_dir` -> `SteamDir::from_dir` * `shortcuts_extra` is now just part of `steamlocate` * Make `app_id` name more consistent * `cargo fmt` * Update the README * Bump version to v2.0.0-beta.0 --- Cargo.toml | 7 +- README.md | 162 +++++++++++++++++------------------------- examples/shortcuts.rs | 2 +- src/lib.rs | 17 +++-- src/shortcut.rs | 71 +++++++++--------- src/tests/helpers.rs | 2 +- 6 files changed, 114 insertions(+), 147 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7526fb0..bcd6cae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "steamlocate" -version = "2.0.0-alpha.1" +version = "2.0.0-beta.0" authors = ["William Venner "] edition = "2018" repository = "https://github.com/WilliamVenner/steamlocate-rs" @@ -19,16 +19,13 @@ rustdoc-args = ["--cfg", "docsrs", "--cfg", "steamlocate_doctest"] [features] default = ["locate"] locate = ["locate_backend"] -shortcuts_extras = ["crc"] [dependencies] +crc = "3.0" keyvalues-parser = "0.2" keyvalues-serde = "0.2" serde = { version = "1.0.0", features = ["derive"] } -# TODO: is this really worth making optional? It should be a really small dep -crc = { version = "3.0", optional = true } - # Custom cfg used to enable a dependency only needed for doctests [target."cfg(steamlocate_doctest)".dependencies] tempfile = "3.8.1" diff --git a/README.md b/README.md index 528a2f9..d9dd57b 100644 --- a/README.md +++ b/README.md @@ -5,127 +5,97 @@ # steamlocate -A crate which efficiently locates any Steam application on the filesystem, and/or the Steam installation itself. +A crate which efficiently locates any Steam application on the filesystem, +and/or the Steam installation itself. -This crate is best used when you do not want to depend on the Steamworks API for your program. In some cases the Steamworks API may be more appropriate to use, in which case I recommend the fantastic [steamworks](https://github.com/Thinkofname/steamworks-rs) crate. You don't need to be a Steamworks partner to get installation directory locations from the Steamworks API. +This crate is best used when you do not want to depend on the Steamworks API +for your program. In some cases the Steamworks API may be more appropriate to +use, in which case I recommend the fantastic +[steamworks](https://github.com/Thinkofname/steamworks-rs) crate. You don't +need to be a Steamworks partner to get installation directory locations from +the Steamworks API. -**This crate supports Windows, macOS and Linux.** +# Using steamlocate -## Using steamlocate -Simply add to your [Cargo.toml](https://doc.rust-lang.org/cargo/reference/manifest.html) file: -```toml -[dependencies] -steamlocate = "0.*" -``` - -To use [steamid-ng](#steamid-ng-support) with steamlocate, add this to your [Cargo.toml](https://doc.rust-lang.org/cargo/reference/manifest.html) file: -```toml -[dependencies] -steamid-ng = "1.*" +Simply add `steamlocate` using +[`cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html). -[dependencies.steamlocate] -version = "0.*" -features = ["steamid_ng"] +```console +$ cargo add steamlocate ``` -## Caching -All functions in this crate cache their results, meaning you can call them as many times as you like and they will always return the same reference. +## Feature flags -If you need to get uncached results, simply instantiate a new [SteamDir](https://docs.rs/steamlocate/*/steamlocate/struct.SteamDir.html). +Default: `locate` -## steamid-ng Support -This crate supports [steamid-ng](https://docs.rs/steamid-ng) and can automatically convert [SteamApp::last_user](struct.SteamApp.html#structfield.last_user) to a [SteamID](https://docs.rs/steamid-ng/*/steamid_ng/struct.SteamID.html) for you. +| Feature flag | Description | +| :---: | :--- | +| `locate` | Enables automatically detecting the Steam installation on supported platforms (currently Windows, MacOS, and Linux). Unsupported platforms will return a runtime error. | -To enable this support, [use the `steamid_ng` Cargo.toml feature](#using-steamlocate). +# Examples -## Examples +## Locate the Steam installation and a specific game -#### Locate the installed Steam directory -```rust -extern crate steamlocate; -use steamlocate::SteamDir; +The `SteamDir` is going to be your entrypoint into _most_ parts of the API. +After you locate it you can access related information. -match SteamDir::locate() { - Some(steamdir) => println!("{:#?}", steamdir), - None => panic!("Couldn't locate Steam on this computer!") -} +```rust,ignore +let steam_dir = steamlocate::SteamDir::locate()?; +println!("Steam installation - {}", steam_dir.path().display()); +// ^^ prints something like `Steam installation - C:\Program Files (x86)\Steam` + +const GMOD_APP_ID: u32 = 4_000; +let (garrys_mod, _lib) = steam_dir + .find_app(GMOD_APP_ID)? + .expect("Of course we have G Mod"); +assert_eq!(garrys_mod.name.as_ref().unwrap(), "Garry's Mod"); +println!("{garrys_mod:#?}"); +// ^^ prints something like vv ``` -```rust -SteamDir ( - path: PathBuf: "C:\\Program Files (x86)\\Steam" -) +```rust,ignore +App { + app_id: 4_000, + install_dir: "GarrysMod", + name: Some("Garry's Mod"), + universe: Some(Public), + // much much more data +} ``` -#### Locate an installed Steam app by its app ID -This will locate Garry's Mod anywhere on the filesystem. -```rust -extern crate steamlocate; -use steamlocate::SteamDir; +## Get an overview of all libraries and apps on the system -let mut steamdir = SteamDir::locate().unwrap(); -match steamdir.app(&4000) { - Some(app) => println!("{:#?}", app), - None => panic!("Couldn't locate Garry's Mod on this computer!") -} -``` -```rust -SteamApp ( - appid: u32: 4000, - path: PathBuf: "C:\\Program Files (x86)\\steamapps\\common\\GarrysMod", - vdf: , - name: Some(String: "Garry's Mod"), - last_user: Some(u64: 76561198040894045) -) -``` +You can iterate over all of Steam's libraries from the steam dir. Then from each library you +can iterate over all of its apps. -#### Locate all Steam apps on this filesystem -```rust -extern crate steamlocate; -use steamlocate::{SteamDir, SteamApp}; -use std::collections::HashMap; +```rust,ignore +let steam_dir = steamlocate::SteamDir::locate()?; -let mut steamdir = SteamDir::locate().unwrap(); -let apps: &HashMap> = steamdir.apps(); +for library in steam_dir.libraries()? { + let library = library?; + println!("Library - {}", library.path().display()); -println!("{:#?}", apps); -``` -```rust -{ - 4000: SteamApp ( - appid: u32: 4000, - path: PathBuf: "C:\\Program Files (x86)\\steamapps\\common\\GarrysMod", - vdf: , - name: Some(String: "Garry's Mod"), - last_user: Some(u64: 76561198040894045) - ) - ... + for app in library.apps() { + let app = app?; + println!(" App {} - {:?}", app.app_id, app.name); + } } ``` -#### Locate all Steam library folders -```rust -extern crate steamlocate; -use steamlocate::{SteamDir, LibraryFolders}; -use std::{vec, path::PathBuf}; - -let mut steamdir: SteamDir = SteamDir::locate().unwrap(); -let libraryfolders: &LibraryFolders = steamdir.libraryfolders(); -let paths: &Vec = &libraryfolders.paths; - -println!("{:#?}", paths); -``` -```rust -{ - "C:\\Program Files (x86)\\Steam\\steamapps", - "D:\\Steam\\steamapps", - "E:\\Steam\\steamapps", - "F:\\Steam\\steamapps", - ... -} +On my laptop this prints + +```text +Library - /home/wintermute/.local/share/Steam + App 1628350 - Steam Linux Runtime 3.0 (sniper) + App 1493710 - Proton Experimental + App 4000 - Garry's Mod +Library - /home/wintermute/temp steam lib + App 391540 - Undertale + App 1714040 - Super Auto Pets + App 2348590 - Proton 8.0 ``` ## Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the MIT license, -shall be dual licensed as above, without any additional terms or conditions. +shall be licensed as above, without any additional terms or conditions. diff --git a/examples/shortcuts.rs b/examples/shortcuts.rs index a070d07..74fe0b4 100644 --- a/examples/shortcuts.rs +++ b/examples/shortcuts.rs @@ -5,7 +5,7 @@ fn main() { println!("Shortcuts:"); for maybe_shortcut in steamdir.shortcuts().unwrap() { match maybe_shortcut { - Ok(shortcut) => println!(" - {} {}", shortcut.appid, shortcut.app_name), + Ok(shortcut) => println!(" - {} {}", shortcut.app_id, shortcut.app_name), Err(err) => println!("Failed reading potential shortcut: {err}"), } } diff --git a/src/lib.rs b/src/lib.rs index 448f6d4..77cbe6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ //! The [`SteamDir`] is going to be your entrypoint into _most_ parts of the API. After you locate //! it you can access related information. //! -//! ```rust +//! ``` //! # /* //! let steam_dir = steamlocate::SteamDir::locate()?; //! # */ @@ -34,7 +34,9 @@ //! // ^^ prints something like `Steam installation - C:\Program Files (x86)\Steam` //! //! const GMOD_APP_ID: u32 = 4_000; -//! let (garrys_mod, _lib) = steam_dir.find_app(GMOD_APP_ID)?.expect("Of course we have G Mod"); +//! let (garrys_mod, _lib) = steam_dir +//! .find_app(GMOD_APP_ID)? +//! .expect("Of course we have G Mod"); //! assert_eq!(garrys_mod.name.as_ref().unwrap(), "Garry's Mod"); //! println!("{garrys_mod:#?}"); //! // ^^ prints something like vv @@ -120,9 +122,14 @@ pub use crate::error::{Error, Result}; pub use crate::library::Library; pub use crate::shortcut::Shortcut; +// Run doctests on the README too +#[doc = include_str!("../README.md")] +#[cfg(doctest)] +pub struct ReadmeDoctests; + /// The entrypoint into most of the rest of the API /// -/// Use either [`SteamDir::locate()`] or [`SteamDir::from_steam_dir()`] to create a new instance. +/// Use either [`SteamDir::locate()`] or [`SteamDir::from_dir()`] to create a new instance. /// From there you have access to: /// /// - The Steam installation directory @@ -219,7 +226,7 @@ impl SteamDir { } // TODO: rename to `from_dir()` and make consitent with similar constructors on other structs - pub fn from_steam_dir(path: &Path) -> Result { + pub fn from_dir(path: &Path) -> Result { if !path.is_dir() { return Err(Error::validation(ValidationError::missing_dir())); } @@ -236,6 +243,6 @@ impl SteamDir { pub fn locate() -> Result { let path = locate::locate_steam_dir()?; - Self::from_steam_dir(&path) + Self::from_dir(&path) } } diff --git a/src/shortcut.rs b/src/shortcut.rs index 202178e..53d2f3b 100644 --- a/src/shortcut.rs +++ b/src/shortcut.rs @@ -19,7 +19,7 @@ use crate::{ #[non_exhaustive] pub struct Shortcut { /// Steam's provided app id - pub appid: u32, + pub app_id: u32, /// The name of the application pub app_name: String, /// The executable used to launch the app @@ -28,20 +28,33 @@ pub struct Shortcut { pub executable: String, /// The directory that the application should be run in pub start_dir: String, + /// The shortcut's Steam ID calculated from the executable path and app name + pub steam_id: u64, } -#[cfg(feature = "shortcuts_extras")] impl Shortcut { /// Calculates the shortcut's Steam ID from the executable and app name - pub fn steam_id(&self) -> u64 { - let algorithm = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + pub fn new(app_id: u32, app_name: String, executable: String, start_dir: String) -> Self { + fn calculate_steam_id(executable: &[u8], app_name: &[u8]) -> u64 { + let algorithm = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); - let mut digest = algorithm.digest(); - digest.update(self.executable.as_bytes()); - digest.update(self.app_name.as_bytes()); + let mut digest = algorithm.digest(); + digest.update(executable); + digest.update(app_name); - let top = digest.finalize() | 0x80000000; - ((top as u64) << 32) | 0x02000000 + let top = digest.finalize() | 0x80000000; + ((top as u64) << 32) | 0x02000000 + } + + let steam_id = calculate_steam_id(executable.as_bytes(), app_name.as_bytes()); + + Self { + app_id, + app_name, + executable, + start_dir, + steam_id, + } } } @@ -180,7 +193,7 @@ fn parse_shortcuts(contents: &[u8]) -> Option> { if !after_many_case_insensitive(&mut it, b"\x02appid\x00") { return Some(shortcuts); } - let appid = parse_value_u32(&mut it)?; + let app_id = parse_value_u32(&mut it)?; if !after_many_case_insensitive(&mut it, b"\x01AppName\x00") { return None; @@ -197,12 +210,7 @@ fn parse_shortcuts(contents: &[u8]) -> Option> { } let start_dir = parse_value_str(&mut it)?; - let shortcut = Shortcut { - appid, - app_name, - executable, - start_dir, - }; + let shortcut = Shortcut::new(app_id, app_name, executable, start_dir); shortcuts.push(shortcut); } } @@ -219,22 +227,25 @@ mod tests { shortcuts, vec![ Shortcut { - appid: 2786274309, + app_id: 2786274309, app_name: "Anki".into(), executable: "\"anki\"".into(), start_dir: "\"./\"".into(), + steam_id: 0xe89614fe02000000, }, Shortcut { - appid: 2492174738, + app_id: 2492174738, app_name: "LibreOffice Calc".into(), executable: "\"libreoffice\"".into(), start_dir: "\"./\"".into(), + steam_id: 0xdb01c79902000000, }, Shortcut { - appid: 3703025501, + app_id: 3703025501, app_name: "foo.sh".into(), executable: "\"/usr/local/bin/foo.sh\"".into(), start_dir: "\"/usr/local/bin/\"".into(), + steam_id: 0x9d55017302000000, } ], ); @@ -244,30 +255,12 @@ mod tests { assert_eq!( shortcuts, vec![Shortcut { - appid: 2931025216, + app_id: 2931025216, app_name: "Second Life".into(), executable: "\"/Applications/Second Life Viewer.app\"".into(), start_dir: "\"/Applications/\"".into(), + steam_id: 0xfdd972df02000000, }] ); } - - #[cfg_attr( - not(feature = "shortcuts_extras"), - ignore = "Needs `shortcuts_extras` feature" - )] - #[test] - fn shortcuts_extras() { - #[cfg(not(feature = "shortcuts_extras"))] - unreachable!(); - #[cfg(feature = "shortcuts_extras")] - { - let contents = include_bytes!("../tests/sample_data/shortcuts.vdf"); - let shortcuts = parse_shortcuts(contents).unwrap(); - let ideal_ids = vec![0xe89614fe02000000, 0xdb01c79902000000, 0x9d55017302000000]; - for (id, shortcut) in ideal_ids.into_iter().zip(shortcuts.iter()) { - assert_eq!(id, shortcut.steam_id()); - } - } - } } diff --git a/src/tests/helpers.rs b/src/tests/helpers.rs index a2eb940..6982956 100644 --- a/src/tests/helpers.rs +++ b/src/tests/helpers.rs @@ -96,7 +96,7 @@ impl TempSteamDirBuilder { .collect(); Ok(TempSteamDir { - steam_dir: SteamDir::from_steam_dir(&steam_dir)?, + steam_dir: SteamDir::from_dir(&steam_dir)?, _tmps: tmps, }) }