feat: make hook redirect IP configurable from launcher
The DLL now reads openfut.cfg from its own directory on DLL_PROCESS_ATTACH and uses the IP it contains as the redirect target instead of hardcoding 127.0.0.1. Falls back to 127.0.0.1 if the file is absent. The launcher writes openfut.cfg alongside version.dll when deploying, and the Setup tab exposes a "Redirect IP" field with an "Update" button that rewrites openfut.cfg in-place without redeploying the DLL. Useful when running the emulator on a different machine on the LAN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<std::path::PathBuf> {
|
||||
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"))
|
||||
}
|
||||
@@ -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<GetaddrinfoFn> = OnceLock::new();
|
||||
// Stored as a NUL-terminated byte string so the hook can pass it to getaddrinfo.
|
||||
static REDIRECT_IP: OnceLock<Vec<u8>> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+41
-1
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-6
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user