2 Commits

Author SHA1 Message Date
funman300 5294f589ad 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.
2026-08-21 16:12:41 +00:00
funman300 7edf682291 diag(fifa17): port the passive kit-selector trace onto main
Cherry-pick of 4b1d5aa from wip/kit-selector-re, which forked before the TLS
work and cannot be rebased: that branch predates fifa17_tls.rs/patch_mem.rs and
carries a large unrelated lineage (probe/lsx/recv_hook). Only the kit_trace
commit's own contents are taken.

Traces the client-side FUT pre-match kit path in CardsDLL: the GetMatchKits_DP
gate (KITS_AVAILABLE), setAvailableKits (home/away list count), the kit-item
clone driver (item type/subid/teamid at FUN_1801c3480), and the local teamkits
DB clone. Read-only passive detours reusing season_trace's installers, which
this widens to pub(crate).

The one open unknown it answers: the selector requires item+0x60 == 4, a pile
value the server has never been observed to produce (/club emits 1,
/purchased 6).

Verified in the cross-built artifact that the roster TLS gate patch is intact
alongside the new trace (fifa17_tls markers present, KIT_* markers present).
2026-08-21 04:15:58 +00:00
7 changed files with 398 additions and 15 deletions
Generated
+1
View File
@@ -2293,6 +2293,7 @@ dependencies = [
"eframe",
"egui",
"openfut-common",
"parking_lot",
"serde",
"serde_json",
"tokio",
+61 -2
View File
@@ -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 `<base>/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.
+1
View File
@@ -159,6 +159,7 @@ unsafe extern "system" fn worker(_: *mut core::ffi::c_void) -> u32 {
crate::sbc_request_trace::install();
crate::store_entry::install();
crate::season_trace::install();
crate::kit_trace::install();
0
}
+235
View File
@@ -0,0 +1,235 @@
//! Passive, behavior-preserving diagnostic traces for FIFA 17's FUT pre-match
//! KIT SELECTOR data flow.
//!
//! RE (2026-08-20, Ghidra on CardsDLL_Win64_retail.dll) established that the
//! pre-match kit selector is fed ENTIRELY client-side (NOT by POW/EASFC):
//!
//! * `FUT_GET_MATCH_KITS_DP` (id 0x7565) builder `FUN_1800be6a0` (rva 0xbe6a0)
//! reads a boolean gate `ctx+0x152` (`KITS_AVAILABLE`); when false, or when
//! the two available-kit vectors are empty, the selector renders blank/white.
//! * The available home/away kit-id lists live on `FutSquadServiceImpl`
//! (`this+0xe08` home, `this+0xe38` away) and are written by the setter
//! `FUN_180196760` (rva 0x96760, vtable slot 0x1d0): args (this, srcVec, side).
//! * A club KIT ITEM is turned into an available kit by `FUN_1801c3480`
//! (rva 0x1c3480): it reads item fields (`+0x4c==7`, `+0x60==4`,
//! `+0x5c`∈{101 home,102 away}, `+0x94` source teamid, `+0xba`
//! teamkittypetechid) and calls `FUN_1801c44b0` (rva 0x1c44b0) to clone that
//! team's kit rows from the CLIENT-LOCAL `teamkits` DB into the FUT club
//! (teamtechid 130000).
//!
//! These traces answer, in one operator-driven match, exactly WHERE the empty
//! selector originates: do kit club items reach the client (kit_item_clone), does
//! the clone into the FUT club happen (kit_db_clone), does the available list get
//! set non-empty (set_available_kits), and what does the selector finally read
//! (get_match_kits: KITS_AVAILABLE + count). Every trace is read-only: it logs,
//! then tail-calls the original through a trampoline. Copied prologues are whole,
//! position-independent instructions (the one rip-relative prologue uses the
//! relocating installer).
use core::sync::atomic::{AtomicUsize, Ordering};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
use crate::sbc_trace::{readable_range, validate_cards_build};
use crate::season_trace::{install_detour, install_detour_reloc, rd_i32, rd_u8};
use crate::write_log;
static REPORTS: AtomicUsize = AtomicUsize::new(0);
fn budget() -> bool {
REPORTS.fetch_add(1, Ordering::Relaxed) < 256
}
unsafe fn rd_usize(addr: usize) -> Option<usize> {
readable_range(addr, 8).then(|| core::ptr::read_volatile(addr as *const usize))
}
// FUT_GET_MATCH_KITS_DP builder FUN_1800be6a0 (0xbe6a0). rcx = DP model ctx.
// ctx+0x152 is the KITS_AVAILABLE bool that gates the whole selector list.
static GET_MATCH_KITS_TRAMP: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn get_match_kits_wrapper(
rcx: usize,
rdx: usize,
r8: usize,
r9: usize,
) -> usize {
if budget() {
let avail = rd_u8(rcx + 0x152);
write_log(&format!(
"KIT_GET: FUT_GET_MATCH_KITS_DP ctx={rcx:#x} KITS_AVAILABLE={avail:?}\n"
));
}
let t = GET_MATCH_KITS_TRAMP.load(Ordering::Acquire);
if t == 0 {
return 0;
}
let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
core::mem::transmute(t);
original(rcx, rdx, r8, r9)
}
// setAvailableKits FUN_180196760 (0x96760): (this, srcVec, side). srcVec is an
// int vector {begin@+0, end@+8}; count = (end-begin)/4. side 0=home, 1=away.
static SET_AVAILABLE_KITS_TRAMP: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn set_available_kits_wrapper(
rcx: usize,
rdx: usize,
r8: usize,
r9: usize,
) -> usize {
if budget() {
let count = match (rd_usize(rdx), rd_usize(rdx + 8)) {
(Some(b), Some(e)) if e >= b => ((e - b) / 4) as i64,
_ => -1,
};
write_log(&format!(
"KIT_SET: setAvailableKits this={rcx:#x} side={r8} count={count}\n"
));
}
let t = SET_AVAILABLE_KITS_TRAMP.load(Ordering::Acquire);
if t == 0 {
return 0;
}
let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
core::mem::transmute(t);
original(rcx, rdx, r8, r9)
}
// Kit-item clone driver FUN_1801c3480 (0x1c3480): rdx = param_2, the club-item
// event; the item struct is at *(param_2+0x10). Logs the fields the function
// branches on so we can see whether a kit club item reaches the client and its
// home/away designator + source teamid.
static KIT_ITEM_CLONE_TRAMP: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn kit_item_clone_wrapper(
rcx: usize,
rdx: usize,
r8: usize,
r9: usize,
) -> usize {
if budget() {
if let Some(item) = rd_usize(rdx + 0x10) {
write_log(&format!(
"KIT_ITEM: clone-driver item={item:#x} type[+0x4c]={:?} subid[+0x5c]={:?} \
cat[+0x60]={:?} teamid[+0x94]={:?} kittype[+0xba]={:?}\n",
rd_i32(item + 0x4c),
rd_i32(item + 0x5c),
rd_i32(item + 0x60),
rd_i32(item + 0x94),
rd_i32(item + 0xba),
));
} else {
write_log(&format!(
"KIT_ITEM: clone-driver param_2={rdx:#x} (item ptr unreadable)\n"
));
}
}
let t = KIT_ITEM_CLONE_TRAMP.load(Ordering::Acquire);
if t == 0 {
return 0;
}
let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
core::mem::transmute(t);
original(rcx, rdx, r8, r9)
}
// Kit DB clone FUN_1801c44b0 (0x1c44b0): (clubmgr, side, teamtechid, kittype).
// Fires only when the driver decided the item is a home(101)/away(102) kit, so
// this is the proof the FUT-club (teamtechid 130000) kit rows get synthesized.
static KIT_DB_CLONE_TRAMP: AtomicUsize = AtomicUsize::new(0);
unsafe extern "system" fn kit_db_clone_wrapper(
rcx: usize,
rdx: usize,
r8: usize,
r9: usize,
) -> usize {
if budget() {
write_log(&format!(
"KIT_DBCLONE: clone team kit side={rdx} src_teamtechid={r8} kittype={r9}\n"
));
}
let t = KIT_DB_CLONE_TRAMP.load(Ordering::Acquire);
if t == 0 {
return 0;
}
let original: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
core::mem::transmute(t);
original(rcx, rdx, r8, r9)
}
unsafe fn worker() {
let mut base = 0usize;
for _ in 0..600u32 {
base = GetModuleHandleA(c"CardsDLL_Win64_retail.dll".as_ptr().cast()) as usize;
if base != 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
if base == 0 || !validate_cards_build(base) {
write_log("KIT_TRACE: CardsDLL unavailable/invalid; kit trace inactive\n");
return;
}
// FUN_1800be6a0: 48 8b c4 55 41 54 41 55 41 56 41 57 48 8d 68 a1 (copy_len 16).
install_detour(
base,
0xbe6a0,
"GetMatchKits_DP(0xbe6a0)",
16,
&[
0x48, 0x8b, 0xc4, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x8d,
0x68, 0xa1,
],
get_match_kits_wrapper as *const () as usize,
&GET_MATCH_KITS_TRAMP,
);
// FUN_180196760: 48 89 54 24 10 53 48 83 ec 30 48 c7 44 24 20 fe ff ff ff (copy_len 19).
install_detour(
base,
0x96760,
"setAvailableKits(0x96760)",
19,
&[
0x48, 0x89, 0x54, 0x24, 0x10, 0x53, 0x48, 0x83, 0xec, 0x30, 0x48, 0xc7, 0x44, 0x24,
0x20, 0xfe, 0xff, 0xff, 0xff,
],
set_available_kits_wrapper as *const () as usize,
&SET_AVAILABLE_KITS_TRAMP,
);
// FUN_1801c3480: 48 89 5c 24 08 57 48 83 ec 60 <48 8b 05 disp32> (rip-relative
// MOV RAX,[rip+..] at copied offset 10; disp32 at 13, insn end 17; copy_len 17).
install_detour_reloc(
base,
0x1c3480,
"kitItemClone(0x1c3480)",
17,
&[
0x48, 0x89, 0x5c, 0x24, 0x08, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0x8b, 0x05, 0x4f,
0x82, 0x11, 0x00,
],
13,
17,
kit_item_clone_wrapper as *const () as usize,
&KIT_ITEM_CLONE_TRAMP,
);
// FUN_1801c44b0: 48 8b c4 55 41 54 41 55 41 56 41 57 48 8d 68 c8 (copy_len 16).
install_detour(
base,
0x1c44b0,
"kitDbClone(0x1c44b0)",
16,
&[
0x48, 0x8b, 0xc4, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x8d,
0x68, 0xc8,
],
kit_db_clone_wrapper as *const () as usize,
&KIT_DB_CLONE_TRAMP,
);
write_log("KIT_TRACE: all kit-selector traces armed\n");
}
/// Arm the passive kit-selector diagnostics on a deferred thread (CardsDLL is not
/// yet loaded at DllMain time). Read-only: never changes game behavior.
pub(crate) fn install() {
write_log("KIT_TRACE: requested; deferred signature validation starting\n");
std::thread::spawn(|| unsafe { worker() });
}
+2
View File
@@ -18,6 +18,8 @@ mod fifa17;
#[cfg(feature = "fifa17")]
mod fifa17_tls;
mod iat;
#[cfg(feature = "fifa17")]
mod kit_trace;
mod patch_mem;
#[cfg(feature = "fifa17")]
mod sbc_dispatch;
+97 -13
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;
@@ -29,14 +30,14 @@ use crate::write_log;
static REPORTS: AtomicUsize = AtomicUsize::new(0);
unsafe fn rd_i32(addr: usize) -> Option<i32> {
pub(crate) unsafe fn rd_i32(addr: usize) -> Option<i32> {
readable_range(addr, 4).then(|| core::ptr::read_volatile(addr as *const i32))
}
unsafe fn rd_u8(addr: usize) -> Option<u8> {
pub(crate) unsafe fn rd_u8(addr: usize) -> Option<u8> {
readable_range(addr, 1).then(|| core::ptr::read_volatile(addr as *const u8))
}
/// Read a NUL-terminated string safely (bounded, only reads mapped bytes).
unsafe fn rd_cstr(addr: usize, max: usize) -> String {
pub(crate) unsafe fn rd_cstr(addr: usize, max: usize) -> String {
if addr == 0 || !readable_range(addr, 1) {
return String::from("<unreadable>");
}
@@ -56,7 +57,7 @@ unsafe fn rd_cstr(addr: usize, max: usize) -> String {
/// Generic passive detour: overwrite the first `copy_len` bytes of `target` (which
/// MUST be whole, position-independent instructions) with an absolute jump to
/// `wrapper`; the wrapper calls the trampoline (copied prologue + jump back).
unsafe fn install_detour(
pub(crate) unsafe fn install_detour(
base: usize,
rva: usize,
name: &str,
@@ -267,7 +268,7 @@ unsafe fn alloc_near(base: usize, size: usize) -> Option<usize> {
/// both within the copied bytes). The trampoline is allocated near `base` and the
/// disp32 is relocated so it resolves to the same absolute address. Read-only.
#[allow(clippy::too_many_arguments)]
unsafe fn install_detour_reloc(
pub(crate) unsafe fn install_detour_reloc(
base: usize,
rva: usize,
name: &str,
@@ -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"
)),
}
}
+1
View File
@@ -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,
},
}
}