diff --git a/Cargo.lock b/Cargo.lock
index 24d9295..bc53fc9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2293,6 +2293,7 @@ dependencies = [
"eframe",
"egui",
"openfut-common",
+ "parking_lot",
"serde",
"serde_json",
"tokio",
diff --git a/openfut-common/src/lib.rs b/openfut-common/src/lib.rs
index 2bd5033..96724d0 100644
--- a/openfut-common/src/lib.rs
+++ b/openfut-common/src/lib.rs
@@ -53,6 +53,12 @@ pub mod default_ports {
pub const BLAZE_REDIRECTOR: u16 = 42127;
/// OpenFUT FIFA 17 Blaze main listener.
pub const BLAZE_MAIN: u16 = 42130;
+ /// OpenFUT FUT web-file (CDN) content server. Unlike the others this is not
+ /// an EA redirect target: the client never dials it directly, because its
+ /// `RS4::ServerSettings` CDN base arrives EMPTY in the emulator. The hook
+ /// supplies the missing `/fut/` prefix, and the base is built from the
+ /// configured server host plus this port.
+ pub const FUT_CONTENT: u16 = 8110;
}
/// OpenFUT destination ports. Each field is where an intercepted EA source port
@@ -66,6 +72,9 @@ pub struct OpenFutPorts {
pub blaze_redirector: u16,
/// Destination for EA :42127 traffic (Blaze main).
pub blaze_main: u16,
+ /// FUT web-file content server. Not a redirect destination — see
+ /// [`default_ports::FUT_CONTENT`].
+ pub fut_content: u16,
}
impl Default for OpenFutPorts {
@@ -74,6 +83,7 @@ impl Default for OpenFutPorts {
https: default_ports::HTTPS,
blaze_redirector: default_ports::BLAZE_REDIRECTOR,
blaze_main: default_ports::BLAZE_MAIN,
+ fut_content: default_ports::FUT_CONTENT,
}
}
}
@@ -209,6 +219,7 @@ impl ServerConfig {
"https_port" => ports.https = parse_port(value)?,
"blaze_redirector_port" => ports.blaze_redirector = parse_port(value)?,
"blaze_main_port" => ports.blaze_main = parse_port(value)?,
+ "fut_content_port" => ports.fut_content = parse_port(value)?,
other => {
return Err(ConfigError::MalformedConfig(format!(
"line {}: unknown key '{other}'",
@@ -225,8 +236,27 @@ impl ServerConfig {
/// Serialize to the structured `openfut.cfg` format.
pub fn to_cfg_string(&self) -> String {
format!(
- "host={}\nhttps_port={}\nblaze_redirector_port={}\nblaze_main_port={}\n",
- self.host, self.ports.https, self.ports.blaze_redirector, self.ports.blaze_main
+ "host={}\nhttps_port={}\nblaze_redirector_port={}\nblaze_main_port={}\nfut_content_port={}\n",
+ self.host,
+ self.ports.https,
+ self.ports.blaze_redirector,
+ self.ports.blaze_main,
+ self.ports.fut_content
+ )
+ }
+
+ /// Base URL the FUT web-file (CDN) prefix is built from, e.g.
+ /// `http://10.10.0.120:8110/fut/`.
+ ///
+ /// The client's `RS4::ServerSettings` CDN base arrives EMPTY in the emulator,
+ /// so FUT web-file urls reach the download entry point as bare relative paths
+ /// and fail. The hook supplies this prefix. Built from the SAME configured
+ /// host as every other redirect, so a lab address is never compiled in.
+ pub fn fut_content_base(&self) -> String {
+ format!(
+ "http://{}:{}/fut/",
+ self.host.trim(),
+ self.ports.fut_content
)
}
@@ -414,12 +444,36 @@ mod tests {
https: 8443,
blaze_redirector: 10041,
blaze_main: 42127,
+ fut_content: 8110,
},
};
let s = c.to_cfg_string();
assert_eq!(ServerConfig::parse(&s).unwrap(), c);
}
+ /// The FUT web-file prefix follows the CONFIGURED server, so no lab address
+ /// is ever compiled into the hook.
+ #[test]
+ fn fut_content_base_follows_the_configured_host() {
+ let c = ServerConfig::parse("host=192.168.1.50\n").unwrap();
+ assert_eq!(c.fut_content_base(), "http://192.168.1.50:8110/fut/");
+
+ let c = ServerConfig::parse("host=fut.mylan.home\nfut_content_port=9110\n").unwrap();
+ assert_eq!(c.fut_content_base(), "http://fut.mylan.home:9110/fut/");
+ }
+
+ /// A cfg written before `fut_content_port` existed must still parse, taking
+ /// the default rather than failing the whole config (which would disarm the
+ /// network redirect too).
+ #[test]
+ fn cfg_without_content_port_takes_the_default() {
+ let c = ServerConfig::parse(
+ "host=10.0.0.5\nhttps_port=8443\nblaze_redirector_port=42127\nblaze_main_port=42130\n",
+ )
+ .unwrap();
+ assert_eq!(c.ports.fut_content, default_ports::FUT_CONTENT);
+ }
+
#[test]
fn configured_ipv4_becomes_correct_sockaddr() {
// Resolve an IPv4 literal and confirm the sin_addr value.
diff --git a/openfut-hook/Cargo.lock b/openfut-hook/Cargo.lock
index f02d3a6..c75ff37 100644
--- a/openfut-hook/Cargo.lock
+++ b/openfut-hook/Cargo.lock
@@ -2,10 +2,15 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "openfut-common"
+version = "0.1.0"
+
[[package]]
name = "openfut-hook"
version = "0.1.0"
dependencies = [
+ "openfut-common",
"windows-sys",
]
diff --git a/openfut-hook/Cargo.toml b/openfut-hook/Cargo.toml
index 6c8e75b..2d9ab23 100644
--- a/openfut-hook/Cargo.toml
+++ b/openfut-hook/Cargo.toml
@@ -39,6 +39,10 @@ windows-sys = { version = "0.59", features = [
"Win32_System_Diagnostics_Debug",
"Win32_System_Kernel",
] }
+# Single source of truth for the OpenFUT redirect config (openfut.cfg schema,
+# EA-port -> OpenFUT-port map, WinSock byte-order helpers). Shared with the
+# launcher so the hook and openfut.cfg agree by construction.
+openfut-common = { path = "../openfut-common" }
[profile.release]
opt-level = "s"
diff --git a/openfut-hook/src/season_trace.rs b/openfut-hook/src/season_trace.rs
index c92d208..a97f29f 100644
--- a/openfut-hook/src/season_trace.rs
+++ b/openfut-hook/src/season_trace.rs
@@ -13,17 +13,18 @@
//! (no rip-relative / rel32 in the copied bytes).
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
+use std::sync::OnceLock;
use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache;
+use windows_sys::Win32::System::Diagnostics::Debug::{
+ AddVectoredExceptionHandler, EXCEPTION_POINTERS,
+};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
use windows_sys::Win32::System::Memory::{
VirtualAlloc, VirtualProtect, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READ,
PAGE_EXECUTE_READWRITE, PAGE_READWRITE,
};
use windows_sys::Win32::System::Threading::GetCurrentProcess;
-use windows_sys::Win32::System::Diagnostics::Debug::{
- AddVectoredExceptionHandler, EXCEPTION_POINTERS,
-};
use crate::sbc_trace::{
absolute_jump, allocate_trampoline, readable_range, target_va, validate_cards_build,
@@ -140,15 +141,35 @@ macro_rules! season_call_trace {
};
}
-season_call_trace!(load_current_native_wrapper, LOAD_CURRENT_NATIVE_TRAMP, "LoadCurrentOfflineSeason_native");
-season_call_trace!(start_season_native_wrapper, START_SEASON_NATIVE_TRAMP, "StartSeason_native");
-season_call_trace!(get_info_native_wrapper, GET_INFO_NATIVE_TRAMP, "GetOfflineSeasonInfo_native");
+season_call_trace!(
+ load_current_native_wrapper,
+ LOAD_CURRENT_NATIVE_TRAMP,
+ "LoadCurrentOfflineSeason_native"
+);
+season_call_trace!(
+ start_season_native_wrapper,
+ START_SEASON_NATIVE_TRAMP,
+ "StartSeason_native"
+);
+season_call_trace!(
+ get_info_native_wrapper,
+ GET_INFO_NATIVE_TRAMP,
+ "GetOfflineSeasonInfo_native"
+);
// Real LoadOfflineSeasons native (FUN_18004ee10) — what _LoadCurrentSeason
// actually calls; hands the callback name to the manager's async slot 0x80.
-season_call_trace!(load_offline_real_wrapper, LOAD_OFFLINE_REAL_TRAMP, "LoadOfflineSeasons_native(0x4ee10)");
+season_call_trace!(
+ load_offline_real_wrapper,
+ LOAD_OFFLINE_REAL_TRAMP,
+ "LoadOfflineSeasons_native(0x4ee10)"
+);
// Async LoadOfflineSeasons impl (mgr slot 0x80, FUN_180057560): reads the season
// count and invokes the LoadSeasons_Complete AS callback.
-season_call_trace!(load_offline_async_wrapper, LOAD_OFFLINE_ASYNC_TRAMP, "LoadOfflineSeasons_asyncimpl(0x57560)");
+season_call_trace!(
+ load_offline_async_wrapper,
+ LOAD_OFFLINE_ASYNC_TRAMP,
+ "LoadOfflineSeasons_asyncimpl(0x57560)"
+);
// LoadCurrentOfflineSeason IMPL (manager slot 0x20): registers the load callbacks
// and starts the async op. param_1=manager, param_2=state byte, param_3=seasonId
@@ -189,7 +210,12 @@ unsafe extern "system" fn load_current_impl_wrapper(
// Completion callback FUN_1800578e0 (0x578e0). Kept from the first pass to confirm
// whether it ever fires; logs the result fields it branches on.
static COMPLETION_TRAMP: AtomicUsize = AtomicUsize::new(0);
-unsafe extern "system" fn completion_wrapper(ctx: usize, result: usize, r8: usize, r9: usize) -> usize {
+unsafe extern "system" fn completion_wrapper(
+ ctx: usize,
+ result: usize,
+ r8: usize,
+ r9: usize,
+) -> usize {
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
if n < 64 {
let status = rd_i32(result + 0x1c);
@@ -215,7 +241,11 @@ unsafe extern "system" fn completion_wrapper(ctx: usize, result: usize, r8: usiz
// this binding — it is NOT LoadOfflineSeasons). Called from _InitializeScreen for
// the division display, sync. Its prologue holds a rip-relative `MOV RCX,[rip+disp]`,
// so it needs the relocating installer below.
-season_call_trace!(get_users_division_wrapper, GET_USERS_DIVISION_TRAMP, "GetUsersOfflineDivision_native(0x4eb50)");
+season_call_trace!(
+ get_users_division_wrapper,
+ GET_USERS_DIVISION_TRAMP,
+ "GetUsersOfflineDivision_native(0x4eb50)"
+);
/// Find a free page within ~±1.5 GiB of `base`, so a rip-relative disp32 into
/// CardsDLL data still fits after we relocate a copied prologue into it.
@@ -269,7 +299,9 @@ unsafe fn install_detour_reloc(
let jump = absolute_jump(wrapper);
let tramp_len = copy_len + jump.len();
let Some(tramp) = alloc_near(base, tramp_len) else {
- write_log(&format!("SEASON_TRACE: {name}: near trampoline alloc failed\n"));
+ write_log(&format!(
+ "SEASON_TRACE: {name}: near trampoline alloc failed\n"
+ ));
return false;
};
core::ptr::copy_nonoverlapping(target as *const u8, tramp as *mut u8, copy_len);
@@ -278,7 +310,9 @@ unsafe fn install_detour_reloc(
let abs_target = target as i64 + insn_end as i64 + orig_disp;
let new_disp = abs_target - (tramp as i64 + insn_end as i64);
if new_disp < i32::MIN as i64 || new_disp > i32::MAX as i64 {
- write_log(&format!("SEASON_TRACE: {name}: reloc out of range ({new_disp:#x})\n"));
+ write_log(&format!(
+ "SEASON_TRACE: {name}: reloc out of range ({new_disp:#x})\n"
+ ));
return false;
}
core::ptr::write_unaligned((tramp + disp_off) as *mut i32, new_disp as i32);
@@ -286,7 +320,9 @@ unsafe fn install_detour_reloc(
core::ptr::copy_nonoverlapping(back.as_ptr(), (tramp + copy_len) as *mut u8, back.len());
let mut old = 0u32;
if VirtualProtect(tramp as _, tramp_len, PAGE_EXECUTE_READ, &mut old) == 0 {
- write_log(&format!("SEASON_TRACE: {name}: trampoline protect failed\n"));
+ write_log(&format!(
+ "SEASON_TRACE: {name}: trampoline protect failed\n"
+ ));
return false;
}
FlushInstructionCache(GetCurrentProcess(), tramp as _, tramp_len);
@@ -319,7 +355,12 @@ unsafe fn install_detour_reloc(
// completion ctx (cbref at +0x18), param_2 = result obj (byte0=ok flag; +8 = error
// string ptr when byte0==0). Logs the EXACT status string delivered. Passive.
static FINAL_COMPLETION_TRAMP: AtomicUsize = AtomicUsize::new(0);
-unsafe extern "system" fn final_completion_wrapper(ctx: usize, result: usize, r8: usize, r9: usize) -> usize {
+unsafe extern "system" fn final_completion_wrapper(
+ ctx: usize,
+ result: usize,
+ r8: usize,
+ r9: usize,
+) -> usize {
// Read the delivered status: byte0==0 => failure with an error string at +8.
let flag = rd_u8(result);
let errstr = if flag == Some(0) {
@@ -344,7 +385,11 @@ unsafe extern "system" fn final_completion_wrapper(ctx: usize, result: usize, r8
Some(_) => "SUCCESS",
None => "??",
};
- let shown = if flag == Some(0) { errstr.as_str() } else { "SUCCESS" };
+ let shown = if flag == Some(0) {
+ errstr.as_str()
+ } else {
+ "SUCCESS"
+ };
write_log(&format!(
"SEASONS_LOAD_CALLBACK: final kind={kind} result={shown:?} flag={flag:?} ctx={ctx:#x} cbref={cbref:#x}\n"
));
@@ -353,7 +398,10 @@ unsafe extern "system" fn final_completion_wrapper(ctx: usize, result: usize, r8
// by the WEBFILE base-supply (the real file now downloads), so the guarded
// success-forcing bypass is DISABLED — a recurring CACHE_PACKNAMES here means
// the base-supply did not take effect and MUST NOT be masked.
- if flag == Some(0) && errstr.contains("CACHE_PACKNAMES") && !BYPASS_DONE.swap(true, Ordering::AcqRel) {
+ if flag == Some(0)
+ && errstr.contains("CACHE_PACKNAMES")
+ && !BYPASS_DONE.swap(true, Ordering::AcqRel)
+ {
write_log("SEASONS_BYPASS: DISABLED (base-supply active); CACHE_PACKNAMES not masked\n");
}
let t = FINAL_COMPLETION_TRAMP.load(Ordering::Acquire);
@@ -369,14 +417,23 @@ unsafe extern "system" fn final_completion_wrapper(ctx: usize, result: usize, r8
// "CACHE_PACKNAMES_FAILED" when result==0 or *(i32)(result+0x1c)!=0; else chains
// the next async stage. Logs whether the first async stage succeeded. Passive.
static STAGE1_COMPLETION_TRAMP: AtomicUsize = AtomicUsize::new(0);
-unsafe extern "system" fn stage1_completion_wrapper(param1: usize, result: usize, r8: usize, r9: usize) -> usize {
+unsafe extern "system" fn stage1_completion_wrapper(
+ param1: usize,
+ result: usize,
+ r8: usize,
+ r9: usize,
+) -> usize {
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
if n < 64 {
if result == 0 {
write_log("SEASONS_STAGE1: result=NULL -> CACHE_PACKNAMES_FAILED\n");
} else {
let status = rd_i32(result + 0x1c);
- let verdict = if status == Some(0) { "ok(chain next)" } else { "CACHE_PACKNAMES_FAILED" };
+ let verdict = if status == Some(0) {
+ "ok(chain next)"
+ } else {
+ "CACHE_PACKNAMES_FAILED"
+ };
write_log(&format!(
"SEASONS_STAGE1: result={result:#x} status(+0x1c)={} -> {verdict}\n",
status.map(|x| x.to_string()).unwrap_or_else(|| "??".into()),
@@ -397,17 +454,34 @@ unsafe extern "system" fn stage1_completion_wrapper(param1: usize, result: usize
// rip-relative `MOV R8,[DAT_1802e6580]`, so it uses the relocating installer
// (disp32 at copied offset 7, instruction end 11).
//
-// BASE-SUPPLY (staging experiment): the client's RS4::ServerSettings CDN base
-// (DAT_1802e6408+0x30) is EMPTY in the emulator — FUN_180124270 only sets it when
-// the OSDK getter slot0x3f8 is non-empty, and it has no default (unlike the API
-// base). So every FUT WEBFILE url arrives here as a BARE relative path and 999s
-// (client sentinel). We supply the missing intended `/fut/` prefix pointing
-// at the staging content server so the REAL file downloads and parses. This is a
-// data-supply, NOT a success-forcing bypass; absolute urls (containing "://",
-// e.g. the "http://sbc/..." tile route) pass through untouched.
-const STAGING_FUT_BASE: &str = "http://10.10.0.120:8110/fut/";
+// BASE-SUPPLY: the client's RS4::ServerSettings CDN base (DAT_1802e6408+0x30) is
+// EMPTY in the emulator — FUN_180124270 only sets it when the OSDK getter
+// slot0x3f8 is non-empty, and it has no default (unlike the API base). So every
+// FUT WEBFILE url arrives here as a BARE relative path and 999s (client
+// sentinel). We supply the missing intended `/fut/` prefix so the REAL file
+// downloads and parses. This is a data-supply, NOT a success-forcing bypass;
+// absolute urls (containing "://", e.g. the "http://sbc/..." tile route) pass
+// through untouched.
+//
+// The prefix comes from `openfut.cfg` via `openfut-common`, the same single
+// source of truth as every redirect target, so no lab address is compiled in.
+// Unset (config missing/unusable) means NO rewrite: a url is left exactly as the
+// client built it rather than pointed at a guessed host.
+static FUT_CONTENT_BASE: OnceLock = OnceLock::new();
+
+/// Arm the FUT web-file prefix from the resolved configuration. Idempotent: the
+/// first call wins.
+pub(crate) fn set_fut_content_base(base: String) {
+ let _ = FUT_CONTENT_BASE.set(base);
+}
+
static URL_CAPTURE_TRAMP: AtomicUsize = AtomicUsize::new(0);
-unsafe extern "system" fn url_capture_wrapper(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize {
+unsafe extern "system" fn url_capture_wrapper(
+ rcx: usize,
+ rdx: usize,
+ r8: usize,
+ r9: usize,
+) -> usize {
let orig = rd_cstr(rcx, 256);
let mut arg_rcx = rcx;
// Owned buffer that stays alive across the original() call below. The caller
@@ -415,11 +489,13 @@ unsafe extern "system" fn url_capture_wrapper(rcx: usize, rdx: usize, r8: usize,
// client copies the url synchronously during the call — a local buffer is
// sufficient and nothing is leaked.
let mut full: Vec = Vec::new();
- if !orig.is_empty() && !orig.contains("://") {
- full.extend_from_slice(STAGING_FUT_BASE.as_bytes());
- full.extend_from_slice(orig.trim_start_matches('/').as_bytes());
- full.push(0); // NUL terminator for the C-string
- arg_rcx = full.as_ptr() as usize;
+ if let Some(base) = FUT_CONTENT_BASE.get() {
+ if !orig.is_empty() && !orig.contains("://") {
+ full.extend_from_slice(base.as_bytes());
+ full.extend_from_slice(orig.trim_start_matches('/').as_bytes());
+ full.push(0); // NUL terminator for the C-string
+ arg_rcx = full.as_ptr() as usize;
+ }
}
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
if n < 64 {
@@ -533,61 +609,138 @@ unsafe fn worker() {
write_log("SEASON_TRACE: crash logger (VEH) armed\n");
// (rva, name, copy_len, signature, wrapper, trampoline slot)
install_detour(
- base, 0x4eb70, "LoadCurrentOfflineSeason_native", 15,
- &[0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff, 0xff],
- load_current_native_wrapper as *const () as usize, &LOAD_CURRENT_NATIVE_TRAMP,
+ base,
+ 0x4eb70,
+ "LoadCurrentOfflineSeason_native",
+ 15,
+ &[
+ 0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff,
+ 0xff,
+ ],
+ load_current_native_wrapper as *const () as usize,
+ &LOAD_CURRENT_NATIVE_TRAMP,
);
install_detour(
- base, 0x4f340, "StartSeason_native", 15,
- &[0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff, 0xff],
- start_season_native_wrapper as *const () as usize, &START_SEASON_NATIVE_TRAMP,
+ base,
+ 0x4f340,
+ "StartSeason_native",
+ 15,
+ &[
+ 0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff,
+ 0xff,
+ ],
+ start_season_native_wrapper as *const () as usize,
+ &START_SEASON_NATIVE_TRAMP,
);
install_detour(
- base, 0x4e850, "GetOfflineSeasonInfo_native", 15,
- &[0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x6c, 0x24, 0x10, 0x48, 0x89, 0x74, 0x24, 0x18],
- get_info_native_wrapper as *const () as usize, &GET_INFO_NATIVE_TRAMP,
+ base,
+ 0x4e850,
+ "GetOfflineSeasonInfo_native",
+ 15,
+ &[
+ 0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x6c, 0x24, 0x10, 0x48, 0x89, 0x74, 0x24,
+ 0x18,
+ ],
+ get_info_native_wrapper as *const () as usize,
+ &GET_INFO_NATIVE_TRAMP,
);
install_detour(
- base, 0x57230, "LoadCurrentOfflineSeason_impl", 19,
- &[0x48, 0x8b, 0xc4, 0x57, 0x48, 0x81, 0xec, 0x80, 0x00, 0x00, 0x00, 0x48, 0xc7, 0x40, 0x98, 0xfe, 0xff, 0xff, 0xff],
- load_current_impl_wrapper as *const () as usize, &LOAD_CURRENT_IMPL_TRAMP,
+ base,
+ 0x57230,
+ "LoadCurrentOfflineSeason_impl",
+ 19,
+ &[
+ 0x48, 0x8b, 0xc4, 0x57, 0x48, 0x81, 0xec, 0x80, 0x00, 0x00, 0x00, 0x48, 0xc7, 0x40,
+ 0x98, 0xfe, 0xff, 0xff, 0xff,
+ ],
+ load_current_impl_wrapper as *const () as usize,
+ &LOAD_CURRENT_IMPL_TRAMP,
);
install_detour(
- base, 0x578e0, "LoadCurrentOfflineSeason_completion", 16,
- &[0x48, 0x8b, 0xc4, 0x57, 0x48, 0x83, 0xec, 0x70, 0x48, 0xc7, 0x40, 0xd0, 0xfe, 0xff, 0xff, 0xff],
- completion_wrapper as *const () as usize, &COMPLETION_TRAMP,
+ base,
+ 0x578e0,
+ "LoadCurrentOfflineSeason_completion",
+ 16,
+ &[
+ 0x48, 0x8b, 0xc4, 0x57, 0x48, 0x83, 0xec, 0x70, 0x48, 0xc7, 0x40, 0xd0, 0xfe, 0xff,
+ 0xff, 0xff,
+ ],
+ completion_wrapper as *const () as usize,
+ &COMPLETION_TRAMP,
);
install_detour_reloc(
- base, 0x4eb50, "GetUsersOfflineDivision_native", 14,
- &[0x48, 0x83, 0xec, 0x28, 0x48, 0x8b, 0x0d, 0x75, 0x18, 0x29, 0x00, 0x48, 0x8b, 0x01],
- 7, 11,
- get_users_division_wrapper as *const () as usize, &GET_USERS_DIVISION_TRAMP,
+ base,
+ 0x4eb50,
+ "GetUsersOfflineDivision_native",
+ 14,
+ &[
+ 0x48, 0x83, 0xec, 0x28, 0x48, 0x8b, 0x0d, 0x75, 0x18, 0x29, 0x00, 0x48, 0x8b, 0x01,
+ ],
+ 7,
+ 11,
+ get_users_division_wrapper as *const () as usize,
+ &GET_USERS_DIVISION_TRAMP,
);
install_detour(
- base, 0x4ee10, "LoadOfflineSeasons_native", 15,
- &[0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff, 0xff],
- load_offline_real_wrapper as *const () as usize, &LOAD_OFFLINE_REAL_TRAMP,
+ base,
+ 0x4ee10,
+ "LoadOfflineSeasons_native",
+ 15,
+ &[
+ 0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff,
+ 0xff,
+ ],
+ load_offline_real_wrapper as *const () as usize,
+ &LOAD_OFFLINE_REAL_TRAMP,
);
install_detour(
- base, 0x57560, "LoadOfflineSeasons_asyncimpl", 17,
- &[0x40, 0x55, 0x56, 0x57, 0x48, 0x83, 0xec, 0x30, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff, 0xff],
- load_offline_async_wrapper as *const () as usize, &LOAD_OFFLINE_ASYNC_TRAMP,
+ base,
+ 0x57560,
+ "LoadOfflineSeasons_asyncimpl",
+ 17,
+ &[
+ 0x40, 0x55, 0x56, 0x57, 0x48, 0x83, 0xec, 0x30, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe,
+ 0xff, 0xff, 0xff,
+ ],
+ load_offline_async_wrapper as *const () as usize,
+ &LOAD_OFFLINE_ASYNC_TRAMP,
);
install_detour(
- base, 0xffe90, "LoadOfflineSeasons_final_completion", 16,
- &[0x48, 0x89, 0x5c, 0x24, 0x08, 0x57, 0x48, 0x83, 0xec, 0x30, 0x80, 0x3a, 0x00, 0x48, 0x8b, 0xda],
- final_completion_wrapper as *const () as usize, &FINAL_COMPLETION_TRAMP,
+ base,
+ 0xffe90,
+ "LoadOfflineSeasons_final_completion",
+ 16,
+ &[
+ 0x48, 0x89, 0x5c, 0x24, 0x08, 0x57, 0x48, 0x83, 0xec, 0x30, 0x80, 0x3a, 0x00, 0x48,
+ 0x8b, 0xda,
+ ],
+ final_completion_wrapper as *const () as usize,
+ &FINAL_COMPLETION_TRAMP,
);
install_detour(
- base, 0x106240, "LoadOfflineSeasons_stage1_completion", 15,
- &[0x48, 0x8b, 0xc4, 0x55, 0x48, 0x8d, 0x68, 0xa1, 0x48, 0x81, 0xec, 0xc0, 0x00, 0x00, 0x00],
- stage1_completion_wrapper as *const () as usize, &STAGE1_COMPLETION_TRAMP,
+ base,
+ 0x106240,
+ "LoadOfflineSeasons_stage1_completion",
+ 15,
+ &[
+ 0x48, 0x8b, 0xc4, 0x55, 0x48, 0x8d, 0x68, 0xa1, 0x48, 0x81, 0xec, 0xc0, 0x00, 0x00,
+ 0x00,
+ ],
+ stage1_completion_wrapper as *const () as usize,
+ &STAGE1_COMPLETION_TRAMP,
);
install_detour_reloc(
- base, 0x17ff90, "start_webfile_dl_url", 14,
- &[0x48, 0x83, 0xec, 0x38, 0x4c, 0x8b, 0x05, 0xe5, 0x65, 0x16, 0x00, 0x4c, 0x8b, 0xd1],
- 7, 11,
- url_capture_wrapper as *const () as usize, &URL_CAPTURE_TRAMP,
+ base,
+ 0x17ff90,
+ "start_webfile_dl_url",
+ 14,
+ &[
+ 0x48, 0x83, 0xec, 0x38, 0x4c, 0x8b, 0x05, 0xe5, 0x65, 0x16, 0x00, 0x4c, 0x8b, 0xd1,
+ ],
+ 7,
+ 11,
+ url_capture_wrapper as *const () as usize,
+ &URL_CAPTURE_TRAMP,
);
write_log("SEASON_TRACE: all season-native traces armed\n");
}
@@ -595,6 +748,46 @@ unsafe fn worker() {
/// Arm the passive season-flow diagnostics on a deferred thread (CardsDLL is not
/// yet loaded at DllMain time). Read-only: never changes game behavior.
pub(crate) fn install() {
+ arm_fut_content_base();
write_log("SEASON_TRACE: requested; deferred signature validation starting\n");
std::thread::spawn(|| unsafe { worker() });
}
+
+/// Resolve the FUT web-file prefix from `openfut.cfg` next to the game exe, via
+/// the shared `openfut-common` parser — the same single source of truth the
+/// network redirect uses, so the lab address is never compiled in.
+///
+/// Fails SAFE: an absent or unusable config arms nothing, and the url rewriter
+/// then leaves every url exactly as the client built it.
+fn arm_fut_content_base() {
+ let path = match std::env::current_exe()
+ .ok()
+ .and_then(|p| p.parent().map(|d| d.join("openfut.cfg")))
+ {
+ Some(p) => p,
+ None => {
+ write_log("SEASONS_WEBFILE_BASE: cannot locate openfut.cfg — no url rewrite\n");
+ return;
+ }
+ };
+ let contents = match std::fs::read_to_string(&path) {
+ Ok(c) => c,
+ Err(e) => {
+ write_log(&format!(
+ "SEASONS_WEBFILE_BASE: {} unreadable ({e}) — no url rewrite\n",
+ path.display()
+ ));
+ return;
+ }
+ };
+ match openfut_common::ServerConfig::parse(&contents) {
+ Ok(cfg) => {
+ let base = cfg.fut_content_base();
+ write_log(&format!("SEASONS_WEBFILE_BASE: armed {base}\n"));
+ set_fut_content_base(base);
+ }
+ Err(e) => write_log(&format!(
+ "SEASONS_WEBFILE_BASE: openfut.cfg unusable ({e}) — no url rewrite\n"
+ )),
+ }
+}
diff --git a/openfut-hook/src/store_entry.rs b/openfut-hook/src/store_entry.rs
index eec72dc..238b978 100644
--- a/openfut-hook/src/store_entry.rs
+++ b/openfut-hook/src/store_entry.rs
@@ -439,7 +439,9 @@ unsafe fn worker() {
pub(crate) fn install() {
// Promoted: armed by the build. No environment variable participates.
REPAIR_ENABLED.store(REPAIR_PROMOTED, Ordering::Release);
- crate::write_log("STORE_TABS: bind sensor + pre-warm ARMED (promoted); strict signature gate\n");
+ crate::write_log(
+ "STORE_TABS: bind sensor + pre-warm ARMED (promoted); strict signature gate\n",
+ );
std::thread::spawn(|| unsafe { worker() });
}
diff --git a/src/config.rs b/src/config.rs
index 6be3c54..49c9e62 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -314,6 +314,7 @@ impl LauncherConfig {
https: self.openfut_https_port,
blaze_redirector: self.openfut_blaze_redirector_port,
blaze_main: self.openfut_blaze_main_port,
+ fut_content: openfut_common::default_ports::FUT_CONTENT,
},
}
}