diff --git a/crates/asset_transport/src/lib.rs b/crates/asset_transport/src/lib.rs index cb8586f..ee6beb9 100644 --- a/crates/asset_transport/src/lib.rs +++ b/crates/asset_transport/src/lib.rs @@ -28,7 +28,7 @@ pub use progress::{ StageOutcome, StageScope, StageSnapshot, WorkCount, peak_resident_bytes, process_resident_bytes, }; -pub use steam::{MW2_SHORTCUT, SteamCandidate, SteamProbe, link_steam_mw2}; +pub use steam::{MW2_SHORTCUT, SteamCandidate, SteamProbe, link_steam_games}; pub use zone::{ Iw4WireFormat, Iw5ZoneMemory, T5ZoneMemory, ZoneImage, ZoneMemory, ZoneOpenError, open_zone, open_zone_shared, parse_zone_image, xfile_arena_row, zone_share_counts, diff --git a/crates/asset_transport/src/steam.rs b/crates/asset_transport/src/steam.rs index c818e98..a6e6ee0 100644 --- a/crates/asset_transport/src/steam.rs +++ b/crates/asset_transport/src/steam.rs @@ -4,16 +4,17 @@ use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; +use crate::ZoneGame; use crate::discover::GamesRoot; #[cfg(windows)] -use crate::discover::{IW4_ZONE_VERSION, peek_zone_version, search_roots}; +use crate::discover::{search_roots, zone_game_for_path}; pub const MW2_SHORTCUT: &str = "Modern Warfare 2.lnk"; #[derive(Clone, Debug, Default)] pub struct SteamProbe { pub steam_found: bool, - pub tried: Vec<(PathBuf, SteamCandidate)>, + pub tried: Vec<(ZoneGame, PathBuf, SteamCandidate)>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -26,47 +27,67 @@ pub enum SteamCandidate { } #[cfg(windows)] -pub fn link_steam_mw2(root: &GamesRoot) -> SteamProbe { +pub fn link_steam_games(root: &GamesRoot) -> SteamProbe { let mut probe = SteamProbe::default(); - let link = root.0.join(MW2_SHORTCUT); - if link.exists() || has_mw2(&search_roots(&root.0)) { + let roots = search_roots(&root.0); + let titles = [ + (ZoneGame::Iw4, MW2_SHORTCUT, "Call of Duty Modern Warfare 2"), + ( + ZoneGame::Iw5, + "Modern Warfare 3.lnk", + "Call of Duty Modern Warfare 3", + ), + (ZoneGame::T5, "Black Ops.lnk", "Call of Duty Black Ops"), + ]; + let missing = titles + .into_iter() + .filter(|(game, shortcut, _)| !root.0.join(shortcut).exists() && !has_game(&roots, *game)) + .collect::>(); + if missing.is_empty() { return probe; } let libraries = steam_libraries(); probe.steam_found = !libraries.is_empty(); - let mut valid = Vec::new(); - for library in libraries { - let path = library - .join("steamapps") - .join("common") - .join("Call of Duty Modern Warfare 2"); - let candidate = if !path.is_dir() { - SteamCandidate::Missing - } else if has_mw2(std::slice::from_ref(&path)) { - valid.push(probe.tried.len()); - SteamCandidate::Linked - } else { - SteamCandidate::NoMultiplayerData - }; - probe.tried.push((path, candidate)); - } - match valid[..] { - [index] => { - let (target, candidate) = &mut probe.tried[index]; - match create_shortcut(&link, target) { - Ok(()) => diag::info!( - Zone, - "linked Steam MW2 {} as {}", - target.display(), - link.display() - ), - Err(error) => *candidate = SteamCandidate::ShortcutFailed(error), - } + for (game, shortcut, folder) in missing { + let link = root.0.join(shortcut); + let mut valid = Vec::new(); + for library in &libraries { + let path = library.join("steamapps").join("common").join(folder); + let candidate = if !path.is_dir() { + SteamCandidate::Missing + } else if has_game(std::slice::from_ref(&path), game) { + valid.push(probe.tried.len()); + SteamCandidate::Linked + } else { + SteamCandidate::NoMultiplayerData + }; + probe.tried.push((game, path, candidate)); } - [] => {} - _ => { - for index in valid { - probe.tried[index].1 = SteamCandidate::OneOfSeveral; + match valid[..] { + [index] => { + let (_, target, candidate) = &mut probe.tried[index]; + match create_shortcut(&link, target) { + Ok(()) => diag::info!( + Zone, + "linked Steam {} as {}", + target.display(), + link.display() + ), + Err(error) => { + diag::warn!(Zone, "{error}"); + *candidate = SteamCandidate::ShortcutFailed(error); + } + } + } + [] => {} + _ => { + diag::warn!( + Zone, + "several Steam installs of {folder}; create {shortcut} to select one" + ); + for index in valid { + probe.tried[index].2 = SteamCandidate::OneOfSeveral; + } } } } @@ -74,19 +95,20 @@ pub fn link_steam_mw2(root: &GamesRoot) -> SteamProbe { } #[cfg(not(windows))] -pub fn link_steam_mw2(_root: &GamesRoot) -> SteamProbe { +pub fn link_steam_games(_root: &GamesRoot) -> SteamProbe { SteamProbe::default() } #[cfg(windows)] -fn has_mw2(roots: &[PathBuf]) -> bool { +fn has_game(roots: &[PathBuf], game: ZoneGame) -> bool { roots.iter().any(|root| { std::iter::once(root.clone()) .chain(subdirs(root)) - .flat_map(|tree| subdirs(&tree.join("zone"))) - .any(|language| { - peek_zone_version(&language.join("common_mp.ff")) == Some(IW4_ZONE_VERSION) + .flat_map(|tree| { + let zone = tree.join("zone"); + std::iter::once(zone.clone()).chain(subdirs(&zone)) }) + .any(|language| zone_game_for_path(&language.join("common_mp.ff")) == Some(game)) }) } diff --git a/crates/bootstrap/src/launch.rs b/crates/bootstrap/src/launch.rs index cf68332..fb60fb1 100644 --- a/crates/bootstrap/src/launch.rs +++ b/crates/bootstrap/src/launch.rs @@ -126,7 +126,7 @@ pub fn launch( acceptance: Option, cheats: sim::HostCheats, ) { - let steam = asset_transport::link_steam_mw2(&games); + let steam = asset_transport::link_steam_games(&games); diag::info!(Launch, "{}", asset_transport::games_root_report(&games)); if let Some(plan) = crate::frame_owner::prefer_performance_cores() { @@ -235,7 +235,11 @@ fn mw2_not_found( text.push_str("Steam was not found on this PC.\n"); } else { text.push_str("Tried in Steam:\n"); - for (folder, candidate) in &steam.tried { + for (_, folder, candidate) in steam + .tried + .iter() + .filter(|(game, _, _)| *game == asset_core::ZoneGame::Iw4) + { let outcome = match candidate { SteamCandidate::Missing => "not installed here".to_owned(), SteamCandidate::NoMultiplayerData => { diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md index f2de8dd..dd21f7a 100644 --- a/docs/WINDOWS.md +++ b/docs/WINDOWS.md @@ -19,12 +19,13 @@ IW4L/ `iw4l.exe licenses` prints the licence and notice texts compiled into the executable. The runtime reads the installations those shortcuts point to. MW2 multiplayer data -is required for the menu; BO1 and MW3 are optional. Without `Modern Warfare 2.lnk` -and without MW2 in the folder or its shortcuts, `iw4l.exe` looks in the Steam -libraries and, when exactly one has MW2 multiplayer data, creates -`Modern Warfare 2.lnk` to it. Otherwise it shows the folders it tried and how to -add the shortcut by hand: right-click inside the folder, New > Shortcut, paste the -MW2 folder path. +is required for the menu; BO1 and MW3 are optional. For each missing title, +`iw4l.exe` checks the Steam libraries and creates its shortcut when exactly one +install has supported multiplayer data. Existing shortcuts and titles already +found in the configured folders are preserved. If several installs qualify, +create a shortcut to the one you want. If MW2 is still missing, the launcher shows +the folders it tried and how to add the shortcut: right-click inside the launcher +folder, New > Shortcut, paste the game folder path. Launch `iw4l.exe`. Before starting the game or contacting QUIC, it checks its community's HTTPS manifest. An unchanged executable starts normally. An update @@ -46,5 +47,5 @@ One adjacent `.iw4l-server` is selected automatically; with several, set `IW4L_COMMUNITY` to the chosen file path before launching. See [`MASTER.md`](MASTER.md). On Windows the executable directory is the working directory. Game discovery -uses `IW4L_GAMES` or shortcuts beside `iw4l.exe`, then Steam for MW2; local `.env` settings remain +uses `IW4L_GAMES` or shortcuts beside `iw4l.exe`, then Steam for MW2, BO1 and MW3; local `.env` settings remain available for game configuration. Publishing: [`DEPLOY.md`](DEPLOY.md).