A modular FAF (Supreme Commander: Forged Alliance Forever) client in Rust + Tauri, with a React frontend reusing the ForgeMapToolkit design system.
Architecture is the contract: see docs/ARCHITECTURE.md.
One source of truth in Rust, a fully state-driven UI, no per-tab logic.
The unidirectional state loop is wired end-to-end and proven:
UI dispatch ─▶ Tauri command ─▶ service ─▶ event ─▶ reduce(AppState) ─▶ emit ─▶ store ─▶ UI
Features so far:
- session — connection-status handshake.
- auth — login/logout via the Port pattern (
AuthPort). Real providerOAuthAuth(FAF Ory Hydra, Authorization Code + PKCE);FakeAuthfor tests/offline. State-driven login ↔ app routing. - nav — multi-tab shell; the active tab lives in
AppStateso the backend can drive navigation too. - settings — persisted preferences (first: UI
theme), loaded from disk on startup and saved on change viaSettingsPort→FileSettings. The theme is type-safe across the boundary (aThemeenum) and applied as<html data-theme>. Four themes ship:forgeDark(default),forgeLight,javaClient,pythonClient— each a token set intokens.css, so adding a theme touches no component. - lobby — a live open-games list driven by a streaming port (
LobbyPort::connect→ snapshot stream →GamesUpdatedevents), with explicitconnect/disconnect. Real providerLobbyClient(FAF lobby WebSocket protocol) behindFAF_REAL_LOBBY=1, verified live against production;FakeLobbyis the default. The connection flow isGET user.faforever.com/lobby/access→ verifiedwss://…/?verify=…URL →ask_session→auth(token +faf-uidfingerprint) →game_info. The OAuth access token reaches the lobby via an in-memoryTokenStore, never throughAppState.
CI (.github/workflows/ci.yml) runs tests, clippy, typecheck, build, a
bindings-drift check (fails if ui/src/ipc/bindings.ts is stale), and a tokens-only
guardrail (fails if any component CSS hardcodes a hex color instead of a token).
Env toggles for local dev:
FAF_FAKE_AUTH=1— skip the browser login (offline fake auth)FAF_REAL_LOBBY=1— use the real lobby WebSocket client instead of the fakeFAF_REAL_LAUNCH=1— actually start the ICE adapter + game on join (else the join stops at the modeled launch order). RequiresFAF_GAME_PATH+ the adapter binaries.FAF_ICE_ADAPTER_KIND—java(default) selects the Javafaf-ice-adapter;goselectsfaf-pioneer.FAF_UID_PATH— path to thefaf-uidexecutable (https://github.com/FAForever/uid/releases)FAF_ICE_ADAPTER_JAR— path tofaf-ice-adapter.jar(Java adapter)FAF_JAVA_PATH— path to thejavaexecutable (defaultjava, i.e. onPATH)FAF_ICE_ADAPTER_PATH— path to thefaf-pioneerexecutable (Go adapter)FAF_GAME_PATH— path toForgedAlliance.exe(its folder must holdinit_<mod>.lua)FAF_CLIENT_VERSION— client version reported to the lobby (defaults to the crate version)FAF_USER_API_BASE/FAF_API_BASE/FAF_HYDRA_BASE/FAF_LOBBY_URL— endpoint overrides (e.g. staging)
crates/
faf-domain/ pure state + events + commands + reducer (no IO, no async)
faf-app/ runtime loop, services, ports, infra
faf-ipc/ generates ui/src/ipc/bindings.ts from the Rust types
src-tauri/ thin Tauri shell (commands + event forwarding)
ui/ React frontend (ipc bridge, Zustand store, features)
docs/ ARCHITECTURE.md
Prerequisites: Rust (stable), Node 20+, pnpm (via corepack enable pnpm), and the
Tauri prerequisites for your OS (on Windows: WebView2, already present on Win 10/11).
pnpm install # frontend deps
pnpm run bindings # regenerate ui/src/ipc/bindings.ts from Rust
pnpm run tauri dev # run the app (Vite + Tauri)Other commands:
cargo test # run Rust tests (reducer + loop)
pnpm run typecheck # tsc over the frontend
pnpm run build # build the frontend to ui/distWhen you add anything, follow docs/ARCHITECTURE.md §8:
- New state → a slice + reducer + tests in
faf-domain/state/. - New capability → a command + event(s) + a service in
faf-app/services/. - New external system → a
Porttrait + aninfraimpl + a mock. - New screen → a
features/<tab>/folder; container selects + dispatches, view uses primitives. - After changing cross-boundary types, run
pnpm run bindings.
Never: mutate state outside a reducer; do IO outside infra; put logic in a component;
hand-write a cross-boundary type.