From 9c4db4128937150281a7db2483548c3250065adb Mon Sep 17 00:00:00 2001 From: openfut Date: Wed, 19 Aug 2026 23:55:41 +0000 Subject: [PATCH] diag(fifa17): probe LoadOfflineSeasons async completions (result string capture) Adds passive field-logging detours on the FutCompetitionServiceImpl::LoadOfflineSeasons async chain resolved by static RE: final completion FUN_1800ffe90 -> logs the exact status string delivered to the AS LoadSeasons_Complete callback ("SUCCESS" vs error string at result+8); stage-1 completion FUN_180106240 -> logs whether the first async stage's status (+0x1c) is ok or CACHE_PACKNAMES_FAILED. Read-only; safe bounded C-string reader (rd_cstr). --- openfut-hook/src/season_trace.rs | 94 ++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/openfut-hook/src/season_trace.rs b/openfut-hook/src/season_trace.rs index b30ffe4..4643066 100644 --- a/openfut-hook/src/season_trace.rs +++ b/openfut-hook/src/season_trace.rs @@ -35,6 +35,23 @@ unsafe fn rd_i32(addr: usize) -> Option { unsafe fn rd_u8(addr: usize) -> Option { 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 { + if addr == 0 || !readable_range(addr, 1) { + return String::from(""); + } + let mut out = Vec::new(); + let mut i = 0; + while i < max && readable_range(addr + i, 1) { + let b = core::ptr::read_volatile((addr + i) as *const u8); + if b == 0 { + break; + } + out.push(b); + i += 1; + } + String::from_utf8_lossy(&out).into_owned() +} /// Generic passive detour: overwrite the first `copy_len` bytes of `target` (which /// MUST be whole, position-independent instructions) with an absolute jump to @@ -291,6 +308,73 @@ unsafe fn install_detour_reloc( } } +// FutCompetitionServiceImpl::LoadOfflineSeasons FINAL completion (FUN_1800ffe90): +// delivers the result to the AS callback LoadSeasons_Complete via +// FUN_18019fb30->slot0x20(vm,"_global",cbref, "SUCCESS" | errString). param_1 = the +// 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 { + let n = REPORTS.fetch_add(1, Ordering::Relaxed); + if n < 64 { + let flag = rd_u8(result); + let (kind, s) = match flag { + Some(0) => { + let p = if readable_range(result + 8, 8) { + core::ptr::read_volatile((result + 8) as *const usize) + } else { + 0 + }; + ("ERROR", rd_cstr(p, 96)) + } + Some(_) => ("SUCCESS", String::from("SUCCESS")), + None => ("??", String::from("")), + }; + let cbref = if readable_range(ctx + 0x18, 8) { + core::ptr::read_volatile((ctx + 0x18) as *const usize) + } else { + 0 + }; + write_log(&format!( + "SEASONS_LOAD_CALLBACK: final kind={kind} result={s:?} flag={flag:?} ctx={ctx:#x} cbref={cbref:#x}\n" + )); + } + let t = FINAL_COMPLETION_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(ctx, result, r8, r9) +} + +// LoadOfflineSeasons STAGE-1 async completion (FUN_180106240): fails with +// "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 { + 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" }; + write_log(&format!( + "SEASONS_STAGE1: result={result:#x} status(+0x1c)={} -> {verdict}\n", + status.map(|x| x.to_string()).unwrap_or_else(|| "??".into()), + )); + } + } + let t = STAGE1_COMPLETION_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(param1, result, r8, r9) +} + unsafe fn worker() { let mut base = 0usize; for _ in 0..600u32 { @@ -346,6 +430,16 @@ unsafe fn worker() { &[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, + ); + 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, + ); write_log("SEASON_TRACE: all season-native traces armed\n"); }