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>
This commit is contained in:
SowinskiBraedenandClaude Opus 5 committed 2026-09-24 20:29:00 -07:00
commit 377f3a31b3
91 files changed
+24994

No files matched your search

+200
View File
@@ -0,0 +1,200 @@
# Mod readiness: investigation findings
**Status: investigated, implemented as reporting only. No action is attached, and none should be until a supported install mechanism exists.**
The launcher mockup shows an installed / outdated / missing mod breakdown, a
download size, and a "Prepare mods & join" action. This document records what of
that is actually achievable today. Measurements were taken on 2026-09-24 against
a real Arma Reforger installation (game build `24903726`, client `1.8.0.13`) and
the live reforgermods.net API.
## Summary
| Question | Answer |
| --- | --- |
| Can installed Workshop mods be detected locally? | **Yes, reliably.** |
| Can installed *versions* be detected? | **Yes**, and they match the API exactly. |
| Does the API expose the versions a server requires? | **Yes**, with sizes. |
| Can missing-mod installation be triggered from outside the game? | **Not verified. No supported mechanism found.** |
| Does the game handle missing mods during the Steam join flow? | **Yes** — this is what makes the launcher useful without the above. |
The first three make a readiness *indicator* feasible, and it now ships. The
fourth is what "Prepare mods & join" would need, and it is the one that does not
hold up — so the launcher reports and does not offer to act.
## Where Reforger stores Workshop content
Downloaded addons live in the user's profile directory, **not** in the Steam
application directory and **not** in Steam Workshop storage (Reforger uses
Bohemia's own Workshop CDN, so `steamapps/workshop` is empty for app 1874880):
```text
%USERPROFILE%\Documents\My Games\ArmaReforger\
addons\<AddonName>_<16-HEX-MOD-ID>\
ServerData.json id, name, installed revision version
meta full Workshop metadata: versions, dependencies, sizes
addon.gproj GUID and dependency GUIDs
data.pak content
resourceDatabase.rdb
logs\
profile\
```
`Documents` is commonly redirected to OneDrive, so the implementation asks the
shell folder registry (`HKCU\...\Explorer\Shell Folders`, `Personal`) first —
which follows a redirection to any path — and keeps
`%USERPROFILE%\Documents`, `%USERPROFILE%\OneDrive\Documents` and
`%OneDrive%\Documents` as fallbacks.
### On Linux
There is no native Linux client: Reforger runs under Proton, so the same profile
directory lives inside the Wine prefix Steam keeps per app, in whichever library
folder the game was installed into:
```text
<library>/steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents/
My Games/ArmaReforger/addons/
```
Everything below this point — the naming rule, the BOM, `ServerData.json`, the
version comparison — is identical, because it is the same Windows game writing
the same files; only the path to them differs.
The scanner probes every known Steam root (`~/.steam/steam`, `~/.steam/root`,
`~/.local/share/Steam`, and the Flatpak
`~/.var/app/com.valvesoftware.Steam/.local/share/Steam`) and reads each one's
`steamapps/libraryfolders.vdf`, because the prefix lives in the library the game
was installed into — routinely not the one Steam itself lives in. `steamuser` is
Proton's fixed prefix account name, not the user's.
## Reliability of local detection
Measured against a real store of **238 directory entries**:
- 235 were addon directories; all 235 contained both `ServerData.json` and `meta`.
- 3 were not addons (a `saves` directory and two stray preview JPEGs) and are
excluded by the `<Name>_<16 hex>` naming rule.
- Coverage of `ServerData.json`: **235 / 235**.
`ServerData.json` is the cheapest source — roughly 250 bytes — and contains
exactly what is needed:
```json
{"id":"62CCD69DD17E4F2F","name":"AKI_Core","revision":{"version":"8.1.1", ...}}
```
Two details that matter in practice:
- Both `ServerData.json` and `meta` are written **UTF-8 with a byte-order mark**.
`serde_json` rejects a leading BOM, so it must be stripped before parsing.
- The mod id is also recoverable from the directory name. Addon names contain
underscores, so the id is the *last* `_`-separated group, and it must be
validated as 16 hex characters rather than simply taken.
## Version agreement with the API
`GET /v2/servers/{id}` returns each required mod with an exact `version` and,
where resolved, a `size` in bytes. Comparing that to the local store:
| Server | Required | Up to date | Outdated | Missing | Missing download |
| --- | ---: | ---: | ---: | ---: | ---: |
| `[OGR2] Old Guard Revival 2` | 122 | **122** | 0 | 0 | 0 |
| `[NA2] TWS UHC Ukraine v Russia` | 173 | 26 | 1 | 146 | 14.42 GiB |
| `LA FEDERACIÓN: ESTADO FALLIDO` | 91 | 15 | 0 | 76 | 15.59 GiB |
| `[BnB1xCOC] Beers an Bipods` | 136 | 39 | 1 | 96 | 8.17 GiB |
| `[EU1] SlavicWar` | 197 | 36 | 1 | 160 | 12.85 GiB |
| `UNSC Battlegroup Athens` | 104 | 12 | 1 | 91 | 19.09 GiB |
The first row is a server the machine's owner plays: **122 of 122 mods matched
on both id and exact version string**, with zero false negatives. Version strings
are byte-identical between `ServerData.json` and the API — no normalisation was
needed. The remaining rows confirm the missing and outdated paths produce
sensible figures rather than degenerate ones.
One caveat on sizes: `modSummary.unresolvedCount` is often non-zero on
`/v2/servers/{id}/mods` (that endpoint skips size enrichment unless asked), while
the detail endpoint resolves them. Any download figure must be reported as a
floor — "at least *N*, plus *k* unknown" — not a total.
## Triggering installation: not solved
This is where the feature stops.
- Reforger mods are **not** Steam Workshop items. `steam://` has no verb that can
install them, and `steamapps/workshop` holds nothing for app 1874880.
- No documented command-line switch or URI on the Reforger client was found that
installs a mod set without entering the game.
- The `meta` file is written *by* the game; treating it as an input — fabricating
entries to make the client fetch content — is unsupported, undocumented, and
would risk corrupting a user's addon store.
What does work is the flow the launcher already uses: `steam://run/1874880//<payload>/`
starts the game with a join request, and **the game itself resolves the missing
dependencies**, prompting for and downloading them before connecting. A user with
none of a server's 173 mods can still press "Join server"; the game handles it.
That is why milestone 1 ships the direct join and no readiness panel.
## What is implemented
`crates/rfm-core/src/modscan.rs` — detection:
- `AddonInventory` — the trait, so readiness logic can be tested against a fixed
inventory and a different platform layout can be added without touching callers.
- `FilesystemInventory` — scans the addon store; `discover()` locates it.
- `parse_server_data` / `addon_id_from_dir_name` — the two parsing rules above.
- `readiness(required, installed)` — up to date / outdated / missing, plus
`missing_bytes` and `unresolved_mods`.
`crates/rfm-core/src/readiness.rs` — reporting. Bridges the API mod list to the
local inventory and produces a `ReadinessReport` for the UI. Two rules are
enforced here rather than in the UI, so they cannot be lost in a redesign:
- A scan that did not run reports `available: false` with every count zeroed.
`is_ready()` is false in that state: **not knowing is not being ready.**
- A mod whose size the API could not resolve is counted in `unresolved_mods`
rather than contributing zero bytes, so `missing_bytes` is always a floor that
the UI can honestly label "at least".
`src/lib/format.ts` — `readinessSummary()` builds the sentence, and is unit
tested against the exact expected string. `src/components/ReadinessNote.tsx`
renders it as a line of text and a count strip. There is no button.
## The copy
```text
96 of 136 mods missing · at least 8.17 GiB · Arma Reforger will download these when you join
1 of 136 mods out of date · Arma Reforger will update these when you join
All 136 mods installed and up to date.
No local Arma Reforger addon folder found, so installed mods could not be checked.
```
"at least" appears whenever any required mod's size is unresolved. The closing
clause names the actor — the game, not the launcher — because that is what
actually performs the download.
## Verified end to end
`cargo test -p rfm-core --test live_api -- --ignored readiness_against_a_real`
runs the real scanner against a real addon store (`RFM_ADDONS_DIR`) and the live
API. Against the 235-addon store described above:
```text
local store: 235 addons
[AU] FTA #1 | From The Ashes req=123 ok= 59 old= 6 miss= 58 >= 1.65 GiB
[AU] FTA #2 | From The Ashes req=127 ok= 61 old= 6 miss= 60 >= 2.84 GiB
[NA6] W.C.S. Realism NATO/RUS Belleau Wood req= 83 ok= 64 old= 4 miss= 15 >= 1.30 GiB
[OG9] Old Guys 9 | Fallujah Iraq req=114 ok= 85 old=29 miss= 0 >= 0.00 GiB
[EU] LuckyGames Hardcore | Bakhmut req=106 ok= 35 old= 9 miss= 62 >= 5.10 GiB
[EU2] W.C.S. Realism NATO/RUS Serhiivka req= 80 ok= 62 old= 4 miss= 14 >= 0.86 GiB
```
The test asserts the three buckets partition the required list exactly. The OG9
row — 0 missing but 29 outdated — is the case that exercises the "update these"
wording rather than "download these".
## Still not solved
Triggering installation. Nothing above changes that, and the "Prepare mods &
join" action stays out until a supported mechanism exists.