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.
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 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.
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.