mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-24 20:32:51 -05:00
723 lines
25 KiB
Rust
723 lines
25 KiB
Rust
use log::{debug, error, info, warn};
|
|
use regex::Regex;
|
|
use std::collections::HashSet;
|
|
use std::fs;
|
|
use std::io::Read;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
use tokio::sync::mpsc;
|
|
use walkdir::WalkDir;
|
|
|
|
// Game information structure
|
|
#[derive(Debug, Clone)]
|
|
pub struct GameInfo {
|
|
pub id: String,
|
|
pub title: String,
|
|
pub path: PathBuf,
|
|
pub native: bool,
|
|
pub api_files: Vec<String>,
|
|
pub cream_installed: bool,
|
|
pub smoke_installed: bool,
|
|
}
|
|
|
|
// Find potential Steam installation directories
|
|
pub fn get_default_steam_paths() -> Vec<PathBuf> {
|
|
let mut paths = Vec::new();
|
|
|
|
// Get user's home directory
|
|
if let Ok(home) = std::env::var("HOME") {
|
|
info!("Searching for Steam in home directory: {}", home);
|
|
|
|
// Common Steam installation locations on Linux
|
|
let common_paths = [
|
|
".steam/steam", // Steam symlink directory
|
|
".steam/root", // Alternative symlink
|
|
".local/share/Steam", // Flatpak Steam installation
|
|
".var/app/com.valvesoftware.Steam/.local/share/Steam", // Flatpak container path
|
|
".var/app/com.valvesoftware.Steam/data/Steam", // Alternative Flatpak path
|
|
"/run/media/mmcblk0p1", // Removable Storage path
|
|
];
|
|
|
|
for path in &common_paths {
|
|
let full_path = PathBuf::from(&home).join(path);
|
|
if full_path.exists() {
|
|
debug!("Found Steam directory: {}", full_path.display());
|
|
paths.push(full_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add Steam Deck paths if they exist
|
|
let deck_paths = ["/home/deck/.steam/steam", "/home/deck/.local/share/Steam"];
|
|
|
|
for path in &deck_paths {
|
|
let p = PathBuf::from(path);
|
|
if p.exists() && !paths.contains(&p) {
|
|
debug!("Found Steam Deck path: {}", p.display());
|
|
paths.push(p);
|
|
}
|
|
}
|
|
|
|
// Try to extract paths from Steam registry file
|
|
if let Some(registry_paths) = read_steam_registry() {
|
|
for path in registry_paths {
|
|
if !paths.contains(&path) && path.exists() {
|
|
debug!("Adding Steam path from registry: {}", path.display());
|
|
paths.push(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
info!("Found {} potential Steam directories", paths.len());
|
|
paths
|
|
}
|
|
|
|
// Try to read the Steam registry file to find installation paths
|
|
fn read_steam_registry() -> Option<Vec<PathBuf>> {
|
|
let home = match std::env::var("HOME") {
|
|
Ok(h) => h,
|
|
Err(_) => return None,
|
|
};
|
|
|
|
let registry_paths = [
|
|
format!("{}/.steam/registry.vdf", home),
|
|
format!("{}/.steam/steam/registry.vdf", home),
|
|
format!("{}/.local/share/Steam/registry.vdf", home),
|
|
];
|
|
|
|
for registry_path in registry_paths {
|
|
let path = Path::new(®istry_path);
|
|
if path.exists() {
|
|
debug!("Found Steam registry at: {}", path.display());
|
|
|
|
if let Ok(content) = fs::read_to_string(path) {
|
|
let mut paths = Vec::new();
|
|
|
|
// Extract Steam installation paths
|
|
let re_steam_path = Regex::new(r#""SteamPath"\s+"([^"]+)""#).unwrap();
|
|
if let Some(cap) = re_steam_path.captures(&content) {
|
|
let steam_path = PathBuf::from(&cap[1]);
|
|
paths.push(steam_path);
|
|
}
|
|
|
|
// Look for install path
|
|
let re_install_path = Regex::new(r#""InstallPath"\s+"([^"]+)""#).unwrap();
|
|
if let Some(cap) = re_install_path.captures(&content) {
|
|
let install_path = PathBuf::from(&cap[1]);
|
|
if !paths.contains(&install_path) {
|
|
paths.push(install_path);
|
|
}
|
|
}
|
|
|
|
if !paths.is_empty() {
|
|
return Some(paths);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
// Find all Steam library folders from base Steam installation paths
|
|
pub fn find_steam_libraries(base_paths: &[PathBuf]) -> Vec<PathBuf> {
|
|
let mut libraries = HashSet::new();
|
|
|
|
for base_path in base_paths {
|
|
debug!("Looking for Steam libraries in: {}", base_path.display());
|
|
|
|
// Check if this path contains a steamapps directory
|
|
let steamapps_path = base_path.join("steamapps");
|
|
if steamapps_path.exists() && steamapps_path.is_dir() {
|
|
debug!("Found steamapps directory: {}", steamapps_path.display());
|
|
libraries.insert(steamapps_path.clone());
|
|
|
|
// Check for additional libraries in libraryfolders.vdf
|
|
parse_library_folders_vdf(&steamapps_path, &mut libraries);
|
|
}
|
|
|
|
// Also check for steamapps in common locations relative to this path
|
|
let possible_steamapps = [
|
|
base_path.join("steam/steamapps"),
|
|
base_path.join("Steam/steamapps"),
|
|
];
|
|
|
|
for path in &possible_steamapps {
|
|
if path.exists() && path.is_dir() && !libraries.contains(path) {
|
|
debug!("Found steamapps directory: {}", path.display());
|
|
libraries.insert(path.clone());
|
|
|
|
// Check for additional libraries in libraryfolders.vdf
|
|
parse_library_folders_vdf(path, &mut libraries);
|
|
}
|
|
}
|
|
}
|
|
|
|
let result: Vec<PathBuf> = libraries.into_iter().collect();
|
|
info!("Found {} Steam library directories", result.len());
|
|
for (i, lib) in result.iter().enumerate() {
|
|
info!(" Library {}: {}", i + 1, lib.display());
|
|
}
|
|
result
|
|
}
|
|
|
|
// Parse libraryfolders.vdf to extract additional library paths
|
|
fn parse_library_folders_vdf(steamapps_path: &Path, libraries: &mut HashSet<PathBuf>) {
|
|
// Check both possible locations of the VDF file
|
|
let vdf_paths = [
|
|
steamapps_path.join("libraryfolders.vdf"),
|
|
steamapps_path.join("config/libraryfolders.vdf"),
|
|
];
|
|
|
|
for vdf_path in &vdf_paths {
|
|
if vdf_path.exists() {
|
|
debug!("Found library folders VDF: {}", vdf_path.display());
|
|
|
|
if let Ok(content) = fs::read_to_string(vdf_path) {
|
|
// Extract library paths using regex for both new and old format VDFs
|
|
let re_path = Regex::new(r#""path"\s+"([^"]+)""#).unwrap();
|
|
for cap in re_path.captures_iter(&content) {
|
|
let path_str = &cap[1];
|
|
let lib_path = PathBuf::from(path_str).join("steamapps");
|
|
|
|
if lib_path.exists() && lib_path.is_dir() && !libraries.contains(&lib_path) {
|
|
debug!("Found library from VDF: {}", lib_path.display());
|
|
// Clone lib_path before inserting to avoid ownership issues
|
|
let lib_path_clone = lib_path.clone();
|
|
libraries.insert(lib_path_clone);
|
|
|
|
// Recursively check this library for more libraries
|
|
parse_library_folders_vdf(&lib_path, libraries);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse an appmanifest ACF file to extract game information
|
|
fn parse_appmanifest(path: &Path) -> Option<(String, String, String)> {
|
|
match fs::read_to_string(path) {
|
|
Ok(content) => {
|
|
// Use regex to extract the app ID, name, and install directory
|
|
let re_appid = Regex::new(r#""appid"\s+"(\d+)""#).unwrap();
|
|
let re_name = Regex::new(r#""name"\s+"([^"]+)""#).unwrap();
|
|
let re_installdir = Regex::new(r#""installdir"\s+"([^"]+)""#).unwrap();
|
|
|
|
if let (Some(app_id_cap), Some(name_cap), Some(dir_cap)) = (
|
|
re_appid.captures(&content),
|
|
re_name.captures(&content),
|
|
re_installdir.captures(&content),
|
|
) {
|
|
let app_id = app_id_cap[1].to_string();
|
|
let name = name_cap[1].to_string();
|
|
let install_dir = dir_cap[1].to_string();
|
|
|
|
return Some((app_id, name, install_dir));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to read ACF file {}: {}", path.display(), e);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
// Check if a file is a Linux ELF binary
|
|
fn is_elf_binary(path: &Path) -> bool {
|
|
if let Ok(mut file) = fs::File::open(path) {
|
|
let mut buffer = [0; 4];
|
|
if file.read_exact(&mut buffer).is_ok() {
|
|
// Check for ELF magic number (0x7F 'E' 'L' 'F')
|
|
return buffer[0] == 0x7F
|
|
&& buffer[1] == b'E'
|
|
&& buffer[2] == b'L'
|
|
&& buffer[3] == b'F';
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
// Check if a game has CreamLinux installed
|
|
fn check_creamlinux_installed(game_path: &Path) -> bool {
|
|
let cream_files = ["cream.sh", "cream_api.ini", "cream_api.so"];
|
|
|
|
for file in &cream_files {
|
|
if game_path.join(file).exists() {
|
|
debug!("CreamLinux installation detected: {}", file);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
// Check if a game has SmokeAPI installed
|
|
fn check_smokeapi_installed(game_path: &Path, api_files: &[String]) -> bool {
|
|
// First check the provided api_files for backup files
|
|
for api_file in api_files {
|
|
let api_path = game_path.join(api_file);
|
|
let api_dir = api_path.parent().unwrap_or(game_path);
|
|
let api_filename = api_path.file_name().unwrap_or_default();
|
|
|
|
// Check for backup file (original file renamed with _o.dll suffix)
|
|
let backup_name = api_filename.to_string_lossy().replace(".dll", "_o.dll");
|
|
let backup_path = api_dir.join(backup_name);
|
|
|
|
if backup_path.exists() {
|
|
debug!("SmokeAPI backup file found: {}", backup_path.display());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Also scan for orphaned backup files (in case the main DLL was removed)
|
|
// This handles the Proton->Native switch case where steam_api*.dll is gone
|
|
// but steam_api*_o.dll backup remains
|
|
for entry in WalkDir::new(game_path)
|
|
.max_depth(5)
|
|
.into_iter()
|
|
.filter_map(Result::ok)
|
|
{
|
|
let path = entry.path();
|
|
if !path.is_file() {
|
|
continue;
|
|
}
|
|
|
|
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
|
|
|
// Look for steam_api*_o.dll backup files (SmokeAPI pattern)
|
|
if filename.starts_with("steam_api") && filename.ends_with("_o.dll") {
|
|
debug!("Found orphaned SmokeAPI backup file: {}", path.display());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
// Scan a game directory to determine if it's native or needs Proton
|
|
// Also collect any Steam API DLLs for potential SmokeAPI installation
|
|
fn scan_game_directory(game_path: &Path) -> (bool, Vec<String>) {
|
|
let mut found_exe = false;
|
|
let mut found_linux_binary = false;
|
|
let mut found_main_executable = false;
|
|
let mut steam_api_files = Vec::new();
|
|
|
|
// Strong indicators for native Linux games
|
|
let mut has_libsteam_api = false;
|
|
let mut has_linux_steam_libs = false;
|
|
let mut linux_binary_count = 0;
|
|
let mut windows_exe_count = 0;
|
|
|
|
// Directories to skip for better performance
|
|
let skip_dirs = [
|
|
"videos",
|
|
"video",
|
|
"movies",
|
|
"movie",
|
|
"sound",
|
|
"sounds",
|
|
"audio",
|
|
"textures",
|
|
"music",
|
|
"localization",
|
|
"shaders",
|
|
"logs",
|
|
"assets/audio",
|
|
"assets/video",
|
|
"assets/textures",
|
|
];
|
|
|
|
// Only scan to a reasonable depth (avoid extreme recursion)
|
|
const MAX_DEPTH: usize = 8;
|
|
|
|
// File extensions to check for (executable and Steam API files)
|
|
let exe_extensions = ["exe", "bat", "cmd", "msi"];
|
|
let binary_extensions = ["so", "bin", "sh", "x86", "x86_64"];
|
|
|
|
// Files that indicate this is likely a launcher/installer
|
|
let installer_patterns = [
|
|
"setup", "install", "launcher", "uninstall", "redist", "vcredist", "directx", "_commonredist", "dotnet", "PhysX"
|
|
];
|
|
|
|
// Recursively walk through the game directory
|
|
for entry in WalkDir::new(game_path)
|
|
.max_depth(MAX_DEPTH) // Limit depth to avoid traversing too deep
|
|
.follow_links(false) // Don't follow symlinks to prevent cycles
|
|
.into_iter()
|
|
.filter_entry(|e| {
|
|
// Skip certain directories for performance
|
|
if e.file_type().is_dir() {
|
|
let file_name = e.file_name().to_string_lossy().to_lowercase();
|
|
if skip_dirs.iter().any(|&dir| file_name == dir) {
|
|
debug!("Skipping directory: {}", e.path().display());
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
})
|
|
.filter_map(Result::ok)
|
|
{
|
|
let path = entry.path();
|
|
if !path.is_file() {
|
|
continue;
|
|
}
|
|
|
|
let filename = path.file_name()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.to_lowercase();
|
|
|
|
// Check for strong Linux indicators first
|
|
if filename == "libsteam_api.so" {
|
|
has_libsteam_api = true;
|
|
debug!("Found strong Linux indicator: {}", path.display());
|
|
}
|
|
|
|
// Check for other Linux Steam libraries
|
|
if filename.starts_with("lib") && filename.contains("steam") && filename.ends_with(".so") {
|
|
has_linux_steam_libs = true;
|
|
debug!("Found Linux Steam library: {}", path.display());
|
|
}
|
|
|
|
// Check file extension
|
|
if let Some(ext) = path.extension() {
|
|
let ext_str = ext.to_string_lossy().to_lowercase();
|
|
|
|
// Check for Windows executables
|
|
if exe_extensions.iter().any(|&e| ext_str == e) {
|
|
// Check if this looks like an installer/utility rather than main game
|
|
let is_likely_installer = installer_patterns.iter()
|
|
.any(|&pattern| filename.contains(pattern));
|
|
|
|
if !is_likely_installer {
|
|
found_exe = true;
|
|
windows_exe_count += 1;
|
|
|
|
// If its in the root directory and not an installer, its likely the main executable
|
|
if path.parent() == Some(game_path) {
|
|
found_main_executable = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for Steam API DLLs
|
|
if ext_str == "dll" {
|
|
if filename == "steam_api.dll" || filename == "steam_api64.dll" {
|
|
if let Ok(rel_path) = path.strip_prefix(game_path) {
|
|
let rel_path_str = rel_path.to_string_lossy().to_string();
|
|
debug!("Found Steam API DLL: {}", rel_path_str);
|
|
steam_api_files.push(rel_path_str);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for Linux binary files
|
|
if binary_extensions.iter().any(|&e| ext_str == e) {
|
|
found_linux_binary = true;
|
|
linux_binary_count += 1;
|
|
|
|
// Check if it's actually an ELF binary for more certainty
|
|
if ext_str == "so" && is_elf_binary(path) {
|
|
found_linux_binary = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for Linux executables (no extension)
|
|
#[cfg(unix)]
|
|
if !path.extension().is_some() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
if let Ok(metadata) = path.metadata() {
|
|
let is_executable = metadata.permissions().mode() & 0o111 != 0;
|
|
|
|
// Check executable permission and ELF format
|
|
if is_executable && is_elf_binary(path) {
|
|
found_linux_binary = true;
|
|
linux_binary_count += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Detection logic with priority system
|
|
let has_steam_api_dll = !steam_api_files.is_empty();
|
|
let is_native = determine_platform(
|
|
has_libsteam_api,
|
|
has_linux_steam_libs,
|
|
found_linux_binary,
|
|
found_exe,
|
|
found_main_executable,
|
|
linux_binary_count,
|
|
windows_exe_count,
|
|
has_steam_api_dll,
|
|
);
|
|
|
|
debug!(
|
|
"Game scan results: native={}, libsteam_api={}, linux_libs={}, linux_binaries={}, exe_files={}, api_dlls={}",
|
|
is_native,
|
|
has_libsteam_api,
|
|
has_linux_steam_libs,
|
|
linux_binary_count,
|
|
windows_exe_count,
|
|
steam_api_files.len()
|
|
);
|
|
|
|
(is_native, steam_api_files)
|
|
}
|
|
|
|
// Priority-based platform detection
|
|
fn determine_platform(
|
|
has_libsteam_api: bool,
|
|
has_linux_steam_libs: bool,
|
|
found_linux_binary: bool,
|
|
found_exe: bool,
|
|
found_main_executable: bool,
|
|
linux_binary_count: usize,
|
|
windows_exe_count: usize,
|
|
has_steam_api_dll: bool,
|
|
) -> bool {
|
|
// Priority 1: Strong Linux indicators
|
|
if has_libsteam_api {
|
|
debug!("Detected as native: libsteam_api.so found");
|
|
return true;
|
|
}
|
|
|
|
if has_linux_steam_libs {
|
|
debug!("Detected as native: Linux steam libraries found");
|
|
return true;
|
|
}
|
|
|
|
// Priority 2: Strong Windows indicators - DLL files are Windows-only
|
|
if has_steam_api_dll {
|
|
debug!("Detected as Windows/Proton: steam_api.dll or steam_api64.dll found");
|
|
return false;
|
|
}
|
|
|
|
// Priority 3: High confidence Linux indicators
|
|
if found_linux_binary && linux_binary_count >= 3 && !found_main_executable {
|
|
debug!("Detected as native: Multiple Linux binaries, no main Windows executable");
|
|
return true;
|
|
}
|
|
|
|
// Priority 4: Balanced assessment
|
|
if found_linux_binary && !found_main_executable && windows_exe_count <= 2 {
|
|
debug!("Detected as native: Linux binaries present, only installer/utility Windows files");
|
|
return true;
|
|
}
|
|
|
|
// Priority 5: Windows indicators
|
|
if found_main_executable || (found_exe && !found_linux_binary) {
|
|
debug!("Detected as Windows/Proton: Main executable or only Windows files found");
|
|
return false;
|
|
}
|
|
|
|
// Priority 6: Default fallback
|
|
if found_linux_binary {
|
|
debug!("Detected as native: Linux binaries found (default fallback)");
|
|
return true;
|
|
}
|
|
|
|
debug!("Detected as Windows/Proton: No strong indicators found");
|
|
false
|
|
}
|
|
|
|
// Find all installed Steam games from library folders
|
|
pub async fn find_installed_games(steamapps_paths: &[PathBuf]) -> Vec<GameInfo> {
|
|
let mut games = Vec::new();
|
|
let seen_ids = Arc::new(tokio::sync::Mutex::new(HashSet::new()));
|
|
|
|
// IDs to skip (tools, redistributables, etc.)
|
|
let skip_ids = Arc::new(
|
|
[
|
|
"228980", // Steamworks Common Redistributables
|
|
"1070560", // Steam Linux Runtime
|
|
"1391110", // Steam Linux Runtime - Soldier
|
|
"1628350", // Steam Linux Runtime - Sniper
|
|
"1493710", // Proton Experimental
|
|
"2180100", // Steam Linux Runtime - Scout
|
|
]
|
|
.iter()
|
|
.copied()
|
|
.collect::<HashSet<&str>>(),
|
|
);
|
|
|
|
// Name patterns to skip (case insensitive)
|
|
let skip_patterns = Arc::new(
|
|
[
|
|
r"(?i)steam linux runtime",
|
|
r"(?i)proton",
|
|
r"(?i)steamworks common",
|
|
r"(?i)redistributable",
|
|
r"(?i)dotnet",
|
|
r"(?i)vc redist",
|
|
]
|
|
.iter()
|
|
.map(|pat| Regex::new(pat).unwrap())
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
|
|
info!("Scanning for installed games in parallel...");
|
|
|
|
// Create a channel to collect results
|
|
let (tx, mut rx) = mpsc::channel(32);
|
|
|
|
// First collect all appmanifest files to process
|
|
let mut app_manifests = Vec::new();
|
|
for steamapps_dir in steamapps_paths {
|
|
if let Ok(entries) = fs::read_dir(steamapps_dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
|
|
|
// Check for appmanifest files
|
|
if filename.starts_with("appmanifest_") && filename.ends_with(".acf") {
|
|
app_manifests.push((path, steamapps_dir.clone()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
info!("Found {} appmanifest files to process", app_manifests.len());
|
|
|
|
// Process appmanifest files
|
|
let max_concurrent = num_cpus::get().max(1).min(8); // Use between 1 and 8 CPU cores
|
|
info!("Using {} concurrent scanners", max_concurrent);
|
|
|
|
// Use a semaphore to limit concurrency
|
|
let semaphore = Arc::new(tokio::sync::Semaphore::new(max_concurrent));
|
|
|
|
// Create a Vec to store all our task handles
|
|
let mut handles = Vec::new();
|
|
|
|
// Process each manifest file
|
|
for (manifest_idx, (path, steamapps_dir)) in app_manifests.iter().enumerate() {
|
|
// Clone what we need for the task
|
|
let path = path.clone();
|
|
let steamapps_dir = steamapps_dir.clone();
|
|
let skip_patterns = Arc::clone(&skip_patterns);
|
|
let tx = tx.clone();
|
|
let seen_ids = Arc::clone(&seen_ids);
|
|
let semaphore = Arc::clone(&semaphore);
|
|
let skip_ids = Arc::clone(&skip_ids);
|
|
|
|
// Create a new task
|
|
let handle = tokio::spawn(async move {
|
|
// Acquire a permit from the semaphore
|
|
let _permit = semaphore.acquire().await.unwrap();
|
|
|
|
// Parse the appmanifest file
|
|
if let Some((id, name, install_dir)) = parse_appmanifest(&path) {
|
|
// Skip if in exclusion list
|
|
if skip_ids.contains(id.as_str()) {
|
|
return;
|
|
}
|
|
|
|
// Add a guard against duplicates
|
|
{
|
|
let mut seen = seen_ids.lock().await;
|
|
if seen.contains(&id) {
|
|
return;
|
|
}
|
|
seen.insert(id.clone());
|
|
}
|
|
|
|
// Skip if the name matches any exclusion patterns
|
|
if skip_patterns.iter().any(|re| re.is_match(&name)) {
|
|
debug!("Skipping runtime/tool: {} ({})", name, id);
|
|
return;
|
|
}
|
|
|
|
// Full path to the game directory
|
|
let game_path = steamapps_dir.join("common").join(&install_dir);
|
|
|
|
// Skip if game directory doesn't exist
|
|
if !game_path.exists() {
|
|
warn!("Game directory not found: {}", game_path.display());
|
|
return;
|
|
}
|
|
|
|
// Scan the game directory to determine platform and find Steam API DLLs
|
|
info!("Scanning game: {} at {}", name, game_path.display());
|
|
|
|
// Scanning is I/O heavy but not CPU heavy, so we can just do it directly
|
|
let (is_native, api_files) = scan_game_directory(&game_path);
|
|
|
|
// Check for CreamLinux installation
|
|
let cream_installed = check_creamlinux_installed(&game_path);
|
|
|
|
// Check for SmokeAPI installation
|
|
// For Proton games: check if api_files exist
|
|
// For Native games: ALSO check for orphaned backup files (proton->native switch)
|
|
let smoke_installed = check_smokeapi_installed(&game_path, &api_files);
|
|
|
|
// Create the game info
|
|
let game_info = GameInfo {
|
|
id,
|
|
title: name,
|
|
path: game_path,
|
|
native: is_native,
|
|
api_files,
|
|
cream_installed,
|
|
smoke_installed,
|
|
};
|
|
|
|
// Send the game info through the channel
|
|
if tx.send(game_info).await.is_err() {
|
|
error!("Failed to send game info through channel");
|
|
}
|
|
}
|
|
});
|
|
|
|
handles.push(handle);
|
|
|
|
// Every 10 files, yield to allow progress updates
|
|
if manifest_idx % 10 == 0 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
}
|
|
|
|
// Drop the original sender so the receiver knows when we're done
|
|
drop(tx);
|
|
|
|
// Spawn a task to collect all the results
|
|
let receiver_task = tokio::spawn(async move {
|
|
let mut results = Vec::new();
|
|
while let Some(game) = rx.recv().await {
|
|
info!("Found game: {} ({})", game.title, game.id);
|
|
info!(" Path: {}", game.path.display());
|
|
info!(
|
|
" Status: Native={}, Cream={}, Smoke={}",
|
|
game.native, game.cream_installed, game.smoke_installed
|
|
);
|
|
|
|
// Log Steam API DLLs if any
|
|
if !game.api_files.is_empty() {
|
|
info!(" Steam API files:");
|
|
for api_file in &game.api_files {
|
|
info!(" - {}", api_file);
|
|
}
|
|
}
|
|
|
|
results.push(game);
|
|
}
|
|
results
|
|
});
|
|
|
|
// Wait for all scan tasks to complete but don't wait for the results yet
|
|
for handle in handles {
|
|
// Ignore errors the receiver task will just get fewer results
|
|
let _ = handle.await;
|
|
}
|
|
|
|
// Now wait for all results to be collected
|
|
if let Ok(results) = receiver_task.await {
|
|
games = results;
|
|
}
|
|
|
|
info!("Found {} installed games", games.len());
|
|
games
|
|
} |