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>
201 lines
9.3 KiB
Markdown
201 lines
9.3 KiB
Markdown
# 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.
|