2 Commits

Author SHA1 Message Date
funman300 fe2e531b0c fix(seasons): derive the FUT web-file base from openfut.cfg, not a lab IP
`STAGING_FUT_BASE` hardcoded `http://10.10.0.120:8110/fut/` into the hook
binary, so the base-supply rewriter only worked on one machine and could not be
merged.

The prefix now comes from the same `openfut.cfg` / `openfut-common` source of
truth as every redirect target: `fut_content_base()` builds
`http://<host>:<fut_content_port>/fut/` from the configured host.
`OpenFutPorts` gains `fut_content` with a named default
(`default_ports::FUT_CONTENT = 8110`) and an optional `fut_content_port=` key,
matching how every other OpenFUT port is already handled. A cfg written before
the key existed still parses and takes the default — failing it would disarm the
network redirect too.

Arming fails SAFE: an absent or unusable config arms nothing and the rewriter
leaves every url exactly as the client built it, rather than pointing it at a
guessed host. This also adds the `openfut-common` dependency to the hook on this
branch; main already has it.

Proof: the cross-built artifact no longer contains the string 10.10.0.120
(previously compiled in), openfut-common is green at 16 tests including the new
config-derived-base and backward-compatibility cases, and the launcher builds.
2026-08-21 04:55:14 +00:00
funman300 c3d41153be wip(seasons): offline-Seasons base-supply URL rewriter + VEH crash logger
RETAINED DIAGNOSTIC WIP (pre-existing, brought forward, NOT production-ready).
Rewrites bare WEBFILE relpaths to a season content base and adds a vectored
crash logger used during offline-Seasons RE. Contains a HARDCODED staging
base (http://10.10.0.120:8110/fut/) \u2014 must be env-parameterized before any
promotion to main. Kept on this wip branch so main stays clean.
2026-08-20 16:07:41 +00:00
14 changed files with 454 additions and 235 deletions
Generated
+1
View File
@@ -2293,6 +2293,7 @@ dependencies = [
"eframe",
"egui",
"openfut-common",
"parking_lot",
"serde",
"serde_json",
"tokio",
+56 -2
View File
@@ -53,6 +53,12 @@ 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.
pub const FUT_CONTENT: u16 = 8110;
}
/// OpenFUT destination ports. Each field is where an intercepted EA source port
@@ -66,6 +72,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 +83,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,
}
}
}
@@ -209,6 +219,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}'",
@@ -225,8 +236,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:8110/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
)
}
@@ -414,12 +444,36 @@ mod tests {
https: 8443,
blaze_redirector: 10041,
blaze_main: 42127,
fut_content: 8110,
},
};
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:8110/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.
+5
View File
@@ -2,10 +2,15 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "openfut-common"
version = "0.1.0"
[[package]]
name = "openfut-hook"
version = "0.1.0"
dependencies = [
"openfut-common",
"windows-sys",
]
+4
View File
@@ -39,6 +39,10 @@ windows-sys = { version = "0.59", features = [
"Win32_System_Diagnostics_Debug",
"Win32_System_Kernel",
] }
# Single source of truth for the OpenFUT redirect config (openfut.cfg schema,
# EA-port -> OpenFUT-port map, WinSock byte-order helpers). Shared with the
# launcher so the hook and openfut.cfg agree by construction.
openfut-common = { path = "../openfut-common" }
[profile.release]
opt-level = "s"
+360 -62
View File
@@ -13,8 +13,12 @@
//! (no rip-relative / rel32 in the copied bytes).
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::OnceLock;
use windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache;
use windows_sys::Win32::System::Diagnostics::Debug::{
AddVectoredExceptionHandler, EXCEPTION_POINTERS,
};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
use windows_sys::Win32::System::Memory::{
VirtualAlloc, VirtualProtect, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READ,
@@ -137,15 +141,35 @@ macro_rules! season_call_trace {
};
}
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");
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)");
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)");
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
@@ -186,7 +210,12 @@ unsafe extern "system" fn load_current_impl_wrapper(
// 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 {
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);
@@ -212,7 +241,11 @@ unsafe extern "system" fn completion_wrapper(ctx: usize, result: usize, r8: usiz
// 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)");
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.
@@ -266,7 +299,9 @@ unsafe fn install_detour_reloc(
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"));
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);
@@ -275,7 +310,9 @@ unsafe fn install_detour_reloc(
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"));
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);
@@ -283,7 +320,9 @@ unsafe fn install_detour_reloc(
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"));
write_log(&format!(
"SEASON_TRACE: {name}: trampoline protect failed\n"
));
return false;
}
FlushInstructionCache(GetCurrentProcess(), tramp as _, tramp_len);
@@ -316,7 +355,12 @@ unsafe fn install_detour_reloc(
// 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 {
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) {
@@ -341,22 +385,24 @@ unsafe extern "system" fn final_completion_wrapper(ctx: usize, result: usize, r8
Some(_) => "SUCCESS",
None => "??",
};
let shown = if flag == Some(0) { errstr.as_str() } else { "SUCCESS" };
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"
));
}
// Guarded one-shot bypass (staging diagnostic only): rewrite the pack-names
// failure to SUCCESS so the offline-season load advances to
// LoadCurrentOfflineSeason. Fires only for the exact CACHE_PACKNAMES failure,
// once per process; verified by the error string before touching memory.
// Base-supply experiment: the CACHE_PACKNAMES failure is expected to be fixed
// by the WEBFILE base-supply (the real file now downloads), so the guarded
// success-forcing bypass is DISABLED — a recurring CACHE_PACKNAMES here means
// the base-supply did not take effect and MUST NOT be masked.
if flag == Some(0)
&& errstr.contains("CACHE_PACKNAMES")
&& readable_range(result, 1)
&& !BYPASS_DONE.swap(true, Ordering::AcqRel)
{
core::ptr::write_volatile(result as *mut u8, 1u8); // take the SUCCESS branch
write_log("SEASONS_BYPASS: forced CACHE_PACKNAMES_FAILED -> SUCCESS (one-shot, staging)\n");
write_log("SEASONS_BYPASS: DISABLED (base-supply active); CACHE_PACKNAMES not masked\n");
}
let t = FINAL_COMPLETION_TRAMP.load(Ordering::Acquire);
if t == 0 {
@@ -371,14 +417,23 @@ unsafe extern "system" fn final_completion_wrapper(ctx: usize, result: usize, r8
// "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 {
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" };
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()),
@@ -395,14 +450,63 @@ unsafe extern "system" fn stage1_completion_wrapper(param1: usize, result: usize
}
// 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, rdx: usize, r8: usize, r9: usize) -> usize {
unsafe extern "system" fn url_capture_wrapper(
rcx: usize,
rdx: usize,
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 {
@@ -410,7 +514,80 @@ unsafe extern "system" fn url_capture_wrapper(rcx: usize, rdx: usize, r8: usize,
}
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
}
// ───────────────────────── crash locator (VEH) ──────────────────────────────
// A vectored exception handler that logs the faulting code/address/module for
// fatal exceptions, then lets the crash proceed (EXCEPTION_CONTINUE_SEARCH). It
// pinpoints the StartSeason crash: whether it is a CardsDLL season-data
// null-deref (fixable by supplying matches/opponents) or an engine/other fault.
static CARDS_BASE: AtomicUsize = AtomicUsize::new(0);
static CARDS_SIZE: AtomicUsize = AtomicUsize::new(0);
static CRASH_LOGS: AtomicUsize = AtomicUsize::new(0);
const EXCEPTION_CONTINUE_SEARCH: i32 = 0;
/// OptionalHeader.SizeOfImage from the module's PE headers (fallback 64 MiB).
unsafe fn cards_image_size(base: usize) -> usize {
if !readable_range(base + 0x3c, 4) {
return 0x0400_0000;
}
let e_lfanew = core::ptr::read_volatile((base + 0x3c) as *const u32) as usize;
let so_off = base + e_lfanew + 0x50; // NT header + OptionalHeader.SizeOfImage
if !readable_range(so_off, 4) {
return 0x0400_0000;
}
core::ptr::read_volatile(so_off as *const u32) as usize
}
unsafe extern "system" fn crash_logger(info: *mut EXCEPTION_POINTERS) -> i32 {
if info.is_null() {
return EXCEPTION_CONTINUE_SEARCH;
}
let rec = (*info).ExceptionRecord;
if rec.is_null() {
return EXCEPTION_CONTINUE_SEARCH;
}
let code = (*rec).ExceptionCode as u32;
// Only fatal codes; skip the many benign first-chance SEH exceptions.
let interesting = matches!(
code,
0xC000_0005 // access violation
| 0xC000_001D // illegal instruction
| 0xC000_0094 // integer divide by zero
| 0xC000_00FD // stack overflow
| 0xC000_0025 // noncontinuable exception
);
if !interesting || CRASH_LOGS.fetch_add(1, Ordering::Relaxed) >= 8 {
return EXCEPTION_CONTINUE_SEARCH;
}
let addr = (*rec).ExceptionAddress as usize;
let base = CARDS_BASE.load(Ordering::Acquire);
let size = CARDS_SIZE.load(Ordering::Acquire);
let module = if base != 0 && addr >= base && addr < base + size {
format!("CardsDLL+{:#x}", addr - base)
} else {
"other".to_string()
};
let (kind, fault) = if code == 0xC000_0005 && (*rec).NumberParameters >= 2 {
let op = (*rec).ExceptionInformation[0];
let fa = (*rec).ExceptionInformation[1];
let k = match op {
0 => "read",
1 => "write",
8 => "exec",
_ => "?",
};
(k, fa)
} else {
("", 0usize)
};
write_log(&format!(
"SEASON_CRASH: code={code:#010x} at={addr:#x} module={module} access={kind} fault_addr={fault:#x}\n"
));
EXCEPTION_CONTINUE_SEARCH
}
unsafe fn worker() {
@@ -426,63 +603,144 @@ unsafe fn worker() {
write_log("SEASON_TRACE: CardsDLL unavailable/invalid; season trace inactive\n");
return;
}
CARDS_BASE.store(base, Ordering::Release);
CARDS_SIZE.store(cards_image_size(base), Ordering::Release);
AddVectoredExceptionHandler(1, Some(crash_logger));
write_log("SEASON_TRACE: crash logger (VEH) armed\n");
// (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,
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,
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,
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,
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,
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,
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,
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,
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,
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,
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,
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");
}
@@ -490,6 +748,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"
)),
}
}
+3 -1
View File
@@ -439,7 +439,9 @@ unsafe fn worker() {
pub(crate) fn install() {
// Promoted: armed by the build. No environment variable participates.
REPAIR_ENABLED.store(REPAIR_PROMOTED, Ordering::Release);
crate::write_log("STORE_TABS: bind sensor + pre-warm ARMED (promoted); strict signature gate\n");
crate::write_log(
"STORE_TABS: bind sensor + pre-warm ARMED (promoted); strict signature gate\n",
);
std::thread::spawn(|| unsafe { worker() });
}
+1 -9
View File
@@ -1804,15 +1804,7 @@ impl LauncherApp {
impl eframe::App for LauncherApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Render continuously (present every vsync) instead of reactively. egui
// normally idles at a low, bursty repaint rate; on a G-Sync / FreeSync
// (VRR) display a windowed app that presents in bursts with idle gaps
// makes DWM keep moving the window in and out of the VRR path and the
// refresh rate swing — which the panel shows as flicker. Presenting on
// every frame keeps the window continuously in VRR at the display's own
// (variable) refresh, which is stable. vsync (on by default) paces this to
// the monitor rather than spinning uncapped.
ctx.request_repaint();
ctx.request_repaint_after(std::time::Duration::from_millis(500));
self.drive_restart_queue();
egui::TopBottomPanel::top("header")
+1 -14
View File
@@ -25,7 +25,6 @@ use crate::config::LauncherConfig;
/// Accept only hostname/IP characters. These values come from config fields that
/// are ever only IPs or hostnames, so a surprising character is a bug — reject it
/// rather than try to escape it into an elevated shell command.
#[cfg(unix)]
fn safe_host(s: &str) -> anyhow::Result<&str> {
let t = s.trim();
if t.is_empty() {
@@ -42,7 +41,6 @@ fn safe_host(s: &str) -> anyhow::Result<&str> {
/// Build the privileged arming script. Pure and unit-tested; the effectful part
/// ([`arm`]) only validates config and hands this to the elevated runner.
#[cfg(unix)]
pub(crate) fn arming_script(
server: &str,
redirector_port: u16,
@@ -86,7 +84,6 @@ pub(crate) fn arming_script(
/// Human-readable list of what [`arm`] changed, in the order the script applies
/// it. Logged by the UI so the user sees exactly what was set — not just that
/// "something" ran under `pkexec`.
#[cfg(unix)]
pub(crate) fn arming_summary(
server: &str,
redirector_port: u16,
@@ -106,16 +103,6 @@ pub(crate) fn arming_summary(
/// Arm the client from config, under one elevated prompt. Requires the same
/// fields preflight reads; a missing one is a clear error, never a silent
/// loopback fallback. Returns the applied changes for the UI to surface.
/// On native Windows there is nothing to arm: routing is the `openfut.cfg` the
/// client-files step writes into the game directory (read by the version.dll
/// hook), and there is no `ptrace_scope`, DNAT, or `/etc/hosts` to set. Returns
/// no changes so the launch sequence treats client preparation as satisfied.
#[cfg(windows)]
pub fn arm(_cfg: &LauncherConfig) -> anyhow::Result<Vec<String>> {
Ok(Vec::new())
}
#[cfg(unix)]
pub fn arm(cfg: &LauncherConfig) -> anyhow::Result<Vec<String>> {
let server = cfg.openfut_server_host.trim();
if server.is_empty() {
@@ -141,7 +128,7 @@ pub fn arm(cfg: &LauncherConfig) -> anyhow::Result<Vec<String>> {
))
}
#[cfg(all(test, unix))]
#[cfg(test)]
mod tests {
use super::*;
+19 -28
View File
@@ -54,18 +54,13 @@ pub struct GameProfile {
impl GameProfile {
/// Whether this profile is filled in enough to launch from.
pub fn configured(&self) -> bool {
// Windows starts the executable directly (no runner); unix needs a
// runner such as umu-run.
#[cfg(windows)]
let runner_ok = true;
#[cfg(unix)]
let runner_ok = !self.runner.trim().is_empty();
runner_ok && !self.executable.trim().is_empty() && !self.game_dir.trim().is_empty()
!self.runner.trim().is_empty()
&& !self.executable.trim().is_empty()
&& !self.game_dir.trim().is_empty()
}
/// Reject a half-filled profile rather than launching something surprising.
pub fn validate(&self) -> Result<(), String> {
#[cfg(unix)]
if self.runner.trim().is_empty() {
return Err("Game profile has no runner (e.g. umu-run).".into());
}
@@ -75,28 +70,23 @@ impl GameProfile {
if self.game_dir.trim().is_empty() {
return Err("Game profile has no game directory.".into());
}
// Wine-prefix links and the DRM licence precondition only exist on the
// unix/Proton launch path; native Windows has neither.
#[cfg(unix)]
{
if !self.prefix_links.is_empty() && self.wine_prefix.trim().is_empty() {
return Err("Game profile defines prefix links but no wine_prefix.".into());
if !self.prefix_links.is_empty() && self.wine_prefix.trim().is_empty() {
return Err("Game profile defines prefix links but no wine_prefix.".into());
}
for l in &self.prefix_links {
if l.link.trim().is_empty() || l.target.trim().is_empty() {
return Err("Game profile has a prefix link with an empty link or target.".into());
}
for l in &self.prefix_links {
if l.link.trim().is_empty() || l.target.trim().is_empty() {
return Err("Game profile has a prefix link with an empty link or target.".into());
}
if std::path::Path::new(&l.link).is_absolute() {
return Err(format!(
"Prefix link {:?} must be relative to the Wine prefix.",
l.link
));
}
if std::path::Path::new(&l.link).is_absolute() {
return Err(format!(
"Prefix link {:?} must be relative to the Wine prefix.",
l.link
));
}
if let Some(lic) = &self.license {
if lic.path.trim().is_empty() || lic.generator.trim().is_empty() {
return Err("Game profile licence needs both a path and a generator.".into());
}
}
if let Some(lic) = &self.license {
if lic.path.trim().is_empty() || lic.generator.trim().is_empty() {
return Err("Game profile licence needs both a path and a generator.".into());
}
}
Ok(())
@@ -324,6 +314,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,
},
}
}
+2 -72
View File
@@ -24,13 +24,11 @@
//! falls back to it, so an existing working setup cannot be broken by upgrading.
use parking_lot::Mutex;
#[cfg(unix)]
use std::collections::BTreeMap;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
#[cfg(unix)]
use std::time::{Duration, Instant};
use crate::config::GameProfile;
@@ -47,7 +45,6 @@ fn say(log: &Log, msg: impl Into<String>) {
/// Returns once the game process has been spawned; its output continues to
/// stream into `log` on background threads. `on_exit` fires when the process
/// ends, which is how the launch state machine leaves its Running state.
#[cfg(unix)]
pub fn launch(
profile: &GameProfile,
log: &Log,
@@ -99,62 +96,6 @@ pub fn launch(
Ok(())
}
/// Windows-native launch: no Wine prefix, no `WINEDLLOVERRIDES` (the game loads
/// the `version.dll` hook from its own directory through the normal search
/// order), and no licence regeneration (the native loader handles DRM).
/// Routing is the `openfut.cfg` that the client-files step already wrote into
/// the game directory.
///
/// The launcher must itself be running elevated (its shortcut carries the
/// RunAsAdmin bit): the loader requires administrator rights, and a child
/// started with `CreateProcess` inherits the launcher's token instead of
/// raising its own UAC prompt.
#[cfg(windows)]
pub fn launch(
profile: &GameProfile,
log: &Log,
on_exit: impl FnOnce() + Send + 'static,
) -> anyhow::Result<()> {
profile.validate().map_err(anyhow::Error::msg)?;
let game_dir = PathBuf::from(&profile.game_dir);
if !game_dir.is_dir() {
anyhow::bail!("game_dir does not exist: {}", game_dir.display());
}
let exe = game_dir.join(&profile.executable);
if !exe.is_file() {
anyhow::bail!("game executable not found: {}", exe.display());
}
let mut cmd = Command::new(&exe);
cmd.current_dir(&game_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (k, v) in &profile.env {
cmd.env(k, v);
}
say(
log,
format!(
"[launcher] launching {} (cwd {})",
exe.display(),
game_dir.display()
),
);
let child = cmd
.spawn()
.map_err(|e| anyhow::anyhow!("could not start {}: {e}", exe.display()))?;
stream(
child,
log.clone(),
"[launcher] game process exited.",
on_exit,
);
Ok(())
}
/// The registry key Wine reads DLL overrides from, and the one value the hook needs.
///
/// Wine loads its own builtin `version.dll` unless an override says otherwise, so the
@@ -169,17 +110,13 @@ pub fn launch(
/// survives restarts and applies to every launch path, including Steam. This mirrors
/// what BepInEx documents for Proton (configure the proxy in winecfg rather than the
/// environment) and what Proton itself already does in this prefix for other titles.
#[cfg(unix)]
const DLL_OVERRIDE_KEY: &str = r"HKCU\Software\Wine\DllOverrides";
#[cfg(unix)]
const HOOK_DLL_VALUE: &str = "version";
#[cfg(unix)]
const HOOK_DLL_OVERRIDE: &str = "native,builtin";
/// `reg add` argv that persists the hook's DLL override, native-first with a builtin
/// fallback. `/f` makes it idempotent, so this is safe to run on every launch and
/// repairs a prefix a player has reset or replaced.
#[cfg(unix)]
fn dll_override_args() -> [&'static str; 10] {
[
"reg",
@@ -201,7 +138,6 @@ fn dll_override_args() -> [&'static str; 10] {
/// Best-effort by design: a failure here is not fatal, because a launch we spawn also
/// carries `WINEDLLOVERRIDES`. It is reported in plain language rather than as a Wine
/// error, since the player cannot act on the latter.
#[cfg(unix)]
fn ensure_dll_override(profile: &GameProfile, log: &Log) {
if profile.wine_prefix.trim().is_empty() {
return;
@@ -227,7 +163,7 @@ fn ensure_dll_override(profile: &GameProfile, log: &Log) {
}
}
#[cfg(all(test, unix))]
#[cfg(test)]
mod override_tests {
use super::*;
@@ -268,7 +204,6 @@ mod override_tests {
///
/// A profile that already pins `version=` wins: an operator overriding the hijack
/// deliberately must not be silently overruled.
#[cfg(unix)]
fn hook_dll_overrides(env: &BTreeMap<String, String>) -> String {
const HOOK: &str = "version=n,b";
match env.get("WINEDLLOVERRIDES").map(|v| v.trim()) {
@@ -282,7 +217,6 @@ fn hook_dll_overrides(env: &BTreeMap<String, String>) -> String {
///
/// Equivalent to `mkdir -p $WINEPREFIX/dosdevices && ln -sfn <target> <link>`:
/// an existing link is replaced, so re-running is harmless.
#[cfg(unix)]
fn prepare_prefix(profile: &GameProfile, log: &Log) -> anyhow::Result<()> {
if profile.wine_prefix.trim().is_empty() || profile.prefix_links.is_empty() {
return Ok(());
@@ -323,7 +257,6 @@ fn prepare_prefix(profile: &GameProfile, log: &Log) -> anyhow::Result<()> {
/// A crashed or failed launch deletes the licence, so this runs before every
/// launch rather than only on first setup — that is the behaviour the shell
/// script proved, and it is why a crash is normally self-healing on the next try.
#[cfg(unix)]
fn ensure_license(profile: &GameProfile, log: &Log) -> anyhow::Result<()> {
let Some(lic) = &profile.license else {
return Ok(());
@@ -383,7 +316,6 @@ fn ensure_license(profile: &GameProfile, log: &Log) -> anyhow::Result<()> {
/// and it is reproduced deliberately — the pattern is a Windows executable name,
/// which cannot match the launcher or a shell running it. (A `pkill -f` pattern
/// that *can* match its own caller is a real hazard; this one cannot.)
#[cfg(unix)]
fn stop_generator(child: &mut Child, lic: &crate::config::LicenseCheck, log: &Log) {
let _ = child.kill();
let _ = child.wait();
@@ -401,7 +333,6 @@ fn stop_generator(child: &mut Child, lic: &crate::config::LicenseCheck, log: &Lo
/// A relative licence path is taken as relative to the Wine prefix; an absolute
/// one is used as given.
#[cfg(unix)]
fn resolve_under_prefix(prefix: &str, path: &str) -> PathBuf {
let p = Path::new(path);
if p.is_absolute() || prefix.trim().is_empty() {
@@ -414,7 +345,6 @@ fn resolve_under_prefix(prefix: &str, path: &str) -> PathBuf {
/// The script's `[[ -s FILE ]]`: present *and* non-empty. A zero-byte licence is
/// as useless as a missing one, and treating it as valid would skip the
/// regeneration that fixes it.
#[cfg(unix)]
fn non_empty_file(path: &Path) -> bool {
std::fs::metadata(path)
.map(|m| m.len() > 0)
@@ -451,7 +381,7 @@ pub fn stream(
});
}
#[cfg(all(test, unix))]
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{LicenseCheck, PrefixLink};
-18
View File
@@ -22,7 +22,6 @@ use std::{
time::{Duration, Instant},
};
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use crate::fifa17_capability::{
@@ -434,21 +433,6 @@ impl ServiceSupervisor {
/// Start `service` only if it is not already usable. Never restarts a healthy
/// service, and never adopts a foreign one as ours.
pub fn ensure_running(&mut self, service: Service, spec: SpawnSpec) -> Result<Ensured, String> {
// On native Windows the "companion services" are NOT separate processes:
// LSX/Origin login and the ProtoSSL cert path are provided in-process by
// stp-origin_emu.dll and the version.dll hook once the game runs. There is
// nothing for the launcher to spawn or supervise, so report ready.
#[cfg(windows)]
{
let _ = spec;
self.log.lock().push(format!(
"[launcher] {} is in-process on Windows (stp/hook) — nothing to start.",
service.label()
));
return Ok(Ensured::Reused);
}
#[cfg(unix)]
{
let runtime = self.observe(service);
if runtime.ready() {
self.log.lock().push(format!(
@@ -476,7 +460,6 @@ impl ServiceSupervisor {
.map_err(|e| e.to_string())?;
*self.slot(service) = ManagedService::from_child(child);
Ok(Ensured::Started)
}
}
/// Stop a service the launcher owns. A foreign process is reported, never
@@ -549,7 +532,6 @@ pub fn spawn(
cmd.env("OPENFUT_AUTOPATCH_LOG", log_path);
}
// Put each companion in its own process group for lifecycle isolation.
#[cfg(unix)]
cmd.process_group(0);
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
-3
View File
@@ -22,9 +22,6 @@ fn main() -> eframe::Result<()> {
.with_icon(app_icon())
.with_inner_size([1040.0, 720.0])
.with_min_inner_size([880.0, 600.0]),
// Pair vsync with the display's VRR (G-Sync + Vsync is the recommended
// combination): frames present on the monitor's own variable refresh.
vsync: true,
..Default::default()
};
+1 -17
View File
@@ -31,7 +31,6 @@ use std::time::Duration;
use crate::config::LauncherConfig;
const PROBE_TIMEOUT: Duration = Duration::from_secs(2);
#[cfg(unix)]
const PTRACE_SCOPE: &str = "/proc/sys/kernel/yama/ptrace_scope";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -87,7 +86,6 @@ impl Check {
}
/// Run every applicable check. Order is the order the game exercises them.
#[cfg(unix)]
pub fn run(cfg: &LauncherConfig) -> Vec<Check> {
vec![
ptrace_scope(),
@@ -98,16 +96,6 @@ pub fn run(cfg: &LauncherConfig) -> Vec<Check> {
]
}
/// On native Windows the client-preparation checks (ptrace_scope, the EA
/// redirector DNAT, `/etc/hosts`) do not apply: there is no host to arm and
/// routing is entirely the `openfut.cfg` the hook reads. Only the two the game
/// truly depends on remain: the backend is reachable and the deployed hook
/// config agrees with the launcher's settings.
#[cfg(windows)]
pub fn run(cfg: &LauncherConfig) -> Vec<Check> {
vec![backend_reachable(cfg), hook_config(cfg)]
}
/// Checks that will stop the game working.
pub fn failures(checks: &[Check]) -> usize {
checks.iter().filter(|c| c.state == State::Fail).count()
@@ -125,7 +113,6 @@ pub fn warnings(checks: &[Check]) -> usize {
/// Unconditional. autopatch is a workspace binary that ships alongside the
/// launcher, so there is no configuration that could make this inapplicable —
/// every launch runs it.
#[cfg(unix)]
fn ptrace_scope() -> Check {
const NAME: &str = "ptrace_scope (autopatch)";
match std::fs::read_to_string(PTRACE_SCOPE) {
@@ -140,7 +127,6 @@ fn ptrace_scope() -> Check {
/// Reading `/proc` in a test would assert facts about the machine running the
/// suite rather than about this code — and left inline, "any value is fine"
/// was a mutation no test could catch.
#[cfg(unix)]
fn ptrace_verdict(raw: &str) -> Check {
const NAME: &str = "ptrace_scope (autopatch)";
let v = raw.trim();
@@ -160,7 +146,6 @@ fn ptrace_verdict(raw: &str) -> Check {
///
/// This tests the *effect* rather than reading firewall rules, so it needs no
/// privilege and stays honest about what the game will actually experience.
#[cfg(unix)]
fn ea_redirect(cfg: &LauncherConfig) -> Check {
const NAME: &str = "EA redirector IP is redirected";
let ip = cfg.ea_redirect_probe_ip.trim();
@@ -199,7 +184,6 @@ fn ea_redirect(cfg: &LauncherConfig) -> Check {
/// So this is a real misconfiguration worth fixing and not a reason to expect
/// failure. Reporting it as fatal, and then being contradicted by a working
/// game, is how a checklist trains its user to ignore it.
#[cfg(unix)]
fn hostname_mapping(cfg: &LauncherConfig) -> Check {
const NAME: &str = "EA hostnames point at OpenFUT";
if cfg.ea_hostnames.is_empty() {
@@ -336,7 +320,7 @@ fn join(ips: &[IpAddr]) -> String {
.join(",")
}
#[cfg(all(test, unix))]
#[cfg(test)]
mod tests {
use super::*;
+1 -9
View File
@@ -299,13 +299,5 @@ fn install_style(ctx: &Context) {
v.widgets.open.rounding = radius;
style.visuals = v;
// egui 0.29 keeps a separate `Style` per theme (dark/light) and renders with
// whichever the theme preference resolves to. `set_style` touches only the
// currently-active theme, so a later switch to the other one would drop our
// named text styles ("Hero", "Subheading", …) and panic in `TextStyle::resolve`.
// Install the full style into BOTH themes and pin the preference to Dark so
// the branded look is stable regardless of the host's system theme.
ctx.set_style_of(egui::Theme::Dark, style.clone());
ctx.set_style_of(egui::Theme::Light, style);
ctx.set_theme(egui::ThemePreference::Dark);
ctx.set_style(style);
}