use std::{ path::{Path, PathBuf}, process::Command, }; // ── Cert installation ───────────────────────────────────────────────────────── /// Find the bridge cert in the given captures dir. pub fn find_bridge_cert(captures_dir: &str) -> Option { let p = Path::new(captures_dir).join("bridge_cert.pem"); p.exists().then_some(p) } /// Install the bridge cert into the Wine/Proton prefix cert store so the game /// trusts TLS connections to the bridge. Falls back to the system CA store. pub fn install_cert(cert_src: &Path) -> anyhow::Result<()> { // Try certutil inside the Proton prefix first (Wine cert store) let wine_result = try_wine_certutil(cert_src); if wine_result.is_ok() { return wine_result; } // Fallback: Linux system CA store via pkexec/sudo let dest = Path::new("/usr/local/share/ca-certificates/openfut-bridge.crt"); let script = format!( "cp '{}' '{}' && update-ca-certificates", cert_src.display(), dest.display() ); run_elevated(&script) } fn try_wine_certutil(cert_src: &Path) -> anyhow::Result<()> { // certutil is available inside a Wine prefix via winetricks or natively let status = Command::new("wine") .args([ "certutil", "-addstore", "-user", "Root", &cert_src.to_string_lossy(), ]) .status(); match status { Ok(s) if s.success() => Ok(()), Ok(s) => anyhow::bail!("wine certutil exited {s}"), Err(e) => anyhow::bail!("wine certutil not available: {e}"), } } pub(crate) fn run_elevated(script: &str) -> anyhow::Result<()> { let status = Command::new("pkexec").args(["sh", "-c", script]).status(); match status { Ok(s) if s.success() => Ok(()), _ => { let s = Command::new("sudo").args(["sh", "-c", script]).status()?; if s.success() { Ok(()) } else { anyhow::bail!("elevated command failed") } } } } // ── DLL hook deployment ─────────────────────────────────────────────────────── /// Deploy openfut_hook.dll into the FIFA 23 game directory and write /// openfut.cfg with the structured server configuration the hook reads. /// `cfg_contents` must be the full `openfut.cfg` body (see /// `LauncherConfig::hook_cfg_contents`) — this function does not invent any /// address itself, so a missing server can never silently become loopback. /// Uses `version.dll` as the hijack name — FIFA 23 loads it but defers to /// the system copy, so Proton picks up our local one first. pub fn deploy_hook_dll(dll_src: &Path, game_dir: &Path, cfg_contents: &str) -> anyhow::Result<()> { if !dll_src.exists() { anyhow::bail!( "Hook DLL not found at {}. Build it first with:\n\ cargo build --release --target x86_64-pc-windows-gnu\n\ (inside openfut-hook/)", dll_src.display() ); } std::fs::create_dir_all(game_dir)?; std::fs::copy(dll_src, game_dir.join("version.dll"))?; std::fs::write(game_dir.join("openfut.cfg"), cfg_contents)?; Ok(()) } /// Update only openfut.cfg without redeploying the DLL. `cfg_contents` is the /// full structured `openfut.cfg` body. pub fn update_hook_config(game_dir: &Path, cfg_contents: &str) -> anyhow::Result<()> { let cfg = game_dir.join("openfut.cfg"); if !cfg.exists() { anyhow::bail!("Hook DLL not deployed yet — deploy first."); } std::fs::write(cfg, cfg_contents)?; Ok(()) } /// Remove the deployed hook DLL from the FIFA game directory. pub fn remove_hook_dll(game_dir: &Path) -> anyhow::Result<()> { let dest = game_dir.join("version.dll"); if dest.exists() { std::fs::remove_file(&dest)?; } Ok(()) } /// Check whether our hook DLL is deployed in the game directory. pub fn hook_dll_deployed(game_dir: &Path) -> bool { game_dir.join("version.dll").exists() } /// The Steam launch options the user needs to paste in to enable the override. /// Proton loads local DLLs named in WINEDLLOVERRIDES ahead of system ones. pub const STEAM_LAUNCH_OPTIONS: &str = "WINEDLLOVERRIDES=\"version=n,b\" %command%"; // ── Game launch ─────────────────────────────────────────────────────────────── /// Launch the game via the user-provided shell command. Runs `sh -c ` /// (optionally from `workdir`), streaming stdout+stderr into `log_buf` on a /// background thread. The launcher does not assume Steam vs umu-run vs a custom /// script — whatever the user configured is what runs. pub fn launch_game( command: &str, workdir: &str, log_buf: std::sync::Arc>, ) -> anyhow::Result<()> { use std::io::{BufRead, BufReader}; use std::process::{Command, Stdio}; if command.trim().is_empty() { anyhow::bail!("No game launch command configured (set it in the Config tab)."); } let mut cmd = Command::new("sh"); cmd.arg("-c").arg(command); if !workdir.trim().is_empty() { cmd.current_dir(workdir); } cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); log_buf .lock() .unwrap() .push(format!("[launcher] launching game: {command}")); let mut child = cmd.spawn()?; if let Some(out) = child.stdout.take() { let buf = std::sync::Arc::clone(&log_buf); std::thread::spawn(move || { for line in BufReader::new(out).lines().map_while(Result::ok) { buf.lock().unwrap().push(line); } }); } if let Some(err) = child.stderr.take() { let buf = std::sync::Arc::clone(&log_buf); std::thread::spawn(move || { for line in BufReader::new(err).lines().map_while(Result::ok) { buf.lock().unwrap().push(line); } }); } // Reap the child in the background so a finished game doesn't linger as a // zombie; we don't block the UI on it. std::thread::spawn(move || { let _ = child.wait(); log_buf .lock() .unwrap() .push("[launcher] game process exited.".to_string()); }); Ok(()) }