Files
reforgermods-launcher/README.md
T
SowinskiBraedenandClaude Opus 5 377f3a31b3 initial commit: reforgermods.net launcher 0.1.0
A desktop server browser for Arma Reforger, built on the reforgermods.net v2
API. Live server list with search, sort and filters; a detail inspector with
population history and mod readiness; measured ICMP latency; favorites, recents
and settings; and direct joining through Steam.

Tauri 2 and Rust, with a Preact frontend that builds to 80 KB of JavaScript in
one chunk. Logic lives in rfm-core, which has no Tauri dependency and is tested
directly; src-tauri is the window, the command surface and the wiring.

Packaged for the public beta: a per-user NSIS installer, a portable .exe and an
Arch package, with signature-verified background updates on Windows and a
release pipeline that builds from a tag. The artifacts are not yet
Authenticode-signed — that is wired and waiting on an Azure Trusted Signing
identity, and docs/distribution.md says so plainly rather than letting anyone
discover it at the SmartScreen dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 20:29:00 -07:00

17 KiB
Raw Blame History

reforgermods.net launcher

A fast, lightweight desktop companion for Arma Reforger, built on the reforgermods.net API. It is a live server browser that can put you straight into a server through Steam, without going through the in-game lobby.

Unofficial. Not affiliated with or endorsed by Bohemia Interactive.

Status

0.1.0 — public beta. The launcher browses and filters the live server list, shows full detail for a selected server, joins directly through Steam, and keeps favorites, recents and settings across restarts. Milestone 2 added population history charts, a scenario filter, and mod readiness as reporting only; 0.1.0 adds packaging, signed auto-updates and a download page.

Beta artifacts are a per-user NSIS installer, a portable .exe and an Arch package, published to dl.reforgermods.net and offered from reforgermods.net/launcher/. They are not Authenticode-signed yet, so Windows shows a SmartScreen warning on first run — the pipeline is wired for Azure Trusted Signing and waiting on the identity. See docs/distribution.md.

What works today:

  • Live server browser over GET /v2/servers, with search, sort, region, platform, official / modded / BattlEye / password / offline filters, and page-local scenario and minimum-player filters
  • Sortable columns. Clicking a heading sorts by it. players and name are ordered by the API across every matching server; queue, ping, mods, region and any reversed direction are orderings the API does not offer, so they reorder the loaded page — marked with a hollow arrow and named in the footer, never presented as a search of the whole fleet
  • Selected-server inspector: scenario, population, queue, region, server FPS, and the full required-mod list with versions and sizes
  • Population history chart over 6h / 24h / 7d / 30d, drawn as inline SVG with no charting library
  • Mod readiness, reporting only: how many required mods are installed, outdated or missing, and at least how much is left to download
  • Measured ping to the selected server by ICMP — a real round trip, not a region estimate
  • Scenario imagery from the Bohemia CDN, host-allowlisted and CSP-enforced
  • Region latency in the browser, sortable — eight ICMP probes cover a whole page
  • Per-platform marks (PC / Xbox / PlayStation) matching the site, not a crossplay flag
  • Fleet-wide player total in the title bar
  • Join Discord where a server advertises an invite in its name
  • Discord Rich Presence, showing the launcher as the running application (needs a Discord application; see docs/discord-presence.md)
  • Direct join: steam://run/1874880//<base64 payload>/
  • Favorites, recent servers and launch history, persisted locally
  • Settings, and an anonymous, user-resettable install identifier
  • Background updates on Windows, signature-verified, downloaded quietly and applied as the app closes — never mid-session. Linux installs are the distro's to update
  • Short-lived response caching, and loading / empty / error / offline states
  • Compact API status in the header, with dataset freshness

Not implemented, on purpose:

  • "Prepare mods & join". Readiness is reported, never acted on. There is no verified way to install Reforger Workshop content from outside the game, so the launcher says what the game will do instead of offering a button it cannot honour. See docs/mod-readiness.md.
  • Accounts, uptime figures, mod browsing, background tray, auto-start.

