Behavior-preserving cargo fmt of two FIFA17 diagnostic modules that carried
long unwrapped macro-invocation / argument lines predating this work. Split
out of the FIFA23-retirement commit to keep that focused. No logic change.
The fifa17 hook path installed no connection redirect (only a module-map dump),
so FIFA17 relied entirely on Linux iptables DNAT / hosts (and had no Windows
equivalent). Add an in-process, config-driven redirect for the fifa17 build:
- openfut-common gains ResolvedServer::redirect_for_ea_port(): the single shared
decision (EA source-port signature -> configured OpenFUT host+port, network
byte order), reused by the hook so it and openfut.cfg agree by construction.
Covers 443->https, 10041/42230->blaze_redirector, 42127->blaze_main.
- connect_hook: redirect_if_ea now dispatches to a config-driven rewrite when
armed (rewrites to the CONFIGURED, possibly remote, server -- not hardcoded
127.0.0.1), matching by EA source port so a hardcoded EA IP (159.153.51.20
redirector) and a DNS-resolved one both land on the server. Legacy loopback
path retained only for the not-yet-retired FIFA23 build.
- fifa17::install() reads openfut.cfg next to FIFA17.exe via openfut-common and
installs connect + WSAConnect (IAT) + ConnectEx (shared redirect). Fail-safe:
missing/invalid config installs NO redirect (traffic untouched), never a
corrupt sockaddr.
Tests: openfut-common redirect map/sockaddr/endian/remote-host/unknown-port.
Cross-builds x86_64-pc-windows-gnu --features fifa17; clippy -D warnings clean.
No Linux fallback removed (migration gate).
The prior Windows branch treated BOTH companions as in-process and started
neither. That is wrong for LSX: FIFA dials the Origin/LSX emulator on
127.0.0.1:4216 and it must run locally on the client (the STEAMPUNKS
stp-origin_emu.dll is the crack's activation emu, not OpenFUT's LSX). Only
autopatch is genuinely in-process on Windows (its ProtoSSL cert patch is done by
the version.dll hook), so skip just that one and spawn LSX through the normal
path. Also resolve the companion as openfut-lsx.exe on Windows.
The 60fps cap still left 16ms gaps with no present; on windowed G-Sync/FreeSync
DWM keeps moving the window in and out of the VRR path across those gaps and the
refresh rate swings, which the panel shows as flicker. Render continuously
(request_repaint every frame) with vsync on so the window stays continuously in
VRR at the display's own variable refresh.
The idle repaint was 500ms (~2fps), below the G-Sync/FreeSync VRR floor, so the
panel ran low-framerate compensation and every hover/animation spiked then
dropped the rate — the swinging refresh rate makes VRR displays flicker. Present
at a constant ~60fps (16ms) instead so VRR locks to one rate. Cheap for a UI
this small; vsync keeps present times regular.
Port the egui launcher to run natively on Windows (no Wine/Proton). The GUI,
launch state machine, config, health/account monitors, and openfut.cfg writing
are unchanged and cross-platform; only the effect layer is branched:
- game_launch: cfg(windows) launch spawns the game executable directly with its
working dir (the version.dll hijack loads from the game dir; no WINEDLLOVERRIDES,
Wine prefix, or licence regen). Requires the launcher to run elevated so the
child inherits admin. Linux Proton path gated cfg(unix).
- arm: cfg(windows) is a no-op (routing is openfut.cfg, written by the client-files
step; no ptrace_scope/DNAT/hosts). Linux arming gated cfg(unix).
- local_services: on Windows LSX/autopatch are in-process (stp-origin_emu.dll +
version.dll hook), so ensure_running reports ready without spawning. Gated the
unix-only CommandExt/process_group.
- preflight: cfg(windows) run() keeps only backend-reachable + hook-config checks.
- config: GameProfile configured()/validate() accept a runner-less Windows profile.
theme: fix a latent cross-platform panic — egui 0.29 keeps a Style per theme, so
set_style only reached the active one and TextStyle::resolve("Hero") panicked when
the other theme rendered. Install the full style into both themes and pin Dark.
Cross-built for x86_64-pc-windows-gnu; Linux build + 75 tests unchanged.
- Passive: log FUN_18017ff90 param_1 = the pack-names/cards-tournament-list
WEBFILE_DL url (via relocating installer; rip-relative MOV R8,[DAT_1802e6580]).
- Guarded one-shot (staging client only): in the final completion FUN_1800ffe90,
when the delivered result string is CACHE_PACKNAMES_FAILED, rewrite result byte0
so it delivers SUCCESS -> LoadSeasons_Complete advances to LoadCurrentOfflineSeason.
String-verified, once per process.
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).
Adds openfut-hook/src/season_trace.rs: read-only CardsDLL detours that log the
FIFA17 FUT offline-season entry native call sequence (no behavior change; each
wrapper logs then calls the original via a trampoline). Traces the FUT_Season
natives proven by the registration table FUN_18004e3f0:
GetUsersOfflineDivision 0x4eb50 (NOT LoadOfflineSeasons),
LoadOfflineSeasons 0x4ee10 + async impl 0x57560,
LoadCurrentOfflineSeason 0x4eb70 + impl 0x57230 + completion 0x578e0,
StartSeason 0x4f340, GetOfflineSeasonInfo 0x4e850.
Includes a near-trampoline installer (install_detour_reloc) that relocates a
single rip-relative disp32 so functions with rip-relative prologues can be
detoured (trampoline allocated within +/-1.5GiB of CardsDLL).
The rebind approach was disproven live: the bind sensor measured mask=0x00 at
screen-show (container empty, all six slots hidden -> no tab bar), and a rebind
after the groups arrived (mask=0x0e = bronze|silver|gold) built NO tab bar. The
Scaleform movie only honours the framework's OWN bind at screen-show, not a later
re-publish/commit.
Root cause therefore stands confirmed: the store's GET store/purchasegroup/all
returns only after screen-show, so the first bind sees an empty container. Re-entry
works because the groups are cached by then.
Fix: load the purchase groups BEFORE the store screen is shown. FUN_180017870
(storefront) issues the store's own group request; firing it from the FUT hub event
pump (a real game thread, before the store screen exists) lets the response arrive
and populate the container so the first screen-show bind sees a full list and binds
the tabs natively -- the re-entry path, on first entry.
The bind detour is retained purely as the read-only SENSOR: the first-entry bind
mask is the definitive measurement of whether the pre-warm landed in time. mask!=0
=> pre-warm worked and the tabs bind natively; mask==0 with storefront_seen!=0 in
the pre-warm log => a hub-time request cannot land in time and the remaining route
is the extracted StoreFront.apt.
Removed: render detour, maybe_rebind/should_rebind, and all rebind state. Re-added
the hub-time maybe_prewarm_groups() call in sbc_dispatch::event_wrapper.
Promoted (build-armed). Deployed artifact 668e9324; profile-gated fifa17 build.
Replaces three disproven store-entry mechanisms (category clamp, late
*_CATEGORY_ID publish, purchase-group pre-warm) with the one repair the
reversing actually supports.
FUN_18007e5e0(ctx, panel) is the native tab binder the screen framework
invokes at screen-show. It is an unrolled six-slot loop; each slot gates on
one hard-coded category token and either publishes that group's id as
PANEL_ID for the slot or hides the slot:
slot 0 mypacks, 1 bronze, 2 silver, 3 gold, 4 special,
slot 5 points (extra gate: (*(store_vtbl+0x30))(store) must be false)
The gate FUN_180014df0(_, idx) resolves the token through FUN_180014380,
which linearly scans the loaded purchase groups (stride 0x108) comparing the
token at group+0x70. So a tab exists iff a purchase group carrying that
token is loaded AT BIND TIME. Our server emits mypacks/bronze/silver/gold
as displayGroup.value, so four tabs are expected.
On a cold session the store screen shows before its own
GET store/purchasegroup/all response arrives: every gate fails, all six
slots take the hide path, and the binder is never invoked again for that
screen. Re-entry works only because the groups are cached by then -- which
is exactly the reported symptom.
The repair re-invokes the binder once, with the framework's own (ctx, panel),
at the first render after the groups arrive, reproducing the re-entry
ordering on the first entry. Repeating the binder is safe: it only publishes
PANEL_ID or hides per slot, reads the group list from a process singleton,
and finishes by tail-calling panel->vtbl[0xd0](panel, true) -- the provider
commit that rebuilds the movie's bar.
Fail-closed: rebind only when the framework's bind observed an EMPTY mask and
at least one token now resolves (a store that already bound tabs is never
touched); one rebind per bind generation, claimed by compare-exchange; only
framework-supplied pointers are ever used; image plus all three function
signatures verified before any write and re-verified under thread suspension.
The gate probe passes a null this, which is sound because FUN_180014df0
forwards rcx to FUN_180014380, which discards it and uses a singleton.
Why the earlier attempts could not work: the clamp forced a single category
(regressing Browse Packs to bronze-only), the publish targeted FUN_18007df60
which does not bind panels, and the pre-warm ran from the FUT event
dispatcher -- after screen-show, so the slot decisions were already made.
Promoted (build-armed, no env var). Rollback is a version.dll file swap.
Fixes the ordering instead of fighting the movie. The tab bar is bound by
FUN_18007e5e0 (six caption tests -> PANEL_ID, else hide panel), which is
slot 0 of a secondary vtable invoked by the screen framework at
screen-show. On a cold session the /store/purchasegroup groups have not
arrived by then, so all six panels hide and no tab bar is drawn. Late
publishing does not fix it: deploying the 0x278a publish at render time
fired with its gate accepting (log: tabpublish=1 state=0x418) and the bar
still did not appear, i.e. the movie ignores late tab updates.
So load the groups BEFORE the store is ever opened. The store screen
issues its own pack-list request at 0x18007f25e as
FUN_180017870(*(base+0x2de0d0)) -- a single-argument call on the
storefront global. Issue exactly that call once per process from the FUT
event dispatcher, which already runs on a game thread long before the
store screen exists. When the user then opens the store, the native
screen-show bind sees a populated group list and binds the tabs itself --
the same reason a second entry has always worked.
Fail-closed: base + CardsDLL image validated, storefront read guarded,
FUN_180017870 fingerprinted before the first call, one request per
process claimed before issuing (no re-entrant double request), and
skipped entirely once groups exist. Logs prewarm= for evidence.
fmt/clippy -D warnings clean, 32 hook tests pass.
The category clamp fixed WHICH content the first store render draws, but
the tab bar was still missing on first entry (operator-observed: first
open = bronze packs with no tab bar; re-entry = same packs WITH
Bronze/Silver/Gold tabs).
Root cause: the store screen dispatcher 0x18007d880 publishes the tab bar
on a DIFFERENT event than it renders. Event 0x278a -> FUN_18007df60
resolves the six hardcoded tab tokens against the loaded purchase groups
and publishes *_CATEGORY_ID; event 0x753f -> FUN_18007dab0 renders. On
first entry the publish runs before /store/purchasegroup has landed, so
all six tokens resolve -1, every panel hides, and no tab bar is drawn;
re-entry only works because the groups are cached by then.
Re-run the native publish once per screen from the render detour, where
the groups are provably present (the ordinal-1 lookup already proves it),
reproducing the working re-entry order (publish, then render). Safe: it
is the same call the dispatcher makes with the same single argument, it
self-gates on screen+0x2cc == 0x418 (a mismatch is a native no-op, not a
fault), it is fingerprinted before the first call, and it runs at most
once per screen instance. Also logs the gate state so a no-op publish is
diagnosable. fmt/clippy -D warnings clean, 29 hook tests pass.
The FUT store flashes a Browse-Packs overview on first open: the store
screen ctor leaves screen+0x290 (CATEGORY_ID) at 0, and the resolver
FUN_1800147f0 treats 0 as list-all, so the first render draws the group
overview before the movie posts a tab ordinal.
Detour the store render FUN_18007dab0 (RVA 0x7dab0): when the incoming
category is 0, substitute the first present group ordinal (1) so the
first frame lands on a real tab. Provably crash-safe: it writes 1 only
after FUN_180014420(_, 1) (the resolver's own ordinal->group lookup,
whose first arg is dead) returns non-NULL, which is exactly the
resolver's non-crash precondition; the positive-invalid NULL deref at
0x14882 is thus unreachable. No group yet -> category left 0 -> Browse,
still safe.
Promoted like the SBC dispatch: build-armed (CLAMP_PROMOTED), no env.
Signature-gated on both the detoured render and the called lookup,
image-validated, installed under thread suspension, fail-closed. Only
the overview flash is addressed; the empty-My-Packs entry dialog is
movie-side (packed .apt) and out of CardsDLL reach (see Vault
Store Resolver Guard 2026-08-19). fmt/clippy -D warnings clean both
feature sets, 26 hook tests pass, x86_64-pc-windows-gnu release builds.
The dashboard named OpenFUT internals at a player: "Client integration", "Local
services", "Hook DLL", and a "Deployed -> 10.10.0.120" value that conflates a DLL
with a server address. None of it tells someone who just wants to play whether they
can press Play.
Rows are now Game files / Background helpers / Game patch, and the patch row states
Installed rather than echoing the host it will point FIFA at.
The important fix is the helper state. Two of the three cases returned labels that
sound like faults for what is the NORMAL idle condition - the helpers only run
alongside a session, and Launch starts whatever is missing - with "Partly running"
being the worst: it reads broken and offers nothing to act on. Both collapse to
"Start with the game", which says what will happen. Observed live: the dashboard
showed "Partly running" while genuinely healthy, and pressing Launch brought
autopatch up on its own.
No behaviour change: readiness values are untouched, so the overall verdict pill and
the launch gating are identical. fmt, clippy -D warnings, 75 tests clean.
Wine ignores the game-directory version.dll proxy unless an override names it.
WINEDLLOVERRIDES covers only a process the launcher spawns itself, so the documented
fallback was to have the user paste Steam launch options by hand - a step a normal
player cannot be expected to perform, and the reason the game had to be started
through a specific wrapper at all.
The launcher now persists version=native,builtin into the prefix registry via Wine
own reg tool before launching (ensure_dll_override). It is /f-idempotent, so it runs
on every launch and repairs a prefix the player has reset or replaced, and it applies
to EVERY launch path including Steam Play. This mirrors what BepInEx documents for
Proton (configure the proxy in winecfg rather than the environment) and what Proton
already does in this prefix for other titles. Best-effort: a failure is reported in
plain language and the launch still carries WINEDLLOVERRIDES.
STEAM_LAUNCH_OPTIONS is demoted to a fallback for prefixes we have never prepared.
Also fixes a pre-existing clippy manual_is_multiple_of in app.rs that was failing the
strict lint gate. fmt clean, clippy -D warnings clean, 75 tests pass.
Retail Gates A-G passed on the pinned CardsDLL build (4706a881), and the repair
has been live-proven repeatedly, so it is now a promoted feature. Arming it from
OPENFUT_SBC_DISPATCH meant any launch that did not export it (Steam, the launcher
Launch button, a bare umu-run) silently lost the SBC screen to the known
response-to-deserializer dispatch defect, leaving a harness script as the only
working entry point.
REPAIR_PROMOTED is now a build constant with a compile-time contract, and both
install sites derive from it: sbc_dispatch::install always arms, and
sbc_trace::install derives the parser/notifier/controller-registration traces from
it because those traces ARE the repair decision inputs, not optional diagnostics.
Promotion weakens no check. Safety stays in the runtime evidence gate rather than a
flag: the worker still validates the exact CardsDLL signatures before installing a
detour, and decide() still requires the transport sentinel status, the pinned
category-response vtable captured while the response object was provably live,
balanced parser counts on the one parser thread, this generation notifier having
entered AND returned, the captured controller/model identity, and one repair per
deserializer generation. An unrecognised build leaves native execution untouched.
Rollback is a file swap (restore the previous version.dll via the hook harness
backup), the documented client rollback path, deliberately not an env kill-switch.
fmt clean, strict clippy clean on default and fifa17 features, 22 tests pass,
release cross-build to x86_64-pc-windows-gnu produces artifact 3641d581.
The launcher shelled out to `python3 lsx_responder_v2.py` and `python3 autopatch.py`
from a configured tools directory. Both are now Rust binaries built from this
workspace (openfut-lsx, openfut-autopatch), so the launch contract loses the
interpreter and the script directory entirely: nothing to locate, nothing to
configure, and no way to run a stale checkout's copy of a responder.
Service::script() becomes Service::binary(), and resolve_binary() prefers a sibling
of the running launcher -- what a workspace build and any sane install layout both
produce -- falling back to the bare name so a PATH install still works. It returns the
bare name rather than failing so that spawn() stays the single place a missing binary
is reported, instead of two error paths for one condition.
foreign_pid() now matches an argv entry's FILE NAME rather than a suffix, so
`/path/to/openfut-lsx` matches while an unrelated argument that merely ends with the
same text does not. It deliberately still reads argv and not comm: comm is truncated
to 15 characters by the kernel, which would misreport both of these names -- the same
trap that made an earlier `pgrep -f` guard match its own shell.
Dead configuration removed rather than left vestigial: fifa17_python and
fifa17_tools_dir, their Settings controls, and validate_local_services(), whose only
two checks were those fields. A validation hook that can only return Ok(()) would
claim the launcher verifies local-service configuration when there is none. The
preflight tools-dir gate is gone too, while the ptrace_scope check it gated is kept --
that check is real and repairable via "Arm client"; only the gate died.
The env contract is unchanged, so the binaries are drop-in: LSX still receives
FUT_PERSONA_ID/FUT_PERSONA_NAME (the persona has to agree with Blaze's
LoginResponse.SESS.PDTL and UTAS's userInfo.personaId), autopatch still receives
OPENFUT_AUTOPATCH_LOG under XDG_RUNTIME_DIR and --launcher-pid so it cannot outlive
its owner, and each companion still gets its own process group.
74 tests green.
The hook ships as a `version.dll` proxy in the game directory, and Proton prefers a
local DLL over its builtin ONLY when WINEDLLOVERRIDES names it -- exactly what
setup::STEAM_LAUNCH_OPTIONS documents ("version=n,b"). Steam users get it from their
launch options. When the launcher spawns the runner itself it applied profile.env and
WINEPREFIX but never the override, so the hook silently did not load.
The failure mode is worse than "hook missing", which is why it went unnoticed: with
no hook there is no port rewrite, so the openfut.cfg the launcher writes two seconds
earlier is inert and the game falls back to EA's real Blaze ports, where /etc/hosts
(10.10.0.120 easw.easports.com) quietly routes it to whatever answers there. The
launch looks completely healthy -- correct log lines, game boots, FUT loads -- while
talking to a different server than the one configured. Production only works here by
accident of the hosts file.
Observed end-to-end: openfut.cfg written 19:50:00 with blaze 42327/42330, FIFA started
19:50:03, and the process was ESTAB to 10.10.0.120:42130 -- production.
/proc/<pid>/maps showed the mapped version.dll was Proton's own
(compatibilitytools.d/UMU-Proton-10.0-4/files/lib/wine/x86_64-windows/version.dll),
not the game-dir hook, and no hook log existed for the Proton prefix at all.
launch() now always sets WINEDLLOVERRIDES, appending to any profile value and
deferring to a profile that pins `version=` itself, so an operator disabling the
hijack on purpose is not silently overruled. Four tests cover absent, unrelated,
explicit and blank-string cases.
Also worth noting for future debugging: ~/.wine/drive_c/openfut_hook.log is stale and
belongs to a non-Proton prefix. It is not evidence about a umu/Proton launch, and
reading it as current is how this was nearly misdiagnosed.
The launcher used to make the user perform OpenFUT's internal launch order by
hand — Start LSX, Start autopatch, Run pre-launch checks, "Arm client", then a
button called *Start Services & Launch Game*. Those are implementation details
of how FIFA 17 is persuaded to talk to OpenFUT, and getting the order wrong
produced failures that surfaced much later as "the game crashed": autopatch
started before ptrace_scope is 0 silently patches nothing at all.
The normal flow is now: open the launcher, read one status card, press
**Launch FIFA 17**.
New `launch` module holds the sequence as a state machine (Phase: Idle,
Checking, PreparingClient, StartingServices, Validating, Launching, Running,
Failed) and runs it on a worker thread, so the UI thread never blocks on a
socket, a Polkit prompt or a process spawn. The UI renders that state; it does
not coordinate services.
Every step asks what is already true before acting:
- a healthy service is reused, never restarted;
- client preparation is skipped when the checks it would repair already pass,
which also avoids a pointless password prompt;
- the hook config is reconciled from the current settings.
It stops at the first failed step and never starts FIFA into a client it knows
is broken. Preparation deliberately runs BEFORE autopatch, against the order in
the brief, because autopatch cannot write FIFA's memory until arming has set
ptrace_scope and would otherwise "succeed" while doing nothing.
Ownership is now tracked, which the old model could not express: it only knew
about children it had spawned, so a service started by hand for a debugging
session read as "stopped" and starting it again just collided on the port.
`ServiceSupervisor` observes our own child first, then scans /proc for a foreign
instance, and reports `ServiceRuntime { running, started_by_launcher, pid,
detail }`. `stop_permitted` refuses to kill anything the launcher did not start,
under any cleanup policy. `CleanupPolicy` states the shipped behaviour — leave
launcher-started services running for the next launch — instead of leaving it to
chance, and the FIFA-exit path goes through it.
Readiness comes from observation, never from a button press: LSX is ready only
when the port FIFA dials is actually held, and "we have not looked" renders as
"Not checked yet", never as green.
Manual controls all survive under **Advanced / Diagnostics** — per-service
start/stop/restart with PIDs and ownership, "Prepare client" (the old "Arm
client", renamed; internals still say arm), "Run pre-launch checks", "View
logs", and a new "Launch game only" escape hatch for debugging a launch the
sequence refuses.
Tests: 73 pass (15 new). Sequencing and ownership are unit-tested through a
`LaunchOps` fake, so "don't launch after a failed step", "don't restart healthy
services" and "don't kill what we didn't start" hold without a FIFA install, a
Polkit agent or root.
Exercised live under Xvfb: the card shows four observed rows and one button; a
launch stopped at LSX with "127.0.0.1:4216 is held by an unrelated process",
listed every step's verdict, and did NOT start the game; Advanced showed a real
pre-existing autopatch as "Running (foreign) · pid 382382 · started outside this
launcher" with Stop/Restart disabled.
It counted Pass/Warn/Fail across the whole preflight run, and `backend_reachable`
opens real sockets — so the aggregate silently asserted that the machine running
the suite has the OpenFUT blaze-redirector and account ports open. True on the
server host, false everywhere else, including the game machine where anyone
building the launcher would run it. Predates this branch; found by running the
suite on .105 instead of only here. Now asserts the hostname check itself, which
is what the test is named for.
The new hook-config check warns when the deployed openfut.cfg disagrees with
the configured server, and this test counts warnings across every check. On the
game machine — which by definition has a hook deployed — that second warning
broke the assertion. Caught by running the suite on .105 rather than only on
the server host. Pins the game dir for the same reason the tools dir is pinned.
Release-readiness pass on the launcher, driven by the end state "open it,
create an account, launch the game".
Fixes a silent correctness bug. `openfut.cfg` in the game dir is the only
server address the *game* can see, but it was written only by Setup's deploy
and its "Save & Update hook" button. Changing the server anywhere else left
FIFA connecting to the previous host while every panel in the launcher showed
the new one online. Now:
- `write_hook_config` reconciles the file from the live config, and runs
fail-closed before every launch, so the file and the UI cannot disagree at
the moment it matters;
- saving Settings pushes the address into the hook immediately;
- a `hook_config` preflight check reads the file back and warns, naming both
addresses, instead of leaving the drift invisible;
- Settings shows the same fact inline, and Save is enabled by drift alone —
a message saying "Save to update it" beside a disabled button is a dead end.
Account creation is now server-authoritative. `account_sync::discover` POSTs
`/openfut/account/sync` with the persona fields *omitted*, which makes the host
answer with the persona it was started with, its club, and the Core coin
balance. The launcher adopts that answer, so it never invents an identity and
the persona the game authenticates with is by construction the one the server
expects. Claiming is gated on the address being valid, NOT on the health pill:
that pill probes the HTTPS port while this talks to the account port, so gating
on it disabled the button on servers that answer it perfectly well.
UX consolidation:
- new Welcome ("Get started") tab: three numbered steps — connect, claim an
account, connect FIFA — each showing live state, ending in the launch CTA;
a fresh install opens on it and it leaves the nav rail once satisfied;
- Config renamed Settings, and made the single owner of the server address:
Setup's duplicate editors (same fields, different save semantics) are now a
read-only summary with actions;
- the dashboard offers account creation in place instead of naming a tab, and
the stale "set the host in the Setup tab" pointers are corrected.
Locks move to parking_lot per project rule (already the convention in
openfut-utas-host and openfut-identity); 47 poisoning unwraps go away.
Verified: 58 tests pass, fmt clean, clippy clean apart from one pre-existing
lint. Driven through the real UI under Xvfb as a fresh install — typed a server,
clicked Create my account, and the config on disk came back with persona
33068179/CAGE claimed from the live host; clicking Save rewrote a stale
`openfut.cfg` from host=10.10.0.99 to host=127.0.0.1.
Modernize manual nul-terminated byte strings to C-string literals (c"...")
at all Win32 GetModuleHandleA/GetProcAddress/getaddrinfo call sites (byte-identical),
drop two redundant SOL_SOCKET-as-i32 casts, remove a needless return in the fifa17
install path, and add a # Safety section to DllMain. Scope the FIFA-23-path
dead-code/unused-import lints (unused only under the fifa17 feature, stripped by the
linker) with a documented crate-level cfg_attr allow. Cross-verified: both the
default and fifa17 builds now pass clippy -D warnings and compile; probe and
capture_baseline still build.
openfut-hook is a Windows version.dll proxy injected into the FIFA client, but
as a member of the parent OpenFUT workspace its [profile.release] was silently
ignored (cargo only honors profiles at the workspace root, and forbids per-package
`panic` overrides). The shipped DLL was therefore built opt-level=3 /
strip=debuginfo / panic=UNWIND -- and unwinding a Rust panic across the
DllMain/FFI boundary into the game process is UB.
Add an empty [workspace] table so the crate is its own root and its release
profile (panic=abort, strip=symbols, opt-level=s) applies. Paired with the
parent workspace `exclude`. Also lands the artifact in openfut-hook/target/
(matching the launcher config.rs default hook_dll_path) instead of the parent
target/. Cross-build verified: panic=abort now emitted; DLL 1200126 -> 861696 B.
Reconcile the two divergent launcher lineages that share merge base 87241ac:
- feat/launcher-arming (13339c1): one-click client arming, modular
preflight/services, and the FIFA 17 verified patched-client capability
reporting (fifa17_capability + local_services stdout parsing + backend
registration).
- feat/sbc-hook-tracing (958ff24): openfut-hook SBC request tracing / RE
instrumentation (sbc_hook, sbc_trace, probe, transport_watch, ...).
The lineages are almost disjoint (launcher crate vs openfut-hook crate); the
only overlap was src/process.rs, which launcher-arming removed (functionality
moved into local_services/game_launch, `mod process` dropped from main.rs) and
sbc-hook-tracing incidentally tidied (map_err->inspect_err). Resolved by keeping
the file DELETED: it is an orphan module in the refactored launcher and is not
part of the SBC feature (which lives entirely in openfut-hook). Both features are
retained in full.
Launcher side of the verified patched-client capability handshake. When
autopatch proves the CardsDLL empty-My-Packs resolver guard is active for the
CURRENT FIFA process, the launcher advertises that to the backend so the backend
may drop the synthetic 65534 sentinel for that session only. Additive and
fail-closed: any parse/registration failure leaves the backend on its default
sentinel path.
- New src/fifa17_capability.rs:
* Fifa17ClientCapabilities { empty_mypacks_resolver: Option<u32> } — per-FIFA-
process state, UNKNOWN at each launch, discarded when that process ends
(never persisted, so a prior launch's capability cannot leak).
* parse_capability_line() / parse_fifa_pid() — pure parsers for autopatch's
stdout token `[store-guard] verified capability fifa17.empty_mypacks_resolver=<v>
fifa_pid=<pid>`; the non-advertising `guard status=...` line yields None.
* register() — tiny stdlib-HTTP POST /openfut/fifa17/capability, modeled on
account_sync::sync (Connection: close, 3s timeouts, 2xx check).
- local_services::spawn: autopatch stdout reader parses each raw line; on the
first verified line it sets the shared capability sink, logs, and fires exactly
one backend register() for this FIFA process. Capability wiring is bundled in a
CapabilityWiring struct (Some for autopatch, None for LSX). LSX unchanged.
- app.rs: LauncherApp holds the shared Fifa17ClientCapabilities; it is reset to
UNKNOWN at the start of launch_game (and when autopatch is stopped) so a new
FIFA process never inherits a previous launch's capability.
- Tests: parse (verified/non-advertising/unrelated/version-2) + a register()
round-trip against an in-process listener.
Design + contract: docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md (superproject).
Pre-existing openfut-hook/* working-tree changes are intentionally left uncommitted.
Add a GUI "Arm client" button that reproduces client_arm.sh in a single
pkexec batch: kernel.yama.ptrace_scope=0, DNAT of EA's hardcoded redirector
IP to the OpenFUT server (+ MASQUERADE reply path), and /etc/hosts rewrites
for every dead EA hostname (removing foreign shadow lines first, so glibc's
first-match resolution can't land on a stale loopback entry). All steps are
idempotent (delete-then-add) and injection-safe: config values are charset-
validated and rejected on a surprising character, never shell-escaped. arm()
returns the concrete change list, which the button logs line-by-line and
echoes as an inline pass/fail status on the pre-launch tab (no tab jump, no
reuse of the local-services toast).
This necessarily lands the surrounding launcher modularization the arm
feature is built on, extracted from the former monolithic app.rs/process.rs:
- preflight: advisory pre-launch checks (ptrace, redirector DNAT, hostnames,
backend reachability) that colour rows but never block Launch
- local_services: launcher-owned LSX/autopatch child processes
- game_launch, account_sync, health, netcheck helpers
- openfut-common: dependency-free shared server-destination/port mapping,
used by both the launcher and (separately) openfut_hook.dll
openfut-hook RE changes are intentionally left uncommitted (separate concern).
fmt + clippy -D warnings clean; 46 tests pass.
- sbc_hook.rs: trace SBC submission/response flow with request IDs
- sbc_request_trace.rs: capture request/response bodies for analysis
- sbc_trace.rs: runtime trace buffer with structured logging
Work in progress - needs validation against live FIFA 17 client
In-process, read-only probes and transport observation built while closing
the online/FUT route from both the memory and network sides.
- probe.rs / dial_notification.rs: menu-time ctx dump, connMgr enumerator,
synthetic dial-notification + direct-call dial trigger, and the
[element+0x40] container write-watchpoint. All env-gated, one-shot,
VirtualQuery-guarded; none alter game state by default.
- transport_watch.rs + connect/connectex/hooks/lib: M0 transport observation
(grep-friendly TRANSPORT_WATCH logging on the existing getaddrinfo/connect/
WSAConnect/ConnectEx detours) and an IPv6 (v4-mapped) EA-redirect so the
game's IPv6 :443 dials land on the bridge instead of the dead servers.
Findings: the game never initiates a Blaze connection offline; the dial
handler is registered by a self-registering, message-driven state machine
whose container stays empty with no Blaze exchange. See openfut-bridge
docs/closure-and-preservation.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
install_force_connect() is no longer auto-called from install_probes_deferred,
so normal probe builds don't poke the online flow. Kept for reference; re-enable
the call to reproduce the 2026-07-02 forcing experiment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expands to 8 entry probes across the connect-state lifecycle (ctor, controller
accessor, four vtable steps) to distinguish entered-but-stalled from never-entered.
Result: ctor fires x4, everything else 0 — subsystem created but dormant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the anadius-connectivity probes with the game-side Nucleus-connect
functions (nucleusConnectREST/Trusted, connect-state tick) and adds a
VirtualQuery-guarded sampler thread that reads X=[0x14acd02c0] -> M=[X+0x360]
-> ctx=[M+0x778] once/sec to observe the session context directly. Per-slot
log cap prevents per-frame handlers flooding the log. Run 3 result: ctx is
non-null but the connect functions are never called (see bridge findings).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The OnlineStatusEventT::HandleMessage dispatch resolves its game-side
listener only at runtime (call [rax+0x28]). openfut_listener_stub patches
FIFA23.exe+0x274d4d7 to replicate the four dispatch instructions while
logging the resolved vtable/fn, then resumes. Alignment-safe (saves/rounds
rsp before the log call). Result: listener = FIFA23.exe+0x2751060 = ret 0,
a no-op default vtable slot -> the online->auth transition is state-polled,
not callback-driven.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hook-side tooling for the LSX/Blaze reverse-engineering effort:
- probe.rs (new, `probe` feature): passive logging detours on FIFA's online-flow
functions via the unhook/rehook pattern (no trampoline/relocation, works on
RIP-relative prologues). Deferred install waits for anadius64.dll to load, then
logs enter/return for GoOnline + GetInternetConnectedState (anadius) and the
OnlineStatusEvent/Login deserializers (FIFA23.exe). Revealed that our pushed LSX
events reach FIFA and parse OK, while GoOnline never fires — localizing the online
gate to FIFA's game-side event consumer.
- connect_hook.rs: redirect FIFA's LSX connect :3216 → :3217 so it lands on the
native openfut-bridge LSX server (slips past anadius's in-process :3216 intercept);
gated off under the `capture_baseline` feature.
- recv_hook.rs: boundary-safe trampolines + LSX peer filtering for the
capture_baseline path (log anadius's real LSX frames when the redirect is off).
Build the instrumented DLL with `--features probe` (or `--features capture_baseline`
for the anadius-baseline capture). Both features are off by default.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds connect_hook, connectex_hook, recv_hook, ssl_patch, tls_bypass, lsx, ea_stub,
and origin_spy modules to intercept EA's TLS and socket layers in addition to
getaddrinfo. Adds DLL-level logging to C:\openfut_hook.log for debugging. Also
patches windows-sys feature flags to include Cryptography and Threading APIs needed
by the new hooks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Step 2 in the Setup tab: grants cap_net_bind_service to the bridge
binary via pkexec/sudo so it can bind port 443 directly. On success,
automatically updates bridge_listen_addr to 0.0.0.0:443 — no iptables
rules required. Step re-detects cap state on each frame so it stays
accurate after a rebuild clears the capability.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The default hook_dll_path was constructed relative to Documents/OpenFUT/
but the launcher repo lives at Documents/openfut-launcher/, causing the
Setup tab to always show "DLL not built yet" even after building.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The DLL now reads openfut.cfg from its own directory on DLL_PROCESS_ATTACH
and uses the IP it contains as the redirect target instead of hardcoding
127.0.0.1. Falls back to 127.0.0.1 if the file is absent.
The launcher writes openfut.cfg alongside version.dll when deploying, and
the Setup tab exposes a "Redirect IP" field with an "Update" button that
rewrites openfut.cfg in-place without redeploying the DLL. Useful when
running the emulator on a different machine on the LAN.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds openfut-hook/, a Windows DLL (cdylib, x86_64-pc-windows-gnu) that
patches the IAT of FIFA 23 at load time to redirect getaddrinfo calls
for fut.ea.com / utas.*.fut.ea.com to 127.0.0.1, sending all FUT
traffic to the local bridge — no /etc/hosts changes needed.
Deployment: the launcher copies openfut_hook.dll into the FIFA 23 game
folder as version.dll (a DLL FIFA loads but delegates to system).
Proton picks up the local copy automatically when you set:
WINEDLLOVERRIDES="version=n,b" %command%
in Steam launch options.
Also updates cert install to try the Wine/Proton cert store (wine
certutil) before falling back to the Linux system CA store, and removes
all hosts file code from setup.rs / app.rs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GUI desktop app (egui/eframe) that manages the openfut-core and
openfut-bridge services with a single window.
- Dashboard: start/stop each service independently or together; live
status indicators; quick-status for cert and hosts
- Logs: real-time stdout/stderr from both processes, colour-coded by
level, follow mode and manual clear
- Setup: add/remove EA hostname redirects in /etc/hosts (pkexec/sudo);
install the bridge TLS cert into the system CA store
- Config: all binary paths and env vars editable in-app; persisted to
~/.config/openfut-launcher/config.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>