From 9b5da57f129e9206bda3fb24920f68f499602bd9 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 25 Jun 2026 21:05:35 -0700 Subject: [PATCH] feat: replace hosts file with DLL injection hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds openfut-hook/, a Windows DLL (cdylib, x86_64-pc-windows-gnu) that patches the IAT of FIFA 23 at load time to redirect getaddrinfo calls for fut.ea.com / utas.*.fut.ea.com to 127.0.0.1, sending all FUT traffic to the local bridge — no /etc/hosts changes needed. Deployment: the launcher copies openfut_hook.dll into the FIFA 23 game folder as version.dll (a DLL FIFA loads but delegates to system). Proton picks up the local copy automatically when you set: WINEDLLOVERRIDES="version=n,b" %command% in Steam launch options. Also updates cert install to try the Wine/Proton cert store (wine certutil) before falling back to the Linux system CA store, and removes all hosts file code from setup.rs / app.rs. Co-Authored-By: Claude Sonnet 4.6 --- openfut-hook/.cargo/config.toml | 3 + openfut-hook/Cargo.lock | 83 ++++++++++++++++ openfut-hook/Cargo.toml | 21 ++++ openfut-hook/src/hooks.rs | 44 +++++++++ openfut-hook/src/iat.rs | 135 ++++++++++++++++++++++++++ openfut-hook/src/lib.rs | 37 +++++++ src/app.rs | 103 ++++++++++++++------ src/config.rs | 13 +++ src/setup.rs | 167 ++++++++++++++------------------ 9 files changed, 482 insertions(+), 124 deletions(-) create mode 100644 openfut-hook/.cargo/config.toml create mode 100644 openfut-hook/Cargo.lock create mode 100644 openfut-hook/Cargo.toml create mode 100644 openfut-hook/src/hooks.rs create mode 100644 openfut-hook/src/iat.rs create mode 100644 openfut-hook/src/lib.rs diff --git a/openfut-hook/.cargo/config.toml b/openfut-hook/.cargo/config.toml new file mode 100644 index 0000000..2e1dac3 --- /dev/null +++ b/openfut-hook/.cargo/config.toml @@ -0,0 +1,3 @@ +[target.x86_64-pc-windows-gnu] +linker = "x86_64-w64-mingw32-gcc" +ar = "x86_64-w64-mingw32-ar" diff --git a/openfut-hook/Cargo.lock b/openfut-hook/Cargo.lock new file mode 100644 index 0000000..f02d3a6 --- /dev/null +++ b/openfut-hook/Cargo.lock @@ -0,0 +1,83 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "openfut-hook" +version = "0.1.0" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/openfut-hook/Cargo.toml b/openfut-hook/Cargo.toml new file mode 100644 index 0000000..8528be9 --- /dev/null +++ b/openfut-hook/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "openfut-hook" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +windows-sys = { version = "0.59", features = [ + "Win32_Foundation", + "Win32_System_LibraryLoader", + "Win32_System_Memory", + "Win32_System_SystemServices", + "Win32_Networking_WinSock", +] } + +[profile.release] +opt-level = "s" +strip = true +panic = "abort" diff --git a/openfut-hook/src/hooks.rs b/openfut-hook/src/hooks.rs new file mode 100644 index 0000000..c925a54 --- /dev/null +++ b/openfut-hook/src/hooks.rs @@ -0,0 +1,44 @@ +/// Network hooks — redirect EA FUT hostnames to localhost. +use std::{ffi::CStr, sync::OnceLock}; + +use windows_sys::Win32::Networking::WinSock::{ADDRINFOA, getaddrinfo as sys_getaddrinfo}; + +const INTERCEPT: &[&str] = &[ + "fut.ea.com", + "utas.mob.v4.fut.ea.com", + "utas.s2.fut.ea.com", +]; + +type GetaddrinfoFn = unsafe extern "system" fn( + *const u8, + *const u8, + *const ADDRINFOA, + *mut *mut ADDRINFOA, +) -> i32; + +static REAL: OnceLock = OnceLock::new(); + +pub fn set_real(f: GetaddrinfoFn) { + let _ = REAL.set(f); +} + +pub unsafe extern "system" fn hooked_getaddrinfo( + node_name: *const u8, + service_name: *const u8, + hints: *const ADDRINFOA, + result: *mut *mut ADDRINFOA, +) -> i32 { + if !node_name.is_null() { + if let Ok(host) = CStr::from_ptr(node_name as *const i8).to_str() { + for target in INTERCEPT { + if host.eq_ignore_ascii_case(target) { + let local = b"127.0.0.1\0"; + let real = REAL.get().copied().unwrap_or(sys_getaddrinfo); + return real(local.as_ptr(), service_name, hints, result); + } + } + } + } + let real = REAL.get().copied().unwrap_or(sys_getaddrinfo); + real(node_name, service_name, hints, result) +} diff --git a/openfut-hook/src/iat.rs b/openfut-hook/src/iat.rs new file mode 100644 index 0000000..998dda9 --- /dev/null +++ b/openfut-hook/src/iat.rs @@ -0,0 +1,135 @@ +/// IAT (Import Address Table) patching. +/// +/// We define the PE structs ourselves rather than pulling in windows-sys PE +/// headers (which are in a different crate / feature path). +use windows_sys::Win32::{ + Foundation::HMODULE, + System::{ + LibraryLoader::{GetModuleHandleA, GetProcAddress}, + Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE}, + }, +}; + +// ── Minimal PE struct definitions ───────────────────────────────────────────── + +#[repr(C)] +struct ImageDosHeader { + e_magic: u16, + _pad: [u16; 29], + e_lfanew: i32, +} + +#[repr(C)] +struct ImageFileHeader { + machine: u16, + number_of_sections: u16, + time_date_stamp: u32, + pointer_to_symbol_table: u32, + number_of_symbols: u32, + size_of_optional_header: u16, + characteristics: u16, +} + +#[repr(C)] +struct ImageDataDirectory { + virtual_address: u32, + size: u32, +} + +#[repr(C)] +struct ImageOptionalHeader64 { + magic: u16, + _pad: [u8; 106], + data_directory: [ImageDataDirectory; 16], +} + +#[repr(C)] +struct ImageNtHeaders64 { + signature: u32, + file_header: ImageFileHeader, + optional_header: ImageOptionalHeader64, +} + +#[repr(C)] +struct ImageImportDescriptor { + original_first_thunk: u32, + time_date_stamp: u32, + forwarder_chain: u32, + name: u32, + first_thunk: u32, +} + +// ── IAT patching ────────────────────────────────────────────────────────────── + +/// Replace every IAT slot in the main module that currently holds +/// `original_fn` with `hook_fn`. +/// +/// # Safety +/// Caller must ensure hook_fn has the same calling convention and signature. +pub unsafe fn patch_iat(original_fn: *const (), hook_fn: *const ()) -> usize { + let module = GetModuleHandleA(std::ptr::null()); + patch_module(module, original_fn, hook_fn) +} + +unsafe fn patch_module( + module: HMODULE, + original_fn: *const (), + hook_fn: *const (), +) -> usize { + if module.is_null() { + return 0; + } + let base = module as usize; + let dos = base as *const ImageDosHeader; + if (*dos).e_magic != 0x5A4D { + return 0; + } + + let nt = (base + (*dos).e_lfanew as usize) as *const ImageNtHeaders64; + + let import_rva = (*nt).optional_header.data_directory[1].virtual_address as usize; + if import_rva == 0 { + return 0; + } + + let mut desc = (base + import_rva) as *const ImageImportDescriptor; + let mut count = 0usize; + + while (*desc).name != 0 { + let ft = (*desc).first_thunk as usize; + let iat_slot = (base + ft) as *mut usize; + let mut i = 0usize; + + loop { + let val = *iat_slot.add(i); + if val == 0 { + break; + } + if val == original_fn as usize { + let target = iat_slot.add(i) as *const std::ffi::c_void; + let mut old: u32 = 0; + VirtualProtect(target, std::mem::size_of::(), PAGE_EXECUTE_READWRITE, &mut old); + *iat_slot.add(i) = hook_fn as usize; + VirtualProtect(target, std::mem::size_of::(), old, &mut old); + count += 1; + } + i += 1; + } + + desc = desc.add(1); + } + + count +} + +/// Resolve the address of an exported function from an already-loaded DLL. +pub unsafe fn resolve(dll: &[u8], fn_name: &[u8]) -> *const () { + let module = GetModuleHandleA(dll.as_ptr()); + if module.is_null() { + return std::ptr::null(); + } + match GetProcAddress(module, fn_name.as_ptr()) { + Some(f) => f as *const (), + None => std::ptr::null(), + } +} diff --git a/openfut-hook/src/lib.rs b/openfut-hook/src/lib.rs new file mode 100644 index 0000000..d4e5b3c --- /dev/null +++ b/openfut-hook/src/lib.rs @@ -0,0 +1,37 @@ +mod hooks; +mod iat; + +use windows_sys::Win32::{ + Foundation::{BOOL, HMODULE, TRUE}, + System::SystemServices::DLL_PROCESS_ATTACH, + Networking::WinSock::ADDRINFOA, +}; + +#[no_mangle] +pub unsafe extern "system" fn DllMain( + _module: HMODULE, + reason: u32, + _reserved: *mut (), +) -> BOOL { + if reason == DLL_PROCESS_ATTACH { + install_hooks(); + } + TRUE +} + +unsafe fn install_hooks() { + let real_ptr = iat::resolve(b"ws2_32.dll\0", b"getaddrinfo\0"); + if real_ptr.is_null() { + return; + } + + let real_fn: unsafe extern "system" fn( + *const u8, + *const u8, + *const ADDRINFOA, + *mut *mut ADDRINFOA, + ) -> i32 = std::mem::transmute(real_ptr); + + hooks::set_real(real_fn); + iat::patch_iat(real_ptr, hooks::hooked_getaddrinfo as *const ()); +} diff --git a/src/app.rs b/src/app.rs index e87882e..2be1992 100644 --- a/src/app.rs +++ b/src/app.rs @@ -33,7 +33,7 @@ pub struct LauncherApp { log_follow: bool, // Setup state - hosts_configured: bool, + hook_deployed: bool, cert_path: Option, setup_message: Option<(bool, String)>, // (success, text) } @@ -54,7 +54,8 @@ impl LauncherApp { let config = LauncherConfig::load(); let cert_path = setup::find_bridge_cert(&config.bridge_captures_dir); - let hosts_configured = setup::hosts_configured(); + let hook_deployed = + setup::hook_dll_deployed(std::path::Path::new(&config.fifa_game_dir)); Self { config, @@ -66,7 +67,7 @@ impl LauncherApp { active_tab: Tab::Dashboard, log_tab: 0, log_follow: true, - hosts_configured, + hook_deployed, cert_path, setup_message: None, } @@ -189,11 +190,11 @@ impl LauncherApp { .num_columns(2) .spacing([12.0, 4.0]) .show(ui, |ui| { - ui.label("Hosts file:"); - if self.hosts_configured { - ui.colored_label(Color32::from_rgb(80, 200, 120), "Configured"); + ui.label("Hook DLL:"); + if self.hook_deployed { + ui.colored_label(Color32::from_rgb(80, 200, 120), "Deployed (version.dll)"); } else { - ui.colored_label(Color32::from_rgb(220, 150, 0), "Not configured"); + ui.colored_label(Color32::from_rgb(220, 150, 0), "Not deployed"); } ui.end_row(); @@ -263,51 +264,81 @@ impl LauncherApp { } fn ui_setup(&mut self, ui: &mut Ui) { + use std::path::Path; + ui.add_space(8.0); ui.heading("System Setup"); ui.add_space(4.0); - ui.label("These steps route FIFA 23 traffic through the emulator. Both require elevated privileges (polkit/sudo)."); + ui.label("These steps route FIFA 23 traffic through the emulator via DLL injection (no hosts file changes needed)."); ui.add_space(12.0); - // ── Hosts file ──────────────────────────────────────────────────────── + // ── Hook DLL deployment ─────────────────────────────────────────────── ui.group(|ui| { ui.set_min_width(ui.available_width()); - ui.strong("Step 1 — Redirect EA hostnames"); + ui.strong("Step 1 — Deploy network hook DLL"); ui.add_space(4.0); - ui.label("Adds entries to /etc/hosts so the game connects to the bridge instead of EA's servers."); + ui.label("Copies openfut_hook.dll into the FIFA 23 folder as version.dll. \ + When Proton loads the game it will intercept network calls and redirect \ + EA hostnames to the local bridge — no hosts file changes required."); ui.add_space(6.0); - // Refresh status - self.hosts_configured = setup::hosts_configured(); + let game_dir = Path::new(&self.config.fifa_game_dir); + self.hook_deployed = setup::hook_dll_deployed(game_dir); + + let dll_src = Path::new(&self.config.hook_dll_path); + let dll_built = dll_src.exists(); + + if !dll_built { + ui.colored_label( + Color32::from_rgb(220, 150, 0), + "⚠ Hook DLL not built yet. Run in openfut-hook/:", + ); + ui.monospace("cargo build --release --target x86_64-pc-windows-gnu"); + ui.add_space(4.0); + } ui.horizontal(|ui| { - if self.hosts_configured { - ui.colored_label(Color32::from_rgb(80, 200, 120), "✔ Configured"); + if self.hook_deployed { + ui.colored_label(Color32::from_rgb(80, 200, 120), "✔ Deployed (version.dll)"); ui.add_space(8.0); - if ui.button("Remove entries").clicked() { - match setup::remove_hosts_entries() { + if ui.button("Remove").clicked() { + match setup::remove_hook_dll(game_dir) { Ok(()) => { - self.setup_message = Some((true, "Hosts entries removed.".into())); - self.hosts_configured = false; + self.setup_message = Some((true, "Hook DLL removed.".into())); + self.hook_deployed = false; } Err(e) => self.setup_message = Some((false, format!("Failed: {e}"))), } } } else { - ui.colored_label(Color32::from_rgb(220, 60, 60), "✘ Not configured"); + ui.colored_label(Color32::from_rgb(220, 60, 60), "✘ Not deployed"); ui.add_space(8.0); - if ui.button("Add entries").clicked() { - match setup::add_hosts_entries() { + if ui.add_enabled(dll_built, egui::Button::new("Deploy")).clicked() { + match setup::deploy_hook_dll(dll_src, game_dir) { Ok(()) => { - self.setup_message = Some((true, "Hosts entries added.".into())); - self.hosts_configured = true; + self.setup_message = Some((true, "Hook DLL deployed as version.dll.".into())); + self.hook_deployed = true; } Err(e) => self.setup_message = Some((false, format!("Failed: {e}"))), } } } }); + + ui.add_space(8.0); + ui.separator(); + ui.add_space(4.0); + ui.strong("Steam Launch Options"); + ui.label("Paste this into FIFA 23 → Properties → Launch Options in Steam:"); + ui.add_space(2.0); + let launch_opt = setup::STEAM_LAUNCH_OPTIONS; + ui.horizontal(|ui| { + ui.monospace(launch_opt); + if ui.small_button("Copy").clicked() { + ui.output_mut(|o| o.copied_text = launch_opt.to_string()); + } + }); }); ui.add_space(12.0); @@ -317,11 +348,10 @@ impl LauncherApp { ui.set_min_width(ui.available_width()); ui.strong("Step 2 — Install TLS certificate"); ui.add_space(4.0); - ui.label("Installs the bridge's self-signed cert into the system CA store so the game accepts HTTPS connections."); + ui.label("Installs the bridge's self-signed cert into the Wine/Proton cert store \ + so the game accepts HTTPS connections to the bridge."); ui.add_space(6.0); - - // Refresh cert path self.cert_path = setup::find_bridge_cert(&self.config.bridge_captures_dir); match &self.cert_path.clone() { @@ -334,7 +364,7 @@ impl LauncherApp { Some(cert) => { ui.label(format!("Cert: {}", cert.display())); ui.add_space(4.0); - if ui.button("Install cert (requires sudo)").clicked() { + if ui.button("Install cert (Wine cert store)").clicked() { match setup::install_cert(cert) { Ok(()) => self.setup_message = Some((true, "Certificate installed.".into())), Err(e) => self.setup_message = Some((false, format!("Failed: {e}"))), @@ -346,7 +376,6 @@ impl LauncherApp { ui.add_space(12.0); - // ── Status message ──────────────────────────────────────────────────── if let Some((ok, msg)) = &self.setup_message { let color = if *ok { Color32::from_rgb(80, 200, 120) @@ -416,6 +445,22 @@ impl LauncherApp { ui.label("TLS enabled:"); changed |= ui.checkbox(&mut self.config.bridge_tls_enabled, "").changed(); ui.end_row(); + + ui.add_space(4.0); + ui.label(""); + ui.end_row(); + + ui.strong("Hook DLL"); + ui.label(""); + ui.end_row(); + + ui.label("Hook DLL path:"); + changed |= ui.text_edit_singleline(&mut self.config.hook_dll_path).changed(); + ui.end_row(); + + ui.label("FIFA 23 game dir:"); + changed |= ui.text_edit_singleline(&mut self.config.fifa_game_dir).changed(); + ui.end_row(); }); if changed { diff --git a/src/config.rs b/src/config.rs index f707d21..687406e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,10 @@ pub struct LauncherConfig { pub bridge_captures_dir: String, pub bridge_core_url: String, pub bridge_tls_enabled: bool, + /// Path to the built openfut_hook.dll (Windows DLL for Proton injection). + pub hook_dll_path: String, + /// FIFA 23 game folder inside the Proton prefix (where the DLL is deployed). + pub fifa_game_dir: String, } impl Default for LauncherConfig { @@ -41,6 +45,15 @@ impl Default for LauncherConfig { .into(), bridge_core_url: "http://127.0.0.1:8080".into(), bridge_tls_enabled: true, + hook_dll_path: base + .join("openfut-launcher/openfut-hook/target/x86_64-pc-windows-gnu/release/openfut_hook.dll") + .to_string_lossy() + .into(), + fifa_game_dir: dirs::home_dir() + .map(|h| h.join(".steam/steam/steamapps/common/FIFA 23")) + .unwrap_or_default() + .to_string_lossy() + .into(), } } } diff --git a/src/setup.rs b/src/setup.rs index 49e9a9a..d1bee91 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -1,129 +1,106 @@ -use std::process::Command; - -/// EA FUT hostnames the bridge needs to intercept. -const INTERCEPT_HOSTS: &[&str] = &[ - "fut.ea.com", - "utas.mob.v4.fut.ea.com", - "utas.s2.fut.ea.com", -]; - -const HOSTS_MARKER_START: &str = "# BEGIN openfut-launcher"; -const HOSTS_MARKER_END: &str = "# END openfut-launcher"; +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 = std::path::Path::new(captures_dir).join("bridge_cert.pem"); +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 system CA store. -/// On Linux, copies to /usr/local/share/ca-certificates/ and runs -/// update-ca-certificates via pkexec (polkit graphical sudo). -pub fn install_cert(cert_src: &std::path::Path) -> anyhow::Result<()> { - let dest = std::path::Path::new("/usr/local/share/ca-certificates/openfut-bridge.crt"); +/// 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; + } - // Write a small shell script that pkexec can run as root + // 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) +} - let status = Command::new("pkexec") - .args(["sh", "-c", &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!("pkexec exited with status {s}"), - Err(e) => { - // pkexec not available — try sudo - let status = Command::new("sudo") - .args(["sh", "-c", &script]) - .status()?; - if status.success() { - Ok(()) - } else { - anyhow::bail!("cert install failed: {e}") - } - } + Ok(s) => anyhow::bail!("wine certutil exited {s}"), + Err(e) => anyhow::bail!("wine certutil not available: {e}"), } } -// ── Hosts file management ───────────────────────────────────────────────────── - -pub fn hosts_configured() -> bool { - let content = std::fs::read_to_string("/etc/hosts").unwrap_or_default(); - content.contains(HOSTS_MARKER_START) -} - -/// Add OpenFUT intercept entries to /etc/hosts (requires root). -pub fn add_hosts_entries() -> anyhow::Result<()> { - let current = std::fs::read_to_string("/etc/hosts")?; - if current.contains(HOSTS_MARKER_START) { - return Ok(()); - } - - let entries: String = INTERCEPT_HOSTS - .iter() - .map(|h| format!("127.0.0.1 {h}\n")) - .collect(); - - let addition = format!("\n{HOSTS_MARKER_START}\n{entries}{HOSTS_MARKER_END}\n"); - let new_content = format!("{current}{addition}"); - - write_hosts(&new_content) -} - -/// Remove OpenFUT intercept entries from /etc/hosts (requires root). -pub fn remove_hosts_entries() -> anyhow::Result<()> { - let current = std::fs::read_to_string("/etc/hosts")?; - if !current.contains(HOSTS_MARKER_START) { - return Ok(()); - } - - let mut out = String::new(); - let mut skip = false; - for line in current.lines() { - if line.trim() == HOSTS_MARKER_START { - skip = true; - continue; - } - if line.trim() == HOSTS_MARKER_END { - skip = false; - continue; - } - if !skip { - out.push_str(line); - out.push('\n'); - } - } - - write_hosts(out.trim_end()) -} - -fn write_hosts(content: &str) -> anyhow::Result<()> { - // Write to a temp file, then mv into place with elevated privileges - let tmp = "/tmp/openfut_hosts_tmp"; - std::fs::write(tmp, content)?; - - let script = format!("mv '{tmp}' /etc/hosts && chmod 644 /etc/hosts"); - - let status = Command::new("pkexec").args(["sh", "-c", &script]).status(); +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]) + .args(["sh", "-c", script]) .status()?; if s.success() { Ok(()) } else { - anyhow::bail!("failed to write /etc/hosts") + anyhow::bail!("elevated command failed") } } } } + +// ── DLL hook deployment ─────────────────────────────────────────────────────── + +/// Deploy openfut_hook.dll into the FIFA 23 game directory. +/// Uses a name that FIFA 23 loads but doesn't need from system: `version.dll`. +/// Proton will prefer the local copy over the system one. +pub fn deploy_hook_dll(dll_src: &Path, game_dir: &Path) -> 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)?; + let dest = game_dir.join("version.dll"); + std::fs::copy(dll_src, &dest)?; + 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%";