aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/steam/proton.rs
diff options
context:
space:
mode:
Diffstat (limited to 'libs/nak/src/steam/proton.rs')
-rw-r--r--libs/nak/src/steam/proton.rs257
1 files changed, 257 insertions, 0 deletions
diff --git a/libs/nak/src/steam/proton.rs b/libs/nak/src/steam/proton.rs
new file mode 100644
index 0000000..064fd96
--- /dev/null
+++ b/libs/nak/src/steam/proton.rs
@@ -0,0 +1,257 @@
+//! Steam Proton detection and management
+//!
+//! Finds Protons that Steam can see and use for non-Steam games.
+//! This includes Steam's built-in Protons and custom Protons in compatibilitytools.d.
+
+use std::fs;
+use std::path::PathBuf;
+
+use super::find_steam_path;
+
+/// Information about an installed Proton version
+#[derive(Debug, Clone)]
+pub struct SteamProton {
+ /// Display name (e.g., "GE-Proton9-20", "Proton Experimental")
+ pub name: String,
+ /// Internal name used in config.vdf (e.g., "proton_experimental", "GE-Proton9-20")
+ pub config_name: String,
+ /// Full path to the Proton installation
+ pub path: PathBuf,
+ /// Whether this is a Steam-provided Proton (vs custom)
+ pub is_steam_proton: bool,
+ /// Whether this is Proton Experimental
+ pub is_experimental: bool,
+}
+
+impl SteamProton {
+ /// Get the path to the wine binary.
+ pub fn wine_binary(&self) -> Option<PathBuf> {
+ let paths = [
+ self.path.join("files/bin/wine"),
+ self.path.join("dist/bin/wine"),
+ ];
+ paths.into_iter().find(|p| p.exists())
+ }
+
+ /// Get the path to the wineserver binary.
+ pub fn wineserver_binary(&self) -> Option<PathBuf> {
+ let paths = [
+ self.path.join("files/bin/wineserver"),
+ self.path.join("dist/bin/wineserver"),
+ ];
+ paths.into_iter().find(|p| p.exists())
+ }
+
+ /// Get the bin directory containing wine executables.
+ pub fn bin_dir(&self) -> Option<PathBuf> {
+ self.wine_binary().and_then(|p| p.parent().map(|p| p.to_path_buf()))
+ }
+}
+
+/// Find all Protons that Steam can use (Proton 10+ only)
+pub fn find_steam_protons() -> Vec<SteamProton> {
+ let mut protons = Vec::new();
+
+ let Some(steam_path) = find_steam_path() else {
+ return protons;
+ };
+
+ let is_flatpak = steam_path.to_string_lossy().contains(".var/app/com.valvesoftware.Steam");
+
+ // 1. Steam's built-in Protons (steamapps/common/Proton*)
+ protons.extend(find_builtin_protons(&steam_path));
+
+ // 2. Custom Protons in user's compatibilitytools.d
+ protons.extend(find_custom_protons(&steam_path));
+
+ // 3. System-level Protons in /usr/share/steam/compatibilitytools.d/
+ if is_flatpak {
+ crate::logging::log_info("Flatpak Steam detected - skipping system protons in /usr/share/steam/compatibilitytools.d/");
+ } else {
+ protons.extend(find_system_protons());
+ }
+
+ // Filter to only include Proton 10+ (required for Steam-native integration)
+ protons.retain(is_proton_10_or_newer);
+
+ // Filter to only include Protons with valid wine binaries
+ protons.retain(|p| {
+ let has_wine = p.wine_binary().is_some();
+ if !has_wine {
+ crate::logging::log_warning(&format!(
+ "Skipping Proton '{}': wine binary not found at expected paths (files/bin/wine or dist/bin/wine)",
+ p.name
+ ));
+ }
+ has_wine
+ });
+
+ // Sort: Experimental first, then by name descending (newest first)
+ protons.sort_by(|a, b| {
+ if a.is_experimental != b.is_experimental {
+ return b.is_experimental.cmp(&a.is_experimental);
+ }
+ b.name.cmp(&a.name)
+ });
+
+ protons
+}
+
+/// Check if a Proton version is 10 or newer
+fn is_proton_10_or_newer(proton: &SteamProton) -> bool {
+ let name = &proton.name;
+
+ if proton.is_experimental || name.contains("Experimental") {
+ return true;
+ }
+
+ if name.contains("CachyOS") {
+ return true;
+ }
+
+ if name == "LegacyRuntime" || name.contains("Runtime") {
+ return false;
+ }
+
+ if name.starts_with("GE-Proton") {
+ if let Some(version_part) = name.strip_prefix("GE-Proton") {
+ let major: Option<u32> = version_part
+ .split('-')
+ .next()
+ .and_then(|s| s.parse().ok());
+ return major.map(|v| v >= 10).unwrap_or(false);
+ }
+ }
+
+ if name.starts_with("Proton ") {
+ if let Some(version_part) = name.strip_prefix("Proton ") {
+ let major: Option<u32> = version_part
+ .split('.')
+ .next()
+ .and_then(|s| s.parse().ok());
+ return major.map(|v| v >= 10).unwrap_or(false);
+ }
+ }
+
+ if name.starts_with("EM-") {
+ if let Some(version_part) = name.strip_prefix("EM-") {
+ let major: Option<u32> = version_part
+ .split('.')
+ .next()
+ .and_then(|s| s.parse().ok());
+ return major.map(|v| v >= 10).unwrap_or(false);
+ }
+ }
+
+ // Unknown format - allow it
+ true
+}
+
+/// Find Steam's built-in Proton versions
+fn find_builtin_protons(steam_path: &std::path::Path) -> Vec<SteamProton> {
+ let mut found = Vec::new();
+ let common_dir = steam_path.join("steamapps/common");
+
+ let Ok(entries) = fs::read_dir(&common_dir) else {
+ return found;
+ };
+
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+
+ let name = entry.file_name().to_string_lossy().to_string();
+
+ if name.starts_with("Proton") && path.join("proton").exists() {
+ let is_experimental = name.contains("Experimental");
+
+ let config_name = if is_experimental {
+ "proton_experimental".to_string()
+ } else {
+ let version = name.replace("Proton ", "");
+ let major = version.split('.').next().unwrap_or(&version);
+ format!("proton_{}", major)
+ };
+
+ found.push(SteamProton {
+ name: name.clone(),
+ config_name,
+ path,
+ is_steam_proton: true,
+ is_experimental,
+ });
+ }
+ }
+
+ found
+}
+
+/// Find custom Protons in compatibilitytools.d
+fn find_custom_protons(steam_path: &std::path::Path) -> Vec<SteamProton> {
+ let mut found = Vec::new();
+ let compat_dir = steam_path.join("compatibilitytools.d");
+
+ let Ok(entries) = fs::read_dir(&compat_dir) else {
+ return found;
+ };
+
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+
+ let name = entry.file_name().to_string_lossy().to_string();
+
+ let has_proton = path.join("proton").exists();
+ let has_vdf = path.join("compatibilitytool.vdf").exists();
+
+ if has_proton || has_vdf {
+ found.push(SteamProton {
+ name: name.clone(),
+ config_name: name.clone(),
+ path,
+ is_steam_proton: false,
+ is_experimental: false,
+ });
+ }
+ }
+
+ found
+}
+
+/// Find system-level Protons in /usr/share/steam/compatibilitytools.d/
+fn find_system_protons() -> Vec<SteamProton> {
+ let mut found = Vec::new();
+ let system_compat_dir = PathBuf::from("/usr/share/steam/compatibilitytools.d");
+
+ let Ok(entries) = fs::read_dir(&system_compat_dir) else {
+ return found;
+ };
+
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+
+ let name = entry.file_name().to_string_lossy().to_string();
+
+ let has_proton = path.join("proton").exists();
+ let has_vdf = path.join("compatibilitytool.vdf").exists();
+
+ if has_proton || has_vdf {
+ found.push(SteamProton {
+ name: name.clone(),
+ config_name: name.clone(),
+ path,
+ is_steam_proton: false,
+ is_experimental: false,
+ });
+ }
+ }
+
+ found
+}