LSX: push OnlineStatusEvent + Login to drive FIFA online (M2 probe)
Add an unsolicited event-push path to the LSX server: after the session establishes, re-push (every 2s) a Login then OnlineStatusEvent, encrypted with the live session seed and null-terminated like any session frame. Formats RE'd from FIFA23.exe's OriginSDK deserializers (authoritative): <Login UserIndex="0" IsLoggedIn="true" LoginReasonCode="0"/> (LoginT, +0x287a30) <OnlineStatusEvent isOnline="true"/> (OnlineStatusEventT, +0x28a4d0) Value encoding is the true/false string literal (FIFA's bool parser at FIFA23.exe+0x28fb50 compares to "false"); isOnline="1" is parsed as not-online. Result (verified with in-process probes): FIFA parses both events every cycle and returns success, and stops its impatient GetProfile polling / sets INGAME presence — the first time FIFA has been driven "online" at the EbisuSDK level (anadius, being offline DRM, never does this). But FIFA does not proceed to the GetAuthCode→Nucleus →Blaze chain; the gate is its game-side event consumer (virtual dispatch at FIFA23.exe+0x274d4e2), not delivery. Full trail in docs/connection-gate-findings.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+87
-14
@@ -13,6 +13,48 @@ use tracing::{debug, info, warn};
|
||||
const GREETING_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb";
|
||||
const AES_KEY: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
|
||||
/// The async `ONLINE_STATUS_EVENT` push FIFA waits for after `GoOnline`.
|
||||
///
|
||||
/// FIFA calls GoOnline (handled in-process by EbisuSDK/anadius), gets success,
|
||||
/// then blocks waiting for an unsolicited `OnlineStatusEvent` push on the LSX
|
||||
/// socket — the M2 wall. Now that our bridge owns FIFA's LSX socket we can send
|
||||
/// it. Format RE'd from FIFA23.exe's OriginSDK deserializer (authoritative):
|
||||
/// element `OnlineStatusEvent` (RTTI `Origin::EventHandler<lsx::OnlineStatusEventT,
|
||||
/// bool>`, parser FIFA23.exe+0x274d4ae), single bool attribute `isOnline`
|
||||
/// (deserializer FIFA23.exe+0x28a4d0). See docs/connection-gate-findings.md.
|
||||
///
|
||||
/// The value is the string literal `true`/`false` (not `1`/`0`): FIFA's bool
|
||||
/// parser (FIFA23.exe+0x28fb50) string-compares the attribute against `"false"`,
|
||||
/// matching the OriginSDK convention every other bool in our LSX responses uses.
|
||||
/// An initial `isOnline="1"` push was delivered but parsed as not-online (FIFA
|
||||
/// kept polling), which pinned the encoding. Re-push cadence below still
|
||||
/// TODO/CONFIRM (single push may suffice once value is correct).
|
||||
const ONLINE_STATUS_EVENT: &str =
|
||||
r#"<LSX><Event sender="EbisuSDK"><OnlineStatusEvent isOnline="true"/></Event></LSX>"#;
|
||||
|
||||
/// The authenticated-session `Login` event. OnlineStatusEvent alone set FIFA's
|
||||
/// *connectivity* (presence went INGAME) but not an authenticated session, so FIFA
|
||||
/// never started the GetAuthCode→Nucleus→Blaze chain. FIFA's Origin event set has a
|
||||
/// separate `Login` event (`Origin::EventHandler<lsx::LoginT,OriginLoginT>`) that
|
||||
/// carries the login state. Format RE'd from FIFA23.exe's LoginT deserializer
|
||||
/// (0x142787a30): fields `UserIndex` (int, primary user 0), `IsLoggedIn` (bool,
|
||||
/// parsed by the same true/false helper as isOnline), `LoginReasonCode` (int/enum).
|
||||
///
|
||||
/// Pushed BEFORE OnlineStatusEvent each cycle (log in, then go online).
|
||||
/// TODO/CONFIRM `LoginReasonCode` — 0 is the natural "no error / normal" value; if
|
||||
/// FIFA switches on a specific success code, RE the consumer and adjust.
|
||||
const LOGIN_EVENT: &str =
|
||||
r#"<LSX><Event sender="EbisuSDK"><Login UserIndex="0" IsLoggedIn="true" LoginReasonCode="0"/></Event></LSX>"#;
|
||||
|
||||
/// Events pushed on the LSX socket each interval, in order, to drive FIFA online.
|
||||
const ONLINE_PUSH_EVENTS: &[&str] = &[LOGIN_EVENT, ONLINE_STATUS_EVENT];
|
||||
|
||||
/// Delay before the first `OnlineStatusEvent` push (and the re-push interval).
|
||||
/// Long enough that FIFA has finished LSX bootstrap and subscribed its event
|
||||
/// listeners + issued GoOnline (GetInternetConnectedState was seen ~2s in), short
|
||||
/// enough to fire while FIFA is still polling and waiting.
|
||||
const ONLINE_PUSH_INTERVAL: std::time::Duration = std::time::Duration::from_millis(2000);
|
||||
|
||||
// Account identity + locale that anadius's in-process LSX emu reports (recovered
|
||||
// from anadius.cfg — the config the emu reads). FIFA carries these values into
|
||||
// its online/loading state, so our bridge must report the SAME ones anadius does;
|
||||
@@ -92,22 +134,53 @@ async fn handle(mut stream: TcpStream) -> anyhow::Result<()> {
|
||||
stream.write_all(accepted.as_bytes()).await?;
|
||||
info!("LSX: handshake complete");
|
||||
|
||||
// 4. Session loop
|
||||
// 4. Session loop — multiplexes request handling with an unsolicited
|
||||
// OnlineStatusEvent push. A single task keeps owning the stream; the
|
||||
// `select!` services reads and lets a timer fire the push, so no socket
|
||||
// split / shared writer is needed.
|
||||
let mut push_timer = tokio::time::interval(ONLINE_PUSH_INTERVAL);
|
||||
// interval fires immediately on the first tick — consume it so the first
|
||||
// push lands after ONLINE_PUSH_INTERVAL (giving FIFA time to subscribe).
|
||||
push_timer.tick().await;
|
||||
loop {
|
||||
let raw = match read_msg(&mut stream).await {
|
||||
Ok(b) => b,
|
||||
Err(_) => break,
|
||||
};
|
||||
// Dump the exact wire bytes so the session seed can be brute-forced
|
||||
// offline against this ciphertext (see docs/connection-gate-findings.md).
|
||||
debug!("LSX: session raw ({} bytes): {}", raw.len(), bytes_to_hex(&raw));
|
||||
tokio::select! {
|
||||
read = read_msg(&mut stream) => {
|
||||
let raw = match read {
|
||||
Ok(b) => b,
|
||||
Err(_) => break,
|
||||
};
|
||||
// Dump the exact wire bytes so the session seed can be brute-forced
|
||||
// offline against this ciphertext (see docs/connection-gate-findings.md).
|
||||
debug!("LSX: session raw ({} bytes): {}", raw.len(), bytes_to_hex(&raw));
|
||||
|
||||
let out = process_frame(&raw, seed);
|
||||
if out.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if stream.write_all(&out).await.is_err() {
|
||||
break;
|
||||
let out = process_frame(&raw, seed);
|
||||
if out.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if stream.write_all(&out).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ = push_timer.tick() => {
|
||||
// Push the Login (authenticated session) then OnlineStatusEvent
|
||||
// (connectivity) so FIFA leaves the "connecting" wait and starts the
|
||||
// GetAuthCode→Nucleus→Blaze chain. Each is encrypted + null-terminated
|
||||
// like any session frame. Re-pushed each interval to cover the
|
||||
// subscribe-timing race until FIFA advances (or the socket closes).
|
||||
let mut push_err = false;
|
||||
for ev in ONLINE_PUSH_EVENTS {
|
||||
let mut frame = lsx_encrypt(ev, seed).into_bytes();
|
||||
frame.push(0);
|
||||
debug!("LSX: pushing event: {ev}");
|
||||
if stream.write_all(&frame).await.is_err() {
|
||||
push_err = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if push_err {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user