7edf682291
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).
620 lines
22 KiB
Rust
620 lines
22 KiB
Rust
//! Passive, behavior-preserving diagnostic traces for FIFA 17's offline-season
|
|
//! entry flow.
|
|
//!
|
|
//! RE (2026-08-19, live memory) placed the "problem communicating with the FIFA
|
|
//! Ultimate Team servers" modal in the `futOfflineSeasonEntry` ActionScript's
|
|
//! season-load path. A first trace on the load completion `FUN_1800578e0`
|
|
//! (`0x578e0`) armed but NEVER fired on an entry attempt — so the modal is raised
|
|
//! before that callback runs. These traces log the actual CardsDLL season-native
|
|
//! call sequence (which functions the entry screen reaches, and in what order) so
|
|
//! we can see exactly where the flow stops/fails. Every trace is read-only: it
|
|
//! logs, then calls the original through a trampoline; it never alters control
|
|
//! flow. Targets are chosen so their copied prologues are position-independent
|
|
//! (no rip-relative / rel32 in the copied bytes).
|
|
|
|
use core::sync::atomic::{AtomicUsize, Ordering};
|
|
|
|
use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache;
|
|
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
|
|
use windows_sys::Win32::System::Memory::{
|
|
VirtualAlloc, VirtualProtect, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READ,
|
|
PAGE_EXECUTE_READWRITE, PAGE_READWRITE,
|
|
};
|
|
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
|
|
|
use crate::sbc_trace::{
|
|
absolute_jump, allocate_trampoline, readable_range, target_va, validate_cards_build,
|
|
};
|
|
use crate::write_log;
|
|
|
|
static REPORTS: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
pub(crate) unsafe fn rd_i32(addr: usize) -> Option<i32> {
|
|
readable_range(addr, 4).then(|| core::ptr::read_volatile(addr as *const i32))
|
|
}
|
|
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).
|
|
pub(crate) 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
|
|
/// `wrapper`; the wrapper calls the trampoline (copied prologue + jump back).
|
|
pub(crate) unsafe fn install_detour(
|
|
base: usize,
|
|
rva: usize,
|
|
name: &str,
|
|
copy_len: usize,
|
|
signature: &[u8],
|
|
wrapper: usize,
|
|
trampoline_slot: &AtomicUsize,
|
|
) -> bool {
|
|
let Some(target) = target_va(base, rva) else {
|
|
write_log(&format!("SEASON_TRACE: {name}: VA overflow\n"));
|
|
return false;
|
|
};
|
|
if !readable_range(target, copy_len)
|
|
|| core::slice::from_raw_parts(target as *const u8, copy_len) != signature
|
|
{
|
|
write_log(&format!(
|
|
"SEASON_TRACE: {name}: prologue signature mismatch at {target:#x}; skip\n"
|
|
));
|
|
return false;
|
|
}
|
|
let Some(trampoline) = allocate_trampoline(target, copy_len) else {
|
|
write_log(&format!("SEASON_TRACE: {name}: trampoline alloc failed\n"));
|
|
return false;
|
|
};
|
|
trampoline_slot.store(trampoline, Ordering::Release);
|
|
let mut patch = [0x90u8; 24];
|
|
let jump = absolute_jump(wrapper);
|
|
patch[..jump.len()].copy_from_slice(&jump);
|
|
let mut old = 0u32;
|
|
if VirtualProtect(target as _, copy_len, PAGE_EXECUTE_READWRITE, &mut old) == 0 {
|
|
write_log(&format!("SEASON_TRACE: {name}: VirtualProtect failed\n"));
|
|
return false;
|
|
}
|
|
core::ptr::copy_nonoverlapping(patch.as_ptr(), target as *mut u8, copy_len);
|
|
let flushed = FlushInstructionCache(GetCurrentProcess(), target as _, copy_len) != 0;
|
|
let mut ignored = 0u32;
|
|
VirtualProtect(target as _, copy_len, old, &mut ignored);
|
|
if flushed {
|
|
write_log(&format!(
|
|
"SEASON_TRACE: {name}: installed at {target:#x} (tramp {trampoline:#x})\n"
|
|
));
|
|
true
|
|
} else {
|
|
write_log(&format!("SEASON_TRACE: {name}: flush failed\n"));
|
|
false
|
|
}
|
|
}
|
|
|
|
fn log_call(name: &str, rcx: usize, rdx: usize, r8: usize) {
|
|
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
|
|
if n < 64 {
|
|
write_log(&format!(
|
|
"SEASON_CALL: {name} rcx={rcx:#x} rdx={rdx:#x} r8={r8:#x}\n"
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Declare a passive 4-register-arg call trace. The wrapper is entered via the
|
|
/// abs-jump patched over the target prologue (original args in rcx/rdx/r8/r9,
|
|
/// caller's return address on the stack), logs, then tail-calls the original via
|
|
/// the trampoline. A 4-arg/usize-return signature safely covers these season
|
|
/// natives (<=4 integer args, void/int returns).
|
|
macro_rules! season_call_trace {
|
|
($wrap:ident, $tramp:ident, $name:literal) => {
|
|
static $tramp: AtomicUsize = AtomicUsize::new(0);
|
|
unsafe extern "system" fn $wrap(rcx: usize, rdx: usize, r8: usize, r9: usize) -> usize {
|
|
log_call($name, rcx, rdx, r8);
|
|
let t = $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)
|
|
}
|
|
};
|
|
}
|
|
|
|
season_call_trace!(
|
|
load_current_native_wrapper,
|
|
LOAD_CURRENT_NATIVE_TRAMP,
|
|
"LoadCurrentOfflineSeason_native"
|
|
);
|
|
season_call_trace!(
|
|
start_season_native_wrapper,
|
|
START_SEASON_NATIVE_TRAMP,
|
|
"StartSeason_native"
|
|
);
|
|
season_call_trace!(
|
|
get_info_native_wrapper,
|
|
GET_INFO_NATIVE_TRAMP,
|
|
"GetOfflineSeasonInfo_native"
|
|
);
|
|
// Real LoadOfflineSeasons native (FUN_18004ee10) — what _LoadCurrentSeason
|
|
// actually calls; hands the callback name to the manager's async slot 0x80.
|
|
season_call_trace!(
|
|
load_offline_real_wrapper,
|
|
LOAD_OFFLINE_REAL_TRAMP,
|
|
"LoadOfflineSeasons_native(0x4ee10)"
|
|
);
|
|
// Async LoadOfflineSeasons impl (mgr slot 0x80, FUN_180057560): reads the season
|
|
// count and invokes the LoadSeasons_Complete AS callback.
|
|
season_call_trace!(
|
|
load_offline_async_wrapper,
|
|
LOAD_OFFLINE_ASYNC_TRAMP,
|
|
"LoadOfflineSeasons_asyncimpl(0x57560)"
|
|
);
|
|
|
|
// LoadCurrentOfflineSeason IMPL (manager slot 0x20): registers the load callbacks
|
|
// and starts the async op. param_1=manager, param_2=state byte, param_3=seasonId
|
|
// string ptr. Logs those, then calls the original.
|
|
static LOAD_CURRENT_IMPL_TRAMP: AtomicUsize = AtomicUsize::new(0);
|
|
unsafe extern "system" fn load_current_impl_wrapper(
|
|
param_1: usize,
|
|
param_2: usize,
|
|
param_3: usize,
|
|
param_4: usize,
|
|
) -> usize {
|
|
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
|
|
if n < 64 {
|
|
// param_3 -> C string season id (best-effort read of first bytes).
|
|
let sid = if param_3 != 0 && readable_range(param_3, 8) {
|
|
let p = *(param_3 as *const usize);
|
|
if p != 0 && readable_range(p, 8) {
|
|
*(p as *const u64)
|
|
} else {
|
|
0
|
|
}
|
|
} else {
|
|
0
|
|
};
|
|
write_log(&format!(
|
|
"SEASON_CALL: LoadCurrentOfflineSeason_impl mgr={param_1:#x} stateByte={param_2:#x} sidPtr={param_3:#x} sidHead={sid:#x}\n"
|
|
));
|
|
}
|
|
let t = LOAD_CURRENT_IMPL_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(param_1, param_2, param_3, param_4)
|
|
}
|
|
|
|
// Completion callback FUN_1800578e0 (0x578e0). Kept from the first pass to confirm
|
|
// whether it ever fires; logs the result fields it branches on.
|
|
static COMPLETION_TRAMP: AtomicUsize = AtomicUsize::new(0);
|
|
unsafe extern "system" fn completion_wrapper(
|
|
ctx: usize,
|
|
result: usize,
|
|
r8: usize,
|
|
r9: usize,
|
|
) -> usize {
|
|
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
|
|
if n < 64 {
|
|
let status = rd_i32(result + 0x1c);
|
|
let state = rd_u8(result + 0x68);
|
|
let season_id = rd_i32(result + 0x5c);
|
|
write_log(&format!(
|
|
"SEASON_LOAD_COMPLETE: ctx={ctx:#x} result={result:#x} status(+0x1c)={} state(+0x68)={} seasonId(+0x5c)={}\n",
|
|
status.map(|x| x.to_string()).unwrap_or_else(|| "??".into()),
|
|
state.map(|x| x.to_string()).unwrap_or_else(|| "??".into()),
|
|
season_id.map(|x| x.to_string()).unwrap_or_else(|| "??".into()),
|
|
));
|
|
}
|
|
let t = 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)
|
|
}
|
|
|
|
// GetUsersOfflineDivision native FUN_18004eb50 (registration FUN_18004e3f0 proved
|
|
// this binding — it is NOT LoadOfflineSeasons). Called from _InitializeScreen for
|
|
// the division display, sync. Its prologue holds a rip-relative `MOV RCX,[rip+disp]`,
|
|
// so it needs the relocating installer below.
|
|
season_call_trace!(
|
|
get_users_division_wrapper,
|
|
GET_USERS_DIVISION_TRAMP,
|
|
"GetUsersOfflineDivision_native(0x4eb50)"
|
|
);
|
|
|
|
/// Find a free page within ~±1.5 GiB of `base`, so a rip-relative disp32 into
|
|
/// CardsDLL data still fits after we relocate a copied prologue into it.
|
|
unsafe fn alloc_near(base: usize, size: usize) -> Option<usize> {
|
|
const GRAN: usize = 0x10000;
|
|
let mut step = GRAN;
|
|
while step < 0x6000_0000 {
|
|
for signed in [step as isize, -(step as isize)] {
|
|
let cand = base.wrapping_add(signed as usize) & !(GRAN - 1);
|
|
if cand == 0 {
|
|
continue;
|
|
}
|
|
let p = VirtualAlloc(cand as _, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
|
if !p.is_null() {
|
|
return Some(p as usize);
|
|
}
|
|
}
|
|
step += GRAN;
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Passive detour for a target whose copied prologue contains a single
|
|
/// rip-relative operand (disp32 at `disp_off`, instruction ending at `insn_end`,
|
|
/// 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)]
|
|
pub(crate) unsafe fn install_detour_reloc(
|
|
base: usize,
|
|
rva: usize,
|
|
name: &str,
|
|
copy_len: usize,
|
|
signature: &[u8],
|
|
disp_off: usize,
|
|
insn_end: usize,
|
|
wrapper: usize,
|
|
trampoline_slot: &AtomicUsize,
|
|
) -> bool {
|
|
let Some(target) = target_va(base, rva) else {
|
|
write_log(&format!("SEASON_TRACE: {name}: VA overflow\n"));
|
|
return false;
|
|
};
|
|
if !readable_range(target, copy_len)
|
|
|| core::slice::from_raw_parts(target as *const u8, copy_len) != signature
|
|
{
|
|
write_log(&format!(
|
|
"SEASON_TRACE: {name}: prologue signature mismatch at {target:#x}; skip\n"
|
|
));
|
|
return false;
|
|
}
|
|
let jump = absolute_jump(wrapper);
|
|
let tramp_len = copy_len + jump.len();
|
|
let Some(tramp) = alloc_near(base, tramp_len) else {
|
|
write_log(&format!(
|
|
"SEASON_TRACE: {name}: near trampoline alloc failed\n"
|
|
));
|
|
return false;
|
|
};
|
|
core::ptr::copy_nonoverlapping(target as *const u8, tramp as *mut u8, copy_len);
|
|
// Relocate the rip-relative disp32 to keep the same absolute target.
|
|
let orig_disp = core::ptr::read_unaligned((target + disp_off) as *const i32) as i64;
|
|
let abs_target = target as i64 + insn_end as i64 + orig_disp;
|
|
let new_disp = abs_target - (tramp as i64 + insn_end as i64);
|
|
if new_disp < i32::MIN as i64 || new_disp > i32::MAX as i64 {
|
|
write_log(&format!(
|
|
"SEASON_TRACE: {name}: reloc out of range ({new_disp:#x})\n"
|
|
));
|
|
return false;
|
|
}
|
|
core::ptr::write_unaligned((tramp + disp_off) as *mut i32, new_disp as i32);
|
|
let back = absolute_jump(target + copy_len);
|
|
core::ptr::copy_nonoverlapping(back.as_ptr(), (tramp + copy_len) as *mut u8, back.len());
|
|
let mut old = 0u32;
|
|
if VirtualProtect(tramp as _, tramp_len, PAGE_EXECUTE_READ, &mut old) == 0 {
|
|
write_log(&format!(
|
|
"SEASON_TRACE: {name}: trampoline protect failed\n"
|
|
));
|
|
return false;
|
|
}
|
|
FlushInstructionCache(GetCurrentProcess(), tramp as _, tramp_len);
|
|
trampoline_slot.store(tramp, Ordering::Release);
|
|
let mut patch = [0x90u8; 24];
|
|
patch[..jump.len()].copy_from_slice(&jump);
|
|
let mut prot = 0u32;
|
|
if VirtualProtect(target as _, copy_len, PAGE_EXECUTE_READWRITE, &mut prot) == 0 {
|
|
write_log(&format!("SEASON_TRACE: {name}: VirtualProtect failed\n"));
|
|
return false;
|
|
}
|
|
core::ptr::copy_nonoverlapping(patch.as_ptr(), target as *mut u8, copy_len);
|
|
let flushed = FlushInstructionCache(GetCurrentProcess(), target as _, copy_len) != 0;
|
|
let mut ignored = 0u32;
|
|
VirtualProtect(target as _, copy_len, prot, &mut ignored);
|
|
if flushed {
|
|
write_log(&format!(
|
|
"SEASON_TRACE: {name}: installed(reloc) at {target:#x} (tramp {tramp:#x})\n"
|
|
));
|
|
true
|
|
} else {
|
|
write_log(&format!("SEASON_TRACE: {name}: flush failed\n"));
|
|
false
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
// Read the delivered status: byte0==0 => failure with an error string at +8.
|
|
let flag = rd_u8(result);
|
|
let errstr = if flag == Some(0) {
|
|
let p = if readable_range(result + 8, 8) {
|
|
core::ptr::read_volatile((result + 8) as *const usize)
|
|
} else {
|
|
0
|
|
};
|
|
rd_cstr(p, 96)
|
|
} else {
|
|
String::new()
|
|
};
|
|
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
|
|
if n < 64 {
|
|
let cbref = if readable_range(ctx + 0x18, 8) {
|
|
core::ptr::read_volatile((ctx + 0x18) as *const usize)
|
|
} else {
|
|
0
|
|
};
|
|
let kind = match flag {
|
|
Some(0) => "ERROR",
|
|
Some(_) => "SUCCESS",
|
|
None => "??",
|
|
};
|
|
let shown = if flag == Some(0) {
|
|
errstr.as_str()
|
|
} else {
|
|
"SUCCESS"
|
|
};
|
|
write_log(&format!(
|
|
"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;
|
|
}
|
|
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)
|
|
}
|
|
|
|
// 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).
|
|
static URL_CAPTURE_TRAMP: AtomicUsize = AtomicUsize::new(0);
|
|
unsafe extern "system" fn url_capture_wrapper(
|
|
rcx: usize,
|
|
rdx: usize,
|
|
r8: usize,
|
|
r9: usize,
|
|
) -> usize {
|
|
let n = REPORTS.fetch_add(1, Ordering::Relaxed);
|
|
if n < 64 {
|
|
write_log(&format!(
|
|
"SEASONS_WEBFILE_URL: url={:?}\n",
|
|
rd_cstr(rcx, 256)
|
|
));
|
|
}
|
|
let t = URL_CAPTURE_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("SEASON_TRACE: CardsDLL unavailable/invalid; season trace inactive\n");
|
|
return;
|
|
}
|
|
// (rva, name, copy_len, signature, wrapper, trampoline slot)
|
|
install_detour(
|
|
base,
|
|
0x4eb70,
|
|
"LoadCurrentOfflineSeason_native",
|
|
15,
|
|
&[
|
|
0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff,
|
|
0xff,
|
|
],
|
|
load_current_native_wrapper as *const () as usize,
|
|
&LOAD_CURRENT_NATIVE_TRAMP,
|
|
);
|
|
install_detour(
|
|
base,
|
|
0x4f340,
|
|
"StartSeason_native",
|
|
15,
|
|
&[
|
|
0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff,
|
|
0xff,
|
|
],
|
|
start_season_native_wrapper as *const () as usize,
|
|
&START_SEASON_NATIVE_TRAMP,
|
|
);
|
|
install_detour(
|
|
base,
|
|
0x4e850,
|
|
"GetOfflineSeasonInfo_native",
|
|
15,
|
|
&[
|
|
0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x6c, 0x24, 0x10, 0x48, 0x89, 0x74, 0x24,
|
|
0x18,
|
|
],
|
|
get_info_native_wrapper as *const () as usize,
|
|
&GET_INFO_NATIVE_TRAMP,
|
|
);
|
|
install_detour(
|
|
base,
|
|
0x57230,
|
|
"LoadCurrentOfflineSeason_impl",
|
|
19,
|
|
&[
|
|
0x48, 0x8b, 0xc4, 0x57, 0x48, 0x81, 0xec, 0x80, 0x00, 0x00, 0x00, 0x48, 0xc7, 0x40,
|
|
0x98, 0xfe, 0xff, 0xff, 0xff,
|
|
],
|
|
load_current_impl_wrapper as *const () as usize,
|
|
&LOAD_CURRENT_IMPL_TRAMP,
|
|
);
|
|
install_detour(
|
|
base,
|
|
0x578e0,
|
|
"LoadCurrentOfflineSeason_completion",
|
|
16,
|
|
&[
|
|
0x48, 0x8b, 0xc4, 0x57, 0x48, 0x83, 0xec, 0x70, 0x48, 0xc7, 0x40, 0xd0, 0xfe, 0xff,
|
|
0xff, 0xff,
|
|
],
|
|
completion_wrapper as *const () as usize,
|
|
&COMPLETION_TRAMP,
|
|
);
|
|
install_detour_reloc(
|
|
base,
|
|
0x4eb50,
|
|
"GetUsersOfflineDivision_native",
|
|
14,
|
|
&[
|
|
0x48, 0x83, 0xec, 0x28, 0x48, 0x8b, 0x0d, 0x75, 0x18, 0x29, 0x00, 0x48, 0x8b, 0x01,
|
|
],
|
|
7,
|
|
11,
|
|
get_users_division_wrapper as *const () as usize,
|
|
&GET_USERS_DIVISION_TRAMP,
|
|
);
|
|
install_detour(
|
|
base,
|
|
0x4ee10,
|
|
"LoadOfflineSeasons_native",
|
|
15,
|
|
&[
|
|
0x40, 0x57, 0x48, 0x83, 0xec, 0x60, 0x48, 0xc7, 0x44, 0x24, 0x20, 0xfe, 0xff, 0xff,
|
|
0xff,
|
|
],
|
|
load_offline_real_wrapper as *const () as usize,
|
|
&LOAD_OFFLINE_REAL_TRAMP,
|
|
);
|
|
install_detour(
|
|
base,
|
|
0x57560,
|
|
"LoadOfflineSeasons_asyncimpl",
|
|
17,
|
|
&[
|
|
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,
|
|
);
|
|
install_detour_reloc(
|
|
base,
|
|
0x17ff90,
|
|
"start_webfile_dl_url",
|
|
14,
|
|
&[
|
|
0x48, 0x83, 0xec, 0x38, 0x4c, 0x8b, 0x05, 0xe5, 0x65, 0x16, 0x00, 0x4c, 0x8b, 0xd1,
|
|
],
|
|
7,
|
|
11,
|
|
url_capture_wrapper as *const () as usize,
|
|
&URL_CAPTURE_TRAMP,
|
|
);
|
|
write_log("SEASON_TRACE: all season-native traces armed\n");
|
|
}
|
|
|
|
/// 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() {
|
|
write_log("SEASON_TRACE: requested; deferred signature validation starting\n");
|
|
std::thread::spawn(|| unsafe { worker() });
|
|
}
|