diff --git a/openfut-hook/src/config.rs b/openfut-hook/src/config.rs new file mode 100644 index 0000000..f50dc3b --- /dev/null +++ b/openfut-hook/src/config.rs @@ -0,0 +1,32 @@ +/// Reads openfut.cfg from the same directory as this DLL. +/// +/// The file contains a single line: the IP the hook should redirect EA +/// hostnames to, e.g. "192.168.1.10" or "127.0.0.1". +/// Falls back to 127.0.0.1 if the file is missing or unreadable. +use windows_sys::Win32::System::LibraryLoader::GetModuleFileNameA; + +pub fn read_redirect_ip(module: windows_sys::Win32::Foundation::HMODULE) -> String { + if let Some(cfg_path) = config_path(module) { + if let Ok(content) = std::fs::read_to_string(&cfg_path) { + let ip = content.trim().to_string(); + if !ip.is_empty() { + return ip; + } + } + } + "127.0.0.1".to_string() +} + +fn config_path(module: windows_sys::Win32::Foundation::HMODULE) -> Option { + let mut buf = vec![0u8; 512]; + let len = unsafe { GetModuleFileNameA(module, buf.as_mut_ptr(), buf.len() as u32) }; + if len == 0 { + return None; + } + let path = std::ffi::CStr::from_bytes_until_nul(&buf[..len as usize + 1]) + .ok()? + .to_str() + .ok()?; + let dll_path = std::path::Path::new(path); + Some(dll_path.parent()?.join("openfut.cfg")) +} diff --git a/openfut-hook/src/hooks.rs b/openfut-hook/src/hooks.rs index c925a54..e28b585 100644 --- a/openfut-hook/src/hooks.rs +++ b/openfut-hook/src/hooks.rs @@ -1,4 +1,3 @@ -/// 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}; @@ -17,11 +16,19 @@ type GetaddrinfoFn = unsafe extern "system" fn( ) -> i32; static REAL: OnceLock = OnceLock::new(); +// Stored as a NUL-terminated byte string so the hook can pass it to getaddrinfo. +static REDIRECT_IP: OnceLock> = OnceLock::new(); pub fn set_real(f: GetaddrinfoFn) { let _ = REAL.set(f); } +pub fn set_redirect_ip(ip: String) { + let mut bytes = ip.into_bytes(); + bytes.push(0); // NUL-terminate for passing to getaddrinfo + let _ = REDIRECT_IP.set(bytes); +} + pub unsafe extern "system" fn hooked_getaddrinfo( node_name: *const u8, service_name: *const u8, @@ -32,9 +39,12 @@ pub unsafe extern "system" fn hooked_getaddrinfo( 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 redirect = REDIRECT_IP + .get() + .map(|v| v.as_ptr()) + .unwrap_or(b"127.0.0.1\0".as_ptr()); let real = REAL.get().copied().unwrap_or(sys_getaddrinfo); - return real(local.as_ptr(), service_name, hints, result); + return real(redirect, service_name, hints, result); } } } diff --git a/openfut-hook/src/lib.rs b/openfut-hook/src/lib.rs index d4e5b3c..ba65a8b 100644 --- a/openfut-hook/src/lib.rs +++ b/openfut-hook/src/lib.rs @@ -1,3 +1,4 @@ +mod config; mod hooks; mod iat; @@ -9,17 +10,21 @@ use windows_sys::Win32::{ #[no_mangle] pub unsafe extern "system" fn DllMain( - _module: HMODULE, + module: HMODULE, reason: u32, _reserved: *mut (), ) -> BOOL { if reason == DLL_PROCESS_ATTACH { - install_hooks(); + install_hooks(module); } TRUE } -unsafe fn install_hooks() { +unsafe fn install_hooks(module: HMODULE) { + // Load redirect IP from openfut.cfg before patching + let ip = config::read_redirect_ip(module); + hooks::set_redirect_ip(ip); + let real_ptr = iat::resolve(b"ws2_32.dll\0", b"getaddrinfo\0"); if real_ptr.is_null() { return; diff --git a/src/app.rs b/src/app.rs index 2be1992..db836fa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -315,7 +315,11 @@ impl LauncherApp { ui.colored_label(Color32::from_rgb(220, 60, 60), "✘ Not deployed"); ui.add_space(8.0); if ui.add_enabled(dll_built, egui::Button::new("Deploy")).clicked() { - match setup::deploy_hook_dll(dll_src, game_dir) { + match setup::deploy_hook_dll( + dll_src, + game_dir, + &self.config.hook_redirect_ip, + ) { Ok(()) => { self.setup_message = Some((true, "Hook DLL deployed as version.dll.".into())); self.hook_deployed = true; @@ -326,6 +330,38 @@ impl LauncherApp { } }); + ui.add_space(8.0); + + // IP configuration — always editable; Update writes openfut.cfg in-place + ui.horizontal(|ui| { + ui.label("Redirect IP:"); + let changed = ui + .add(egui::TextEdit::singleline(&mut self.config.hook_redirect_ip) + .desired_width(160.0)) + .changed(); + if changed { + self.config_dirty = true; + } + if ui.add_enabled(self.hook_deployed, egui::Button::new("Update")).clicked() { + match setup::update_hook_config( + Path::new(&self.config.fifa_game_dir), + &self.config.hook_redirect_ip, + ) { + Ok(()) => { + self.config.save(); + self.config_dirty = false; + self.setup_message = Some((true, format!( + "Config updated — hook will redirect to {}.", + self.config.hook_redirect_ip + ))); + } + Err(e) => self.setup_message = Some((false, format!("Failed: {e}"))), + } + } + }); + ui.label(egui::RichText::new("Tip: use 127.0.0.1 if the emulator is on this machine, or the LAN IP if it's on another.") + .weak().small()); + ui.add_space(8.0); ui.separator(); ui.add_space(4.0); @@ -461,6 +497,10 @@ impl LauncherApp { ui.label("FIFA 23 game dir:"); changed |= ui.text_edit_singleline(&mut self.config.fifa_game_dir).changed(); ui.end_row(); + + ui.label("Redirect IP:"); + changed |= ui.text_edit_singleline(&mut self.config.hook_redirect_ip).changed(); + ui.end_row(); }); if changed { diff --git a/src/config.rs b/src/config.rs index 687406e..4c0c161 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,8 @@ pub struct LauncherConfig { pub hook_dll_path: String, /// FIFA 23 game folder inside the Proton prefix (where the DLL is deployed). pub fifa_game_dir: String, + /// IP the hook DLL redirects EA hostnames to (written to openfut.cfg). + pub hook_redirect_ip: String, } impl Default for LauncherConfig { @@ -54,6 +56,7 @@ impl Default for LauncherConfig { .unwrap_or_default() .to_string_lossy() .into(), + hook_redirect_ip: "127.0.0.1".into(), } } } diff --git a/src/setup.rs b/src/setup.rs index d1bee91..e9f94dc 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -68,10 +68,11 @@ fn run_elevated(script: &str) -> anyhow::Result<()> { // ── 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<()> { +/// Deploy openfut_hook.dll into the FIFA 23 game directory and write +/// openfut.cfg with the redirect IP the hook will use. +/// 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, redirect_ip: &str) -> anyhow::Result<()> { if !dll_src.exists() { anyhow::bail!( "Hook DLL not found at {}. Build it first with:\n\ @@ -81,8 +82,18 @@ pub fn deploy_hook_dll(dll_src: &Path, game_dir: &Path) -> anyhow::Result<()> { ); } std::fs::create_dir_all(game_dir)?; - let dest = game_dir.join("version.dll"); - std::fs::copy(dll_src, &dest)?; + std::fs::copy(dll_src, game_dir.join("version.dll"))?; + std::fs::write(game_dir.join("openfut.cfg"), redirect_ip)?; + Ok(()) +} + +/// Update only openfut.cfg without redeploying the DLL. +pub fn update_hook_config(game_dir: &Path, redirect_ip: &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, redirect_ip)?; Ok(()) }