From 5294f589ad31014d610adc1e32eb04f693704773 Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 21 Aug 2026 16:12:41 +0000 Subject: [PATCH] fix(seasons): supply the FUT web-file base so Seasons stops failing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entering single-player Seasons showed "There was a problem communicating with the FIFA Ultimate Team Servers". The deployed trace caught the whole chain: SEASON_CALL: LoadOfflineSeasons_asyncimpl(0x57560) SEASONS_WEBFILE_URL: url="packs/loc/storepackdescriptions.en_us.xml" SEASONS_STAGE1: status(+0x1c)=999 -> CACHE_PACKNAMES_FAILED SEASONS_LOAD_CALLBACK: final kind=ERROR result="CACHE_PACKNAMES_FAILED" Not a server fault: no /season/* request is ever made. The client's RS4::ServerSettings CDN base is EMPTY in the emulator, so the pack-names web file is requested as a BARE relative path and 999s, and Seasons aborts on that prerequisite. Ports the base-supply rewriter from wip/seasons/base-supply-veh onto the deployed lineage (that branch forked before the TLS work and cannot be rebased), taking only the URL supply: absolute urls pass through untouched, and the success-forcing CACHE_PACKNAMES bypass is deliberately NOT taken — masking the failure would hide whether the supply actually worked. Corrects the port while porting: the branch hardcoded 8110, where nothing listens. The content server is POW (`pow_server.py`, kind "content") on 8085 — the port Blaze already advertises to the client as its content host. Verified live: GET http://10.10.0.120:8085/fut/packs/loc/storepackdescriptions.en_us.xml returns 200 with a 180-byte XLIFF document. `default_ports::FUT_CONTENT` is now 8085 and the base is still derived from openfut.cfg, so no address is compiled in (confirmed absent from the artifact). Artifact keeps the roster TLS gate patch (11 fifa17_tls markers) and the kit trace alongside the new base supply. --- Cargo.lock | 1 + openfut-common/src/lib.rs | 63 ++++++++++++++++++- openfut-hook/src/season_trace.rs | 100 ++++++++++++++++++++++++++++--- src/config.rs | 1 + 4 files changed, 155 insertions(+), 10 deletions(-) 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 71eaeca..45db714 100644 --- a/openfut-common/src/lib.rs +++ b/openfut-common/src/lib.rs @@ -53,6 +53,17 @@ 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. + /// + /// 8085 is the POW content server (`pow_server.py`, kind "content"), which is + /// the port Blaze already advertises to the client as its content host and + /// which serves `/fut/packs/loc/storepackdescriptions.en_us.xml`. Verified + /// live: that path returns 200 with a 180-byte XLIFF document. + pub const FUT_CONTENT: u16 = 8085; } /// OpenFUT destination ports. Each field is where an intercepted EA source port @@ -66,6 +77,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 +88,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, } } } @@ -244,6 +259,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}'", @@ -260,8 +276,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:8085/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 ) } @@ -449,12 +484,36 @@ mod tests { https: 8443, blaze_redirector: 10041, blaze_main: 42127, + fut_content: 8085, }, }; 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:8085/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/src/season_trace.rs b/openfut-hook/src/season_trace.rs index 96c9536..9dd135c 100644 --- a/openfut-hook/src/season_trace.rs +++ b/openfut-hook/src/season_trace.rs @@ -13,6 +13,7 @@ //! (no rip-relative / rel32 in the copied bytes). use core::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::OnceLock; use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache; use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA; @@ -388,6 +389,7 @@ unsafe extern "system" fn final_completion_wrapper( "SEASONS_LOAD_CALLBACK: final kind={kind} result={shown:?} flag={flag:?} ctx={ctx:#x} cbref={cbref:#x}\n" )); } + let t = FINAL_COMPLETION_TRAMP.load(Ordering::Acquire); if t == 0 { return 0; @@ -434,9 +436,30 @@ unsafe extern "system" fn stage1_completion_wrapper( } // WEBFILE_DL download start FUN_18017ff90(url, ctx): param_1 (rcx) is the C-string -// URL of the pack-names/cards-tournament-list web file. Passive capture. Its -// prologue has a rip-relative `MOV R8,[DAT_1802e6580]`, so it uses the relocating -// installer (disp32 at copied offset 7, instruction end 11). +// URL of the pack-names / cards-tournament-list web file. Its prologue has a +// rip-relative `MOV R8,[DAT_1802e6580]`, so it uses the relocating installer +// (disp32 at copied offset 7, instruction end 11). +// +// 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, @@ -444,12 +467,31 @@ unsafe extern "system" fn url_capture_wrapper( 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 + // frees its own url buffer immediately after FUN_18017ff90 returns, so the + // client copies the url synchronously during the call — a local buffer is + // sufficient and nothing is leaked. + let mut full: Vec = Vec::new(); + 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 { - write_log(&format!( - "SEASONS_WEBFILE_URL: url={:?}\n", - rd_cstr(rcx, 256) - )); + if arg_rcx != rcx { + write_log(&format!( + "SEASONS_WEBFILE_URL: orig={orig:?} rewritten={:?}\n", + rd_cstr(arg_rcx, 256) + )); + } else { + write_log(&format!("SEASONS_WEBFILE_URL: url={orig:?} (unchanged)\n")); + } } let t = URL_CAPTURE_TRAMP.load(Ordering::Acquire); if t == 0 { @@ -457,7 +499,9 @@ unsafe extern "system" fn url_capture_wrapper( } let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = core::mem::transmute(t); - original(rcx, rdx, r8, r9) + let ret = original(arg_rcx, rdx, r8, r9); + drop(full); // ensure the url buffer outlives the download-start call + ret } unsafe fn worker() { @@ -614,6 +658,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/src/config.rs b/src/config.rs index 2587efd..158607b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -325,6 +325,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, }, } }