diff --git a/Cargo.toml b/Cargo.toml index fe821b6..622f35c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" steam-vdf-parser = "0.1.1" # Needed for raw binary PICS VDF parsing (not supported by keyvalues-serde or steam-vent yet) steam-vent = "0.4.2" -steam-vent-proto = "=0.5.2" +steam-vent-proto = "0.5.2" tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs", "time"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } diff --git a/src/infra/runners/tests.rs b/src/infra/runners/tests.rs index 19e3cfe..ffb04ee 100644 --- a/src/infra/runners/tests.rs +++ b/src/infra/runners/tests.rs @@ -18,6 +18,7 @@ mod tests { update_available: false, update_queued: false, active_branch: "public".to_string(), + manifest_missing: false, }, launch_info: LaunchInfo { app_id: 123, @@ -88,6 +89,7 @@ mod tests { update_available: false, update_queued: false, local_manifest_ids: HashMap::new(), + manifest_missing: false, }; let mut config = LauncherConfig::default(); diff --git a/src/library.rs b/src/library.rs index 00e1b52..b1ce87a 100644 --- a/src/library.rs +++ b/src/library.rs @@ -2,7 +2,7 @@ use crate::config::{detect_steam_path, load_launcher_config}; use crate::models::{GameLibrary, GameModel, LibraryGame, LocalGame, OwnedGame}; use anyhow::{Context, Result}; use serde::Deserialize; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use tokio::fs; @@ -30,6 +30,10 @@ pub struct InstalledAppInfo { pub install_path: PathBuf, pub active_branch: String, pub name: Option, + pub manifest_installdir: Option, + pub manifest_installdir_valid: bool, + pub install_dir_resolution_method: String, + pub manifest_missing: bool, } pub async fn find_local_games() -> Result> { @@ -43,6 +47,10 @@ pub async fn find_local_games() -> Result> { install_dir: info.install_path, proton_version: None, active_branch: info.active_branch, + manifest_installdir: info.manifest_installdir, + manifest_installdir_valid: info.manifest_installdir_valid, + install_dir_resolution_method: Some(info.install_dir_resolution_method), + manifest_missing: info.manifest_missing, }); } @@ -107,6 +115,9 @@ pub async fn scan_library_info(root_path: &Path) -> Result Result { + Ok(Some((app_id, mut info))) => { + acf_count += 1; + let steamapps = path.parent().unwrap_or(Path::new("")); + + // Validation and Fallback + let mut valid = true; + let mut method = "manifest".to_string(); + + if crate::utils::is_suspicious_installdir(info.manifest_installdir.as_deref().unwrap_or_default(), app_id) { + valid = false; + method = "manifest_suspicious".to_string(); + } else if !info.install_path.exists() { + valid = false; + method = "manifest_not_found".to_string(); + } + + if !valid { + if let Some(probed_path) = crate::utils::probe_install_dir_by_appid(steamapps, app_id) { + tracing::info!("Resolved suspicious/missing installdir for app {} via probe: {:?}", app_id, probed_path); + info.install_path = probed_path; + method = if method == "manifest_suspicious" { "appid_probe_suspicious" } else { "appid_probe_missing" }.to_string(); + valid = true; + } + } else { + method = "manifest_validated".to_string(); + } + + info.manifest_installdir_valid = valid; + info.install_dir_resolution_method = method; + info.manifest_missing = false; + installed.insert(app_id, info); } Ok(None) => {} Err(e) => println!("Skipping bad manifest {:?}: {}", path, e), } } + + // 2. Recovery: Scan 'common' for orphaned directories (missing ACF) + let common = library_root.join("steamapps").join("common"); + if common.exists() { + if let Ok(mut dir) = fs::read_dir(&common).await { + while let Some(entry) = dir.next_entry().await? { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + // Check if this directory is already tracked by any app_id we found via ACF + let already_tracked = installed.values().any(|info| info.install_path == path); + if already_tracked { + continue; + } + + // Try to identify app_id from steam_appid.txt + let appid_txt = path.join("steam_appid.txt"); + if appid_txt.exists() { + if let Ok(content) = std::fs::read_to_string(&appid_txt) { + if let Ok(app_id) = content.trim().parse::() { + if !installed.contains_key(&app_id) { + recovered_count += 1; + let name = path.file_name().and_then(|n| n.to_str()).map(|s| s.to_string()); + tracing::info!("Recovered orphaned Steam installation for app {}: {:?}", app_id, path); + + installed.insert(app_id, InstalledAppInfo { + install_path: path.clone(), + active_branch: "public".to_string(), + name, + manifest_installdir: path.file_name().and_then(|n| n.to_str()).map(|s| s.to_string()), + manifest_installdir_valid: true, + install_dir_resolution_method: "recovery_orphaned_manifest".to_string(), + manifest_missing: true, + }); + } + } + } + } + } + } + } } + tracing::info!( + "Scan of library root {:?} complete: {} via ACF, {} recovered via fallback.", + root_path, + acf_count, + recovered_count + ); + Ok(installed) } @@ -266,7 +357,7 @@ async fn parse_app_manifest_info(path: &Path) -> Result { let install_path = path .parent() - .map(|p| p.join("common").join(dir)) + .map(|p| p.join("common").join(&dir)) .unwrap_or_default(); Ok(Some(( id, @@ -274,6 +365,10 @@ async fn parse_app_manifest_info(path: &Path) -> Result Vec { out } +/// Reconciles network-owned games with locally discovered Steam installations. +/// +/// Merging logic: +/// 1. Start with all games from the Steam network (owned games). +/// 2. If a network game is also found locally, merge its installation state. +/// 3. If a game is found locally but NOT in the network list (e.g. demos, free titles, +/// or during network failure), add it as a "local-only" discovery. +/// +/// This ensures that installed titles like demos are always discoverable even if +/// the Steam network metadata is incomplete or filtered. pub fn build_game_library( owned: Vec, installed_info: HashMap, ) -> GameLibrary { let mut games = Vec::new(); + let mut remote_appids = HashSet::new(); + + let mut local_only_count = 0; + let mut merged_count = 0; + let mut demo_count = 0; for owned_game in owned { + remote_appids.insert(owned_game.app_id); let info = installed_info.get(&owned_game.app_id); let install_path = info.map(|i| i.install_path.to_string_lossy().to_string()); + let is_installed = install_path.is_some(); let active_branch = info .map(|i| i.active_branch.clone()) .unwrap_or_else(|| "public".to_string()); + if is_installed { + merged_count += 1; + } + + if owned_game.name.to_lowercase().contains("demo") { + demo_count += 1; + } + games.push(LibraryGame { app_id: owned_game.app_id, name: owned_game.name, playtime_forever_minutes: Some(owned_game.playtime_forever_minutes), - is_installed: install_path.is_some(), + is_installed, install_path, local_manifest_ids: owned_game.local_manifest_ids, update_available: owned_game.update_available, update_queued: false, active_branch, + manifest_missing: info.map(|i| i.manifest_missing).unwrap_or(false), }); } for (app_id, info) in installed_info { - if games.iter().any(|g| g.app_id == app_id) { + if remote_appids.contains(&app_id) { continue; } + local_only_count += 1; + let name = info.name.clone().unwrap_or_else(|| format!("App {app_id}")); + if name.to_lowercase().contains("demo") { + demo_count += 1; + } + + tracing::info!("Discovered local-only Steam app (not in owned list): {} ({})", name, app_id); + games.push(LibraryGame { app_id, - name: info.name.unwrap_or_else(|| format!("App {app_id}")), + name, playtime_forever_minutes: None, is_installed: true, install_path: Some(info.install_path.to_string_lossy().to_string()), @@ -342,9 +471,19 @@ pub fn build_game_library( update_available: false, update_queued: false, active_branch: info.active_branch, + manifest_missing: info.manifest_missing, }); } + tracing::info!( + "Library reconciliation complete: {} merged (local+remote), {} remote-only, {} local-only discovered. Total games: {}. Demos detected: {}.", + merged_count, + remote_appids.len() - merged_count, + local_only_count, + games.len(), + demo_count + ); + games.sort_by(|a, b| a.name.cmp(&b.name)); GameLibrary { games } } diff --git a/src/models.rs b/src/models.rs index 460ad40..5efd592 100644 --- a/src/models.rs +++ b/src/models.rs @@ -165,6 +165,14 @@ pub struct LocalGame { pub proton_version: Option, #[serde(default = "default_branch")] pub active_branch: String, + #[serde(default)] + pub manifest_installdir: Option, + #[serde(default = "default_true")] + pub manifest_installdir_valid: bool, + #[serde(default)] + pub install_dir_resolution_method: Option, + #[serde(default)] + pub manifest_missing: bool, } fn default_branch() -> String { @@ -202,6 +210,8 @@ pub struct LibraryGame { pub update_queued: bool, #[serde(default = "default_branch")] pub active_branch: String, + #[serde(default)] + pub manifest_missing: bool, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/src/steam_client.rs b/src/steam_client.rs index 83bbf2c..0eb487a 100644 --- a/src/steam_client.rs +++ b/src/steam_client.rs @@ -1291,29 +1291,52 @@ impl SteamClient { steamid: Some(u64::from(connection.steam_id())), include_appinfo: Some(true), include_played_free_games: Some(true), + include_free_sub: Some(true), + include_extended_appinfo: Some(true), ..Default::default() }; + tracing::info!("Refreshing Steam library: Player.GetOwnedGames(include_appinfo=true, include_played_free_games=true, include_free_sub=true, include_extended_appinfo=true)"); + let response: CPlayer_GetOwnedGames_Response = connection .service_method(request) .await .context("failed calling Player.GetOwnedGames")?; + let total_returned = response.game_count(); let mut owned = Vec::new(); + let mut demos_count = 0; + for game in response.games { + let app_id = game.appid() as u32; + let name = if game.name().is_empty() { + format!("App {}", app_id) + } else { + game.name().to_string() + }; + + // In some proto versions, we might be able to check app type here if extended info is parsed. + // For now, we trust GetOwnedGames filtered them appropriately based on our request. + if name.to_lowercase().contains("demo") { + demos_count += 1; + } + owned.push(OwnedGame { - app_id: game.appid() as u32, - name: if game.name().is_empty() { - format!("App {}", game.appid()) - } else { - game.name().to_string() - }, + app_id, + name, playtime_forever_minutes: game.playtime_forever() as u32, local_manifest_ids: HashMap::new(), update_available: false, }); } + tracing::info!( + "Library refreshed: {} apps returned, {} demos detected. {} games total in local list.", + total_returned, + demos_count, + owned.len() + ); + save_library_cache(&owned).await.ok(); Ok(owned) } @@ -1949,7 +1972,7 @@ impl SteamClient { } // Fallback: search for app id markers if the specified installdir doesn't exist - if let Some(fallback) = self.probe_install_dir_by_appid(&steamapps, appid) { + if let Some(fallback) = crate::utils::probe_install_dir_by_appid(&steamapps, appid) { tracing::info!("Found fallback install dir for app {appid}: {:?}", fallback); return Ok(fallback); } @@ -1966,33 +1989,6 @@ impl SteamClient { .join(appid.to_string())) } - fn probe_install_dir_by_appid(&self, steamapps: &Path, appid: u32) -> Option { - let common = steamapps.join("common"); - if !common.exists() { - return None; - } - - let appid_str = appid.to_string(); - - if let Ok(entries) = std::fs::read_dir(common) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - // Check for steam_appid.txt - let appid_txt = path.join("steam_appid.txt"); - if appid_txt.exists() { - if let Ok(content) = std::fs::read_to_string(appid_txt) { - if content.trim() == appid_str { - return Some(path); - } - } - } - } - } - } - None - } - async fn remote_manifest_ids_static( connection: &Connection, appid: u32, @@ -2119,6 +2115,48 @@ impl SteamClient { self.resolve_install_game_info(appid).await.0 } + pub async fn repair_manifest(&self, appid: u32) -> Result<()> { + let manifest_path = self.appmanifest_path(appid).await?; + if manifest_path.exists() { + tracing::warn!("Appmanifest for {} already exists, creating backup.", appid); + let mut backup_path = manifest_path.clone(); + backup_path.set_extension("acf.bak"); + std::fs::copy(&manifest_path, &backup_path)?; + } + + let (game_name, pics_installdir) = self.resolve_install_game_info(appid).await; + + // Ensure we have a reasonable installdir and name, avoid placeholders + let final_name = if game_name.starts_with("App ") { + // Try to find a better name from local files or just use the appid + game_name + } else { + game_name + }; + + let final_installdir = match pics_installdir { + Some(dir) if !crate::utils::is_suspicious_installdir(&dir, appid) => dir, + _ => { + // Fallback to searching for the actual directory on disk + let cfg = load_launcher_config().await?; + let steamapps = PathBuf::from(&cfg.steam_library_path).join("steamapps"); + if let Some(probed) = crate::utils::probe_install_dir_by_appid(&steamapps, appid) { + probed.file_name().and_then(|n| n.to_str()).unwrap_or(&appid.to_string()).to_string() + } else { + sanitize_install_dir(&final_name) + } + } + }; + + tracing::info!("Repairing manifest for {appid}: name='{final_name}', installdir='{final_installdir}'"); + + // For a minimal repair, we don't necessarily know the full depot list + // but we can write the manifest with just the basic AppState + Self::write_appmanifest(&manifest_path, appid, &final_name, &final_installdir, Vec::new())?; + + Ok(()) + } + pub fn write_appmanifest( path: &Path, appid: u32, @@ -2133,8 +2171,16 @@ impl SteamClient { let game_name = game_name.replace('"', ""); + // StateFlags 4 means 'Fully Installed' + // Universe 1 is the public Steam universe let mut content = format!( - "\"AppState\"\n{{\n\t\"appid\"\t\"{appid}\"\n\t\"name\"\t\"{game_name}\"\n\t\"StateFlags\"\t\"4\"\n\t\"installdir\"\t\"{installdir}\"\n" + "\"AppState\"\n\ + {{\n\ + \t\"appid\"\t\"{appid}\"\n\ + \t\"Universe\"\t\"1\"\n\ + \t\"name\"\t\"{game_name}\"\n\ + \t\"StateFlags\"\t\"4\"\n\ + \t\"installdir\"\t\"{installdir}\"\n" ); if !installed_depots.is_empty() { @@ -2149,6 +2195,7 @@ impl SteamClient { content.push_str("}\n"); + tracing::info!("Writing appmanifest for {appid} to {:?}", path); std::fs::write(path, content) .with_context(|| format!("failed writing {}", path.display()))?; Ok(()) @@ -3038,6 +3085,7 @@ mod tests { update_available: false, update_queued: false, local_manifest_ids: HashMap::new(), + manifest_missing: false, }; let launch_info = LaunchInfo { app_id: 123, diff --git a/src/ui.rs b/src/ui.rs index 4b7aa43..487d111 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -79,6 +79,7 @@ pub enum AsyncOp { BranchUpdated(u32, String), AccountDataFetched(crate::steam_client::AccountData), Uninstalled(u32, String), + ManifestRepaired(u32), PlatformsFetched(u32, Vec, Vec), ExtendedInfoFetched(u32, crate::steam_client::ExtendedAppInfo), LibraryFetched(Vec), @@ -489,6 +490,12 @@ impl SteamLauncher { } self.status = format!("Uninstalled {name}"); } + AsyncOp::ManifestRepaired(appid) => { + if let Some(game) = self.library.iter_mut().find(|g| g.app_id == appid) { + game.manifest_missing = false; + } + self.status = format!("Successfully repaired Steam manifest for app {appid}"); + } AsyncOp::PlatformsFetched(appid, platforms, buffer) => { if platforms.len() > 1 { let game_name = self @@ -697,38 +704,26 @@ impl SteamLauncher { let mut client = self.client.clone(); let tx = self.operation_tx.clone(); self.runtime.spawn(async move { - let result = match client.fetch_owned_games().await { - Ok(owned) => { - let installed = crate::library::scan_installed_app_info() - .await - .unwrap_or_default(); - let mut lib = build_game_library(owned, installed).games; - let _ = client.check_for_updates(&mut lib).await; - Ok(lib) - } - Err(err) => { - if client.is_offline() { - let cached = client.load_cached_owned_games().await.unwrap_or_default(); - let installed = crate::library::scan_installed_app_info() - .await - .unwrap_or_default(); - let mut lib = build_game_library(cached, installed).games; - let _ = client.check_for_updates(&mut lib).await; - Ok(lib) - } else { - Err(err) + let owned = if client.is_offline() { + client.load_cached_owned_games().await.unwrap_or_default() + } else { + match client.fetch_owned_games().await { + Ok(games) => games, + Err(err) => { + tracing::warn!("Failed to fetch owned games from Steam network: {}. Falling back to cache.", err); + client.load_cached_owned_games().await.unwrap_or_default() } } }; - match result { - Ok(lib) => { - let _ = tx.send(AsyncOp::LibraryFetched(lib)); - } - Err(err) => { - let _ = tx.send(AsyncOp::Error(format!("Failed to refresh library: {err}"))); - } - } + let installed = crate::library::scan_installed_app_info() + .await + .unwrap_or_default(); + + let mut lib = build_game_library(owned, installed).games; + let _ = client.check_for_updates(&mut lib).await; + + let _ = tx.send(AsyncOp::LibraryFetched(lib)); }); } @@ -1554,6 +1549,31 @@ impl SteamLauncher { } }); + ui.add_space(16.0); + ui.heading("Library Repair"); + ui.label("If this game was recovered from an orphaned directory, you can recreate its Steam manifest."); + + let mut repair_label = "Repair Steam Manifest".to_string(); + if game.manifest_missing { + repair_label = "⚠ Repair Missing ACF Manifest".to_string(); + } + + if ui.button(repair_label).on_hover_text("Generates a minimal appmanifest_*.acf file so official Steam can recognize this installation.").clicked() { + let client = self.client.clone(); + let tx = self.operation_tx.clone(); + let app_id = game.app_id; + self.runtime.spawn(async move { + match client.repair_manifest(app_id).await { + Ok(()) => { + let _ = tx.send(AsyncOp::ManifestRepaired(app_id)); + } + Err(e) => { + let _ = tx.send(AsyncOp::Error(format!("Failed to repair manifest: {e}"))); + } + } + }); + } + if !self.depot_list.is_empty() { ui.add_space(10.0); egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| { diff --git a/src/utils.rs b/src/utils.rs index d5c1ded..9794562 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1159,6 +1159,52 @@ pub fn cleanup_dll_symlinks(prefix: &Path) -> Result<()> { Ok(()) } +pub fn is_suspicious_installdir(dir: &str, app_id: u32) -> bool { + let dir_lower = dir.to_lowercase(); + if dir_lower == format!("app {}", app_id).to_lowercase() { + return true; + } + // Placeholder-like values from Steam + if dir_lower.starts_with("app ") && dir_lower[4..].chars().all(|c| c.is_ascii_digit()) { + return true; + } + false +} + +pub fn probe_install_dir_by_appid(steamapps: &Path, app_id: u32) -> Option { + let common = steamapps.join("common"); + if !common.exists() { + return None; + } + + let appid_str = app_id.to_string(); + + if let Ok(entries) = std::fs::read_dir(common) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + // Strong signal 1: steam_appid.txt + let appid_txt = path.join("steam_appid.txt"); + if appid_txt.exists() { + if let Ok(content) = std::fs::read_to_string(appid_txt) { + if content.trim() == appid_str { + return Some(path); + } + } + } + + // Signal 2: appid in directory name but NOT just "App " + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + if name.contains(&appid_str) && !is_suspicious_installdir(name, app_id) { + // If it contains appid and is not a suspicious generic name, it's a good candidate + return Some(path); + } + } + } + } + None +} + pub fn steam_wineprefix_for_game( config: &crate::config::LauncherConfig, app_id: u32, diff --git a/tests/path_resolution.rs b/tests/path_resolution.rs index f1d19f5..a7358b9 100644 --- a/tests/path_resolution.rs +++ b/tests/path_resolution.rs @@ -42,6 +42,7 @@ async fn test_batman_path_resolution() { update_available: false, update_queued: false, local_manifest_ids: HashMap::new(), + manifest_missing: false, }; let launch_info = LaunchInfo { diff --git a/tests/repro_issue.rs b/tests/repro_issue.rs new file mode 100644 index 0000000..639c3b7 --- /dev/null +++ b/tests/repro_issue.rs @@ -0,0 +1,92 @@ +use std::fs; +use steamflow::utils::{is_suspicious_installdir, probe_install_dir_by_appid}; + +#[test] +fn test_suspicious_detection() { + assert!(is_suspicious_installdir("App 2410180", 2410180)); + assert!(is_suspicious_installdir("App 123", 123)); + assert!(!is_suspicious_installdir("Portal Prelude RTX", 2410180)); +} + +#[test] +fn test_probing() { + let temp = tempfile::tempdir().unwrap(); + let steamapps = temp.path().join("steamapps"); + let common = steamapps.join("common"); + fs::create_dir_all(&common).unwrap(); + + let portal_dir = common.join("Portal Prelude RTX"); + fs::create_dir(&portal_dir).unwrap(); + fs::write(portal_dir.join("steam_appid.txt"), "2410180").unwrap(); + + let probed = probe_install_dir_by_appid(&steamapps, 2410180).unwrap(); + assert_eq!(probed.file_name().unwrap(), "Portal Prelude RTX"); +} + +#[test] +fn test_probing_by_name_contains_appid() { + let temp = tempfile::tempdir().unwrap(); + let steamapps = temp.path().join("steamapps"); + let common = steamapps.join("common"); + fs::create_dir_all(&common).unwrap(); + + let game_dir = common.join("Some Game 123"); + fs::create_dir(&game_dir).unwrap(); + + let probed = probe_install_dir_by_appid(&steamapps, 123).unwrap(); + assert_eq!(probed.file_name().unwrap(), "Some Game 123"); +} + +#[tokio::test] +async fn test_orphaned_directory_recovery() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path(); + let steamapps = root.join("steamapps"); + let common = steamapps.join("common"); + fs::create_dir_all(&common).unwrap(); + + // 1. A normal game with ACF + let game1_dir = common.join("Game One"); + fs::create_dir(&game1_dir).unwrap(); + let acf1_content = "\"AppState\"\n{\n\t\"appid\"\t\"101\"\n\t\"name\"\t\"Game One\"\n\t\"installdir\"\t\"Game One\"\n}\n"; + fs::write(steamapps.join("appmanifest_101.acf"), acf1_content).unwrap(); + + // 2. An orphaned game directory (missing ACF) + let game2_dir = common.join("Orphaned Game"); + fs::create_dir(&game2_dir).unwrap(); + fs::write(game2_dir.join("steam_appid.txt"), "202").unwrap(); + + let installed = steamflow::library::scan_library_info(root).await.unwrap(); + + // Verify game 1 (ACF) + assert!(installed.contains_key(&101)); + assert_eq!(installed.get(&101).unwrap().manifest_missing, false); + assert_eq!(installed.get(&101).unwrap().install_dir_resolution_method, "manifest_validated"); + + // Verify game 2 (Recovered) + assert!(installed.contains_key(&202)); + assert_eq!(installed.get(&202).unwrap().manifest_missing, true); + assert_eq!(installed.get(&202).unwrap().install_dir_resolution_method, "recovery_orphaned_manifest"); + assert_eq!(installed.get(&202).unwrap().name.as_deref(), Some("Orphaned Game")); +} + +#[test] +fn test_manifest_generation_minimal() { + let temp = tempfile::tempdir().unwrap(); + let manifest_path = temp.path().join("appmanifest_123.acf"); + + steamflow::steam_client::SteamClient::write_appmanifest( + &manifest_path, + 123, + "Test Game", + "TestGameDir", + Vec::new() + ).unwrap(); + + let content = std::fs::read_to_string(&manifest_path).unwrap(); + assert!(content.contains("\"appid\"\t\"123\"")); + assert!(content.contains("\"name\"\t\"Test Game\"")); + assert!(content.contains("\"installdir\"\t\"TestGameDir\"")); + assert!(content.contains("\"Universe\"\t\"1\"")); + assert!(content.contains("\"StateFlags\"\t\"4\"")); +} diff --git a/tests/staged_launch_failure_tests.rs b/tests/staged_launch_failure_tests.rs index d7c5bd6..b4797b1 100644 --- a/tests/staged_launch_failure_tests.rs +++ b/tests/staged_launch_failure_tests.rs @@ -37,6 +37,7 @@ async fn test_stage_validation_failure_launch_info() { update_available: false, update_queued: false, local_manifest_ids: HashMap::new(), + manifest_missing: false, }); // ctx.launch_info is None, so ResolveProfileStage should fail @@ -63,6 +64,7 @@ async fn test_stage_execution_failure_adhoc() { update_available: false, update_queued: false, local_manifest_ids: HashMap::new(), + manifest_missing: false, }); ctx.launch_info = Some(steamflow::steam_client::LaunchInfo { app_id: 123,