Stack

Layer Choice Why
Shell Tauri 2 Native WebView2 on Windows; no bundled browser runtime
Native Rust Steam launching, HTTP, persistence, local filesystem inspection
UI Preact + @preact/signals + TypeScript ~4 KB runtime, JSX in plain .tsx, and per-component signal subscriptions so the one-second freshness clock re-renders the status pill and not the 500-row table
Build Vite 7

The production bundle is 51 KB of JavaScript (17 KB gzipped) and 14 KB of CSS, in one chunk. The history chart is hand-rolled inline SVG — a charting library would have cost more than the rest of the UI combined.

Layout

crates/rfm-core/        Pure Rust. No Tauri dependency, so it is unit-testable directly.
  api/                  HTTP client, typed models, query builder, error taxonomy
  launch.rs             Server id validation -> join payload -> Steam URI
  store.rs              Settings, favorites, recents, launch history
  cache.rs              Bounded TTL cache of response bodies
  modscan.rs            Local Workshop addon detection
  readiness.rs          Required mods vs installed; reporting only, never acts
  ping.rs               ICMP latency; address validation is the security boundary
  presence.rs           Discord Rich Presence text, within Discord's limits

src-tauri/              The application shell. Window, commands, wiring. Thin.
  src/commands.rs       The entire IPC surface
  src/state.rs          Shared state
  src/update.rs         Background updates, Windows only; applied as the app exits
  capabilities/main.json
  tauri.signing.conf.json  Adds only signCommand; merged over the config in CI

src/                    UI
  lib/                  types, IPC wrappers, signals, pure formatters, chart geometry
  components/           title bar, status pill, sidebar, filter bar, server table,
                        inspector, history chart, readiness note, platform badges
  views/                browse, favorites, recent, settings

docs/api-contract.md          Endpoints used, and what is deliberately not used
docs/mod-readiness.md         The mod-readiness investigation and its findings
docs/client-identification.md Install-identity spec and API-side implementation notes
docs/discord-presence.md      Discord Rich Presence setup and behaviour
docs/distribution.md          Installer, code signing, release procedure, R2 layout
packaging/arch/               PKGBUILD and desktop entry for the Arch package
packaging/windows/sign.cmd    Authenticode signing, called by Tauri during bundling

scripts/version.sh            The version, in one place: show / check / set
scripts/release-manifest.py   releases.json (the site) and latest.json (the updater)
scripts/build-windows.sh      Cross-compile the bare .exe from Linux
scripts/build-arch.sh         Native Arch package, built in a container
.github/workflows/release.yml Tag -> build, sign, publish to R2

Prerequisites

  • Rust stable (1.82+) and Node.js 20+
  • Windows (the primary target): the WebView2 runtime, which ships with Windows 11 and current Windows 10, plus the MSVC build tools
  • Linux (development, and Arch packaging): the Tauri system dependencies — libwebkit2gtk-4.1-dev, libgtk-3-dev, libayatana-appindicator3-dev, librsvg2-dev, libsoup-3.0-dev, build-essential

The TLS backend is chosen per target: schannel on Windows (no bundled C crypto library, and the machine's own trust store), rustls elsewhere.

Installed-mod detection finds Reforger's addon store on both platforms: the (often redirected) Documents directory on Windows, read from the shell folder registry, and the Proton prefix inside each Steam library on Linux — there is no native Linux client, so the game's Documents is <library>/steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents. RFM_ADDONS_DIR overrides the search for an install neither layout describes.

Development

npm install
npm run tauri dev          # run the app

Frontend alone, in a browser (IPC calls will fail, but layout iterates fast):

npm run dev

Checks

cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p rfm-core                 # 168 unit tests, no network
npm run check                          # tsc --noEmit
npm test                               # 82 tests, including a jsdom shell smoke test
scripts/version.sh check               # the four version copies agree

Contract checks against the live API, ignored by default:

# Serially: the public rate limit is 60 requests/minute, and these tests
# fetch server detail in loops.
cargo test -p rfm-core --test live_api -- --ignored --test-threads=1

To exercise the mod scanner against a real Arma Reforger addon store:

RFM_ADDONS_DIR="$HOME/Documents/My Games/ArmaReforger/addons" \
  cargo test -p rfm-core --test live_api -- --ignored readiness_against_a_real

The ping tests send ICMP echo requests to live game servers; they are part of the same ignored-by-default suite.

Build

npm run tauri build

Produces a per-user NSIS installer under target/release/bundle/nsis/, plus a detached .sig when an updater signing key is present in the environment. MSI is deliberately not built: it needs admin rights and doubles the release for an audience a game utility does not have.

Releases are cut from a v* tag by .github/workflows/release.yml, which builds on Windows, signs, and publishes to dl.reforgermods.net. The version lives in one place and is propagated by scripts/version.sh:

scripts/version.sh show                # the canonical version
scripts/version.sh set 0.1.1           # rewrite all four copies

The full procedure, the signing state and the R2 layout are in docs/distribution.md.

Cross-building a Windows binary from Linux

No Visual Studio, and no root:

scripts/build-windows.sh            # debug, shows a console with diagnostics
scripts/build-windows.sh --release  # optimised, no console

cargo-xwin fetches the MSVC CRT and Windows SDK from Microsoft into ~/.cache/cargo-xwin, and LLVM's clang-cl/lld-link replace cl.exe and link.exe; the script unpacks those from Debian packages into a local prefix. The result is a standalone .exe — the frontend is embedded at compile time — that needs only the WebView2 runtime, which ships with current Windows.

Building an Arch Linux package

scripts/build-arch.sh               # target/arch/*.pkg.tar.zst
scripts/build-arch.sh --no-check    # skip the rfm-core test suite

The build runs inside an archlinux:base-devel container, so the binary links Arch's own glibc and WebKit rather than the build machine's. This is not fussiness: a Tauri binary built on Debian links libwebkit2gtk-4.1 against Debian's ABI, and handing that to an Arch user produces a blank window or a missing-symbol crash. Docker is only being used as a clean Arch userspace — nothing is deployed.

The tester installs it with sudo pacman -U <file>; pacman pulls webkit2gtk-4.1 and the rest from the declared dependencies.

Two things the PKGBUILD has to work around, both of which fail late and confusingly if forgotten:

  • options=('!lto') — makepkg puts -flto=auto in CFLAGS/LDFLAGS by default, which makes aws-lc-sys (the C crypto behind rustls) compile to GCC LTO bitcode that the linker cannot resolve. The build dies at the final link with a page of undefined aws_lc_* symbols. Rust-level LTO is unaffected; it comes from [profile.release].
  • cmake in makedepends — aws-lc-sys needs it, and base-devel does not include it.

build() asserts the result is a production build before packaging, for the same reason build-windows.sh does: a dev build installs perfectly and then shows ERR_CONNECTION_REFUSED on the tester's machine.

Nvidia under Wayland

WebKitGTK's DMABUF renderer and the proprietary Nvidia driver disagree about buffer import, and GTK aborts before the window is ever mapped:

Gdk-Message: Error 71 (Protocol error) dispatching to Wayland display.

main.rs detects that combination at startup and sets WEBKIT_DISABLE_DMABUF_RENDERER=1 for itself. The detection is deliberately narrow — Nvidia loaded, a Wayland session, GDK not already pinned to X11, and no existing value to override — because the fallback costs GPU acceleration for anyone who does not need it. should_disable_dmabuf is a pure function with tests covering each condition.

To override, set the variable yourself; any value is honoured, including 0.

Direct joining

Given a server UUID, the launcher builds:

{"id":"<uuid>","inviteToken":""}
  -> UTF-8 bytes
  -> standard base64
  -> trailing "=" removed
  -> steam://run/1874880//<payload>/

The doubled slash

steam://run/<appid>//<args>/ is Steam's form for passing launch arguments, and the empty path segment is what marks the payload as one. With a single slash Steam can start the game without forwarding the payload, so the player lands on the main menu and the join request is silently lost. A test pins this shape, because the difference is one character and the failure looks like a successful launch.

Not yet confirmed against a live client. The single-slash form was observed to launch without joining; the doubled slash is the current best understanding of the fix.

A join only carries the payload when the game actually starts. If Arma Reforger is already running, Steam may focus the existing process and drop the arguments, so this must be tested from a fully closed game.

steam://run/1874880 (no payload) is the plain "Launch Arma Reforger" action and is unaffected.

An alternative route

The game also registers its own URL protocol. Its Steam install script (steamapps/common/Arma Reforger/installscript.vdf) writes:

HKCU\SOFTWARE\Classes\ArmaReforger\shell\open\command
  "<dir>\ArmaReforger_BE.exe" -exe ArmaReforgerSteam.exe
  "-addonsDir=<dir>\addons" -uri="%1"

reforger_join_uri() builds ArmaReforger://<payload>, which reaches the launcher as -uri= without depending on how Steam forwards arguments. It is implemented, tested and unused — a second candidate if the Steam form does not join.

This lives in crates/rfm-core/src/launch.rs and is pure and directly tested, including against the verified reference vector. No shell is involved at any point: the URI is handed to the platform's URI opener, and the only value interpolated into it is a ServerId, which cannot be constructed without passing UUID validation.

Ping

A browser cannot measure round-trip latency to a game server; a native launcher can, and this is one of the reasons for building one.

TCP connect timing was tried and rejected. Connecting to a server's UDP game port "succeeded" in 12–18 ms — and so did port 9 and port 47123, which are closed. Some middlebox completes every handshake, so TCP timing yields confident-looking numbers that are entirely fictional.

ICMP echo works. Measured against 12 live servers, 11 answered, with latencies coherent by region (Los Angeles 41 ms, New York 58–83 ms, Frankfurt 154–184 ms, Sydney 173–179 ms). The launcher sends three probes and displays the median, so one delayed packet does not move the figure.

  • Windows uses IcmpSendEcho via the IP Helper API — no elevation required.
  • Unix uses an unprivileged ICMP datagram socket.
  • The selected server is measured exactly, in the inspector.
  • The browser column shows latency to each server's region. /v2/servers publishes no per-server address — only /v2/servers/{id} does — so pinging a 100-row page would cost 100 detail requests against a 60/minute limit. Every row does carry pingSiteId, and there are only eight sites, so eight probes cover the list. Region and server latency agree within roughly 20 ms, which is enough to sort by and is what the game's own browser shows.
  • Roughly a third of servers filter ICMP. Those show "no reply" rather than a region-level estimate dressed up as a measurement.

Security posture

  • Everything from the API — server names, scenario names, mod names, URLs — is treated as untrusted. It is rendered as text content and never interpolated into a shell command, a URI, or markup.
  • Server ids are validated before they reach a URL, on both the launch path and the "open on reforgermods.net" path. The latter builds its URL from the validated id rather than from the API's own modURL field.
  • No API key or credential is embedded. The launcher is a public client and uses anonymous access only. The one value it sends that persists across requests is a random install identifier, visible and resettable in Settings; see docs/client-identification.md.
  • The API origin is not user-configurable, so a tampered settings file cannot redirect requests — or the install identifier — to another host.
  • Server addresses from the API are validated to public literal IPs before any packet is sent, so remote data cannot drive DNS or probe the user's LAN.
  • Image URLs are checked against a single-host allowlist before rendering, with the page CSP enforcing the same list independently.
  • The UI is granted core:default and nothing else — no filesystem, shell, HTTP or opener permission. URIs are opened from Rust, after validation.
  • A strict CSP allows 'self' plus scenario thumbnails from the Bohemia CDN.

Relationship to reforgermods.net

This is a client. All server and mod data comes from the public https://api.reforgermods.net v2 API; the launcher holds no data of its own beyond local preferences and caches, and duplicates no backend logic. See docs/api-contract.md.