fix(seasons): supply the FUT web-file base so Seasons stops failing

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.
This commit is contained in:
funman300
2026-08-21 16:12:41 +00:00
parent 7edf682291
commit 5294f589ad
4 changed files with 155 additions and 10 deletions
+92 -8
View File
@@ -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 `<CDN>/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<String> = 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<u8> = 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"
)),
}
}