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).
This commit is contained in:
@@ -35,6 +35,23 @@ unsafe fn rd_i32(addr: usize) -> Option<i32> {
|
||||
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 {
|
||||
if addr == 0 || !readable_range(addr, 1) {
|
||||
return String::from("<unreadable>");
|
||||
}
|
||||
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("<result unreadable>")),
|
||||
};
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user