3174fe4c1f
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.
119 lines
4.1 KiB
Rust
119 lines
4.1 KiB
Rust
mod account_monitor;
|
|
mod account_sync;
|
|
mod app;
|
|
mod arm;
|
|
mod config;
|
|
mod fifa17_capability;
|
|
mod game_launch;
|
|
mod health;
|
|
mod launch;
|
|
mod local_services;
|
|
mod logs;
|
|
mod netcheck;
|
|
mod preflight;
|
|
mod setup;
|
|
mod theme;
|
|
|
|
fn main() -> eframe::Result<()> {
|
|
let options = eframe::NativeOptions {
|
|
viewport: egui::ViewportBuilder::default()
|
|
.with_title("OpenFUT Launcher")
|
|
.with_app_id("openfut-launcher")
|
|
.with_icon(app_icon())
|
|
.with_inner_size([1040.0, 720.0])
|
|
.with_min_inner_size([880.0, 600.0]),
|
|
..Default::default()
|
|
};
|
|
|
|
eframe::run_native(
|
|
"OpenFUT Launcher",
|
|
options,
|
|
Box::new(|cc| Ok(Box::new(app::LauncherApp::new(cc)))),
|
|
)
|
|
}
|
|
|
|
/// The application / taskbar icon: the same "OF" monogram the header wordmark
|
|
/// shows, drawn white on the signature accent tile. Generated in code (no PNG
|
|
/// dependency) at 4x supersampling and box-downsampled to a crisp 64x64 RGBA —
|
|
/// scales cleanly to the 32x32 the WM typically renders. Colours come from the
|
|
/// theme palette so the icon never drifts from the in-app brand.
|
|
fn app_icon() -> egui::IconData {
|
|
const SIZE: usize = 64; // output edge
|
|
const SS: usize = 4; // supersampling factor
|
|
|
|
let accent = theme::ACCENT;
|
|
let fg = theme::ON_ACCENT;
|
|
|
|
// Rounded-square background: point inside the [0,SIZE]² square with corners
|
|
// rounded to `round_r` (transparent outside, so the icon reads as a tile).
|
|
let round_r = 13.0_f32;
|
|
let inside_bg = |x: f32, y: f32| -> bool {
|
|
let s = SIZE as f32;
|
|
let cx = x.clamp(round_r, s - round_r);
|
|
let cy = y.clamp(round_r, s - round_r);
|
|
let (dx, dy) = (x - cx, y - cy);
|
|
dx * dx + dy * dy <= round_r * round_r
|
|
};
|
|
|
|
// "O" — an elliptical ring on the left.
|
|
let inside_o = |x: f32, y: f32| -> bool {
|
|
let (cx, cy) = (21.0_f32, 32.0_f32);
|
|
let (dx, dy) = (x - cx, y - cy);
|
|
let outer = (dx / 9.0).powi(2) + (dy / 14.0).powi(2) <= 1.0;
|
|
let inner = (dx / 4.8).powi(2) + (dy / 9.5).powi(2) < 1.0;
|
|
outer && !inner
|
|
};
|
|
|
|
// "F" — a stem plus a top and middle bar on the right.
|
|
let inside_f = |x: f32, y: f32| -> bool {
|
|
let stem = (34.0..=39.0).contains(&x) && (18.0..=46.0).contains(&y);
|
|
let top = (34.0..=52.0).contains(&x) && (18.0..=23.0).contains(&y);
|
|
let mid = (34.0..=48.0).contains(&x) && (29.5..=34.0).contains(&y);
|
|
stem || top || mid
|
|
};
|
|
|
|
// Premultiplied-alpha accumulation per output pixel so antialiased edges
|
|
// (both the rounded tile and the letters) never fringe dark.
|
|
let mut rgba = vec![0u8; SIZE * SIZE * 4];
|
|
for oy in 0..SIZE {
|
|
for ox in 0..SIZE {
|
|
let (mut ar, mut ag, mut ab, mut aa) = (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32);
|
|
for sy in 0..SS {
|
|
for sx in 0..SS {
|
|
let x = ox as f32 + (sx as f32 + 0.5) / SS as f32;
|
|
let y = oy as f32 + (sy as f32 + 0.5) / SS as f32;
|
|
let (r, g, b, a) = if inside_o(x, y) || inside_f(x, y) {
|
|
(fg.r(), fg.g(), fg.b(), 255u16)
|
|
} else if inside_bg(x, y) {
|
|
(accent.r(), accent.g(), accent.b(), 255u16)
|
|
} else {
|
|
(0, 0, 0, 0)
|
|
};
|
|
let af = a as f32 / 255.0;
|
|
ar += r as f32 * af;
|
|
ag += g as f32 * af;
|
|
ab += b as f32 * af;
|
|
aa += af;
|
|
}
|
|
}
|
|
let samples = (SS * SS) as f32;
|
|
let idx = (oy * SIZE + ox) * 4;
|
|
let (r, g, b) = if aa > 0.0 {
|
|
(ar / aa, ag / aa, ab / aa)
|
|
} else {
|
|
(0.0, 0.0, 0.0)
|
|
};
|
|
rgba[idx] = r.round() as u8;
|
|
rgba[idx + 1] = g.round() as u8;
|
|
rgba[idx + 2] = b.round() as u8;
|
|
rgba[idx + 3] = (aa / samples * 255.0).round() as u8;
|
|
}
|
|
}
|
|
|
|
egui::IconData {
|
|
rgba,
|
|
width: SIZE as u32,
|
|
height: SIZE as u32,
|
|
}
|
|
}
|