Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
159 commits
Select commit Hold shift + click to select a range
0209688
Propose a mod manager: per-version mod sets and a signed index
Zorro909 Nov 4, 2026
93a7890
Add a mod manager
Zorro909 Nov 4, 2026
c65516e
Trust the published mod index key
Zorro909 Nov 4, 2026
e8b5599
Describe the mod manager as built
Zorro909 Nov 4, 2026
8e65ca4
Name the mod behind every engine load failure
Zorro909 Nov 4, 2026
6fcfa0b
Split the mod backend by job and close the review's safety gaps
Zorro909 Nov 4, 2026
7a2e70f
Document the index format as the launcher enforces it
Zorro909 Nov 4, 2026
889054d
Bring the proposal's layout and trust table up to date
Zorro909 Nov 4, 2026
bd9af6c
Add vitest for unit tests of the UI's pure logic
Zorro909 Nov 4, 2026
5ebf74b
Match the round 2 IPC contract in the frontend types
Zorro909 Nov 4, 2026
6dba301
Order prerelease versions like semver and the backend
Zorro909 Nov 4, 2026
828bca1
Type message keys, add plurals and check every key and error code
Zorro909 Nov 4, 2026
9cc0979
Move check and status decisions out of components
Zorro909 Nov 4, 2026
75729e9
Coordinate profile requests and return typed action results
Zorro909 Nov 4, 2026
e205711
Review every install, unblock Play in place and fit 960x565
Zorro909 Nov 4, 2026
7287796
Make the mock follow the backend's rules and document its scenarios
Zorro909 Nov 4, 2026
7fc695c
Make the language menu a menu and set the document language
Zorro909 Nov 4, 2026
37648a5
Drop an unused check helper
Zorro909 Nov 4, 2026
317d769
Keep Browse summaries to two lines
Zorro909 Nov 4, 2026
ef94350
Merge the round-2 UI work
Zorro909 Nov 4, 2026
a53053d
Use the generated IPC types and test the UI in CI
Zorro909 Nov 4, 2026
a073ed3
Split the index module by job and give the index an age
Zorro909 Nov 4, 2026
0953465
Run mod changes on a context and check them again before committing
Zorro909 Nov 4, 2026
5a5231f
Check source mod ids and merge their releases newest first
Zorro909 Nov 4, 2026
c5630f1
Generate typed command signatures and event payloads
Zorro909 Nov 4, 2026
158c2af
Match crash diagnosis to what the engine logs
Zorro909 Nov 4, 2026
cde8d00
Leave a damaged store file for the download to replace
Zorro909 Nov 4, 2026
8fb4fb2
Bring the mod manager docs up to date
Zorro909 Nov 4, 2026
136030d
Refuse a commit when the wanted build left the catalogue
Zorro909 Nov 4, 2026
88b67ff
Add the round-3 contract types and an honest Unknown error
Zorro909 Nov 4, 2026
c4a2091
Port the mod rules to the mock and test them against shared fixtures
Zorro909 Nov 4, 2026
ff7c5a8
Say which dependency is missing and how each install ends
Zorro909 Nov 4, 2026
e2f33c9
Give each operation one owner of its progress
Zorro909 Nov 4, 2026
ec51341
Show blockers, revocations, notices and plans the same everywhere
Zorro909 Nov 4, 2026
ca5fc22
Reword all four languages for the new states
Zorro909 Nov 4, 2026
8f65504
Check every scenario's content and the new flows in the UI tests
Zorro909 Nov 4, 2026
d14f355
Merge the round-3 UI work
Zorro909 Nov 4, 2026
bc80434
Draw the Portuguese and Spanish accented letters in the pixel font
Zorro909 Nov 4, 2026
6ad2d3f
Read revocations failing closed and check cached sources again
Zorro909 Nov 5, 2026
4a72b81
Check game-version URLs with the same parsed rule as mods
Zorro909 Nov 5, 2026
1107953
Only blame a mod for the engine's own ERROR lines
Zorro909 Nov 5, 2026
92b2f88
Give each version's mods folder copies, not hard links
Zorro909 Nov 5, 2026
7a25ac8
Bind installs to the reviewed plan, give operations ids, report shado…
Zorro909 Nov 5, 2026
c52c4d6
Run the mock's shared rule cases through the real rules
Zorro909 Nov 5, 2026
318b348
Test the service end to end: files, copies, sources, profiles, consent
Zorro909 Nov 5, 2026
c946ed2
Run the Rust job on Linux, Windows and macOS
Zorro909 Nov 5, 2026
433408d
Document round 4 and fix what the proposal got wrong
Zorro909 Nov 5, 2026
7e9d9ac
Refuse command shapes the bindings parser can't read
Zorro909 Nov 5, 2026
8e903a6
Say how to regenerate the bindings and where the shared rule cases run
Zorro909 Nov 5, 2026
9d76d0e
Add contract R4's shapes and calls as pending types and API
Zorro909 Nov 5, 2026
22bfb86
Implement contract R4 in the mock and the shared rule cases
Zorro909 Nov 5, 2026
4b7cc16
Show errors as a translated sentence, with the backend's words behind…
Zorro909 Nov 5, 2026
e431f15
Give operations ids, bind installs to the reviewed plan, and show sha…
Zorro909 Nov 5, 2026
bf8dd6c
Keep the Mods header still and give the list its own scroll
Zorro909 Nov 5, 2026
c67013e
Add the shadowed and chain scenarios and the new mock flags, and docu…
Zorro909 Nov 5, 2026
fb1f9aa
Check the Russian glossary: a weaker word for Unverified, copy not mo…
Zorro909 Nov 5, 2026
c7be2e7
Lead with the unknown fit and its Retry, not a red can't-install, and…
Zorro909 Nov 5, 2026
ad8daa8
Give offline Browse cards and Sources causes their full width, and sh…
Zorro909 Nov 5, 2026
af19826
Keep three whole rows in view under the banners in every language, an…
Zorro909 Nov 5, 2026
4be59fe
Test the R4 UI end to end: layout in every language, cancel, re-plan,…
Zorro909 Nov 5, 2026
a7533c9
Say that a copy in the player's own folder loads instead, when a mod …
Zorro909 Nov 5, 2026
d4738cc
Merge the round-4 UI work
Zorro909 Nov 5, 2026
dfef5b8
Use the round-4 bindings and run the new rule cases in Rust
Zorro909 Nov 5, 2026
6f82676
Show one blocking mod at a time with all its fixes, put results in a …
Zorro909 Nov 5, 2026
4957fed
Key plans by what decides them, and keep a plan usable while the same…
Zorro909 Nov 5, 2026
f175f87
Check consent against what the catalogue says now, keep a damaged sou…
Zorro909 Nov 5, 2026
a5487e0
Follow the backend in the mock and the page: typed commands, restore …
Zorro909 Nov 5, 2026
a42581f
Keep a crash notice while the player's own copy of the mod can still …
Zorro909 Nov 5, 2026
eb8a9ec
Say Unverified and revocations unambiguously in Russian, end no date …
Zorro909 Nov 5, 2026
b173b02
Hold the backend and the mock to one shared install event trace, and …
Zorro909 Nov 5, 2026
7cadf1c
Docs: copies not hard links, what Play safety covers, publishing from…
Zorro909 Nov 5, 2026
87b2b30
Drop the pre-R4 fallbacks, name the destination in install labels, an…
Zorro909 Nov 5, 2026
d002c49
Say a refused index was refused rather than offline; add the invalid …
Zorro909 Nov 5, 2026
9c519fa
Test a click in the first frame the install button is enabled
Zorro909 Nov 5, 2026
f150400
Count the network down only when no origin answered; say Preparing be…
Zorro909 Nov 5, 2026
9302314
One tooltip at a time with a known owner, so Escape closes the focuse…
Zorro909 Nov 5, 2026
6598f3e
Take mod icons only from GitHub's hosts, in the backend, the page and…
Zorro909 Nov 5, 2026
e83694e
Fit the Mods tab at 960x565: whole names and tiers, reasons that wrap…
Zorro909 Nov 5, 2026
5ed07ff
Assert in every scenario and language that marked text is whole and f…
Zorro909 Nov 5, 2026
7e638d7
Keep a refused start on Home, with Check again, when the set can't be…
Zorro909 Nov 5, 2026
b0cb63e
Keep the full Spanish and Portuguese on/off words; narrow the row's s…
Zorro909 Nov 5, 2026
0c3c262
Sequence catalogue refreshes by revision in the backend and the page;…
Zorro909 Nov 5, 2026
8973684
Keep the copied builds listed after a copy, with version, tier and on…
Zorro909 Nov 5, 2026
4cb9166
Say an HTTP error status as the server answering, never as offline; l…
Zorro909 Nov 5, 2026
f7eaa03
Hear an unreviewed mod's check as Unreviewed and a mod a copy leaves …
Zorro909 Nov 5, 2026
3fe3572
Flag translated pieces put side by side in one sentence, and check tr…
Zorro909 Nov 5, 2026
0ab13d9
Type-ahead in the version picker: typed letters go to the version tha…
Zorro909 Nov 5, 2026
93a2f4e
Say which checks didn't pass right above the typed confirmation
Zorro909 Nov 5, 2026
e49f595
Move the crash notice into its own file; the fix choice stays in stat…
Zorro909 Nov 5, 2026
c86c85c
bindings: test nested containers and refused types, say what a comman…
Zorro909 Nov 5, 2026
6e9d8b9
Document that reconcile is idempotent and runs before every launch, a…
Zorro909 Nov 5, 2026
a35e1e3
Shared rule case for a revocation cascade: the revoked dependency is …
Zorro909 Nov 5, 2026
d0184a5
ModFailure is a tagged union by kind: a missing dependency carries de…
Zorro909 Nov 5, 2026
4b33c1e
JSON Schemas for index.json and <id>.mod.json, with the doc examples …
Zorro909 Nov 5, 2026
556a405
Split service.rs into service/: the catalogue, changes to a set, copy…
Zorro909 Nov 5, 2026
9266dc7
Split plan.rs into plan/: resolution and installability, the consent …
Zorro909 Nov 5, 2026
8bb64fa
Say progress through the one live region, once per file and step, ins…
Zorro909 Nov 5, 2026
cc3f03b
Test that an unreviewed mod's review check is heard as Unreviewed
Zorro909 Nov 5, 2026
cfbf7b8
Start from Versions as from Home: one launch hook that reads a refuse…
Zorro909 Nov 5, 2026
84873e1
Hold the whole progress record during a check's dwell, so the file nu…
Zorro909 Nov 5, 2026
8f21f0a
Focus never falls to the page body: a dialog whose opener is gone giv…
Zorro909 Nov 5, 2026
3b6b624
Say blockers as whole sentences one after another, not joined with ', '
Zorro909 Nov 5, 2026
e1a5ae9
Show a build record only when the signed index has all of it, worded …
Zorro909 Nov 5, 2026
8e8957c
Inflate every zip entry once into nothing during admission and count …
Zorro909 Nov 5, 2026
b681496
Hold and say each part of a catalogue refresh as it comes in: the ind…
Zorro909 Nov 5, 2026
1b41a4c
Fresh profile reads after an invalidation never share a read that beg…
Zorro909 Nov 5, 2026
c521ade
Ask before turning on a mod that would stop Play, with the blocker se…
Zorro909 Nov 5, 2026
6d0b60c
A crash that names no mod offers its next step: turn off all of the v…
Zorro909 Nov 5, 2026
bf502e7
Browse cards: the version (or 'Prereleases only') wraps whole beside …
Zorro909 Nov 5, 2026
d15ee6b
Badges, cut details and hashes are no tab stops: rows describe the ti…
Zorro909 Nov 5, 2026
6824db2
Install dialogs keep every shown check row in view at 960x565: detail…
Zorro909 Nov 5, 2026
53c49a6
The copy dialog shows each build's evidence on demand, says once that…
Zorro909 Nov 5, 2026
a8a0c5e
Off mods' names are a solid grey that reads at 4.5:1, not amber faded…
Zorro909 Nov 5, 2026
45e0a10
The collapsed own-folder banner says its files load in every version,…
Zorro909 Nov 5, 2026
294413c
check-i18n fails on a message no code uses; the 16 unused keys are go…
Zorro909 Nov 5, 2026
c009fe7
Crash and update fixes: a file mod's crash offers 'Replace … from fil…
Zorro909 Nov 5, 2026
04eda06
Home's mods line counts the files in the player's own folder, which l…
Zorro909 Nov 5, 2026
962bb1f
No index: the folded banner names the server's status (404), and Brow…
Zorro909 Nov 5, 2026
eb91cd6
A source's rate limit says until when, from GitHub's x-ratelimit-reset
Zorro909 Nov 5, 2026
168dc74
pt-BR, es and ru say that a revocation's reason is quoted in the inde…
Zorro909 Nov 5, 2026
f46fe22
Mock: a file takes about 0.75 s (a five-file copy about 4 s), fast=1 …
Zorro909 Nov 5, 2026
1360f07
What has been said lives in one Speech per app tree, held by a provid…
Zorro909 Nov 5, 2026
8668f79
BlockerPanel splits into a pure pager (mods/blockerPager, tested), us…
Zorro909 Nov 5, 2026
12db4d5
The list of sources, and adding and removing one, live in useCatalog …
Zorro909 Nov 5, 2026
6b8efa3
useModsCatalog, useModProfiles and useModOperations: typed views of t…
Zorro909 Nov 5, 2026
59ffbaa
The Mods tab's view, its one dialog and the row to focus are a tested…
Zorro909 Nov 5, 2026
24c73d4
Every backend command rejects with a CommandError: the payload's code…
Zorro909 Nov 5, 2026
09690c7
A refused download link says which rule it broke: UrlProblem is an en…
Zorro909 Nov 5, 2026
c71f232
A mod whose stored copy is gone says so (new code ModStoreMissing) an…
Zorro909 Nov 5, 2026
bb2974a
Document what reconcile's hashing costs, and why size and time don't …
Zorro909 Nov 5, 2026
8dbf93a
Docs: the schema names the one provenance kind shown and what a whole…
Zorro909 Nov 5, 2026
8f02ae5
AUTHORING.md: the path for mod authors from a first release to the si…
Zorro909 Nov 5, 2026
991955b
The page behind an open dialog is inert, all but the polite live regi…
Zorro909 Nov 5, 2026
052a339
A very long installed name has the row's width, with the switch and R…
Zorro909 Nov 5, 2026
4f07ecc
The Mods list and Browse grid sit on a near-opaque backing: the field…
Zorro909 Nov 5, 2026
abb8fdc
Home's instance card is short: Open folder sits on its title line (in…
Zorro909 Nov 5, 2026
0bacfe3
The Mods page's Installed and Browse tabs keep their size when select…
Zorro909 Nov 5, 2026
a796933
The selected Mods view is a dark tab with an amber underline, its cou…
Zorro909 Nov 5, 2026
ec214b4
INDEX_FORMAT states each rule once: field values in the table, validi…
Zorro909 Nov 6, 2026
f9023ba
AUTHORING is the one publishing guide: it gains the noMods table, the…
Zorro909 Nov 6, 2026
8e64423
PROPOSAL describes the current mod manager design: engine constraints…
Zorro909 Nov 6, 2026
b5e3fe2
Schema descriptions are one or two sentences that point at INDEX_FORMAT
Zorro909 Nov 6, 2026
b907555
The archived mockup has one short banner and one-line captions
Zorro909 Nov 6, 2026
d886b56
The README's mod section is short: the mock's scenario and flag table…
Zorro909 Nov 6, 2026
44ccfb3
Sources links a source with no mods to AUTHORING's troubleshooting se…
Zorro909 Nov 6, 2026
353b382
INDEX_FORMAT: the Fields table's values are enforced, hash formats ar…
Zorro909 Nov 6, 2026
26d6e5c
AUTHORING says which no-mods reason the Sources dialog shows, without…
Zorro909 Nov 6, 2026
4785191
PROPOSAL drops the command list (the bindings test already covers eve…
Zorro909 Nov 6, 2026
c1db6a2
The shared provenance description fits both the index and a source's …
Zorro909 Nov 6, 2026
333aa92
dev-mock: each fail value names its own effect
Zorro909 Nov 6, 2026
a1614b3
README's clippy command is the one CI runs
Zorro909 Nov 6, 2026
16cb5cd
The mock's too-large error names the real 512 MB limit
Zorro909 Nov 6, 2026
3d66f27
INDEX_FORMAT: key rotation states the one-key limit instead of an ove…
Zorro909 Nov 6, 2026
0ccd9a4
PROPOSAL: the provenance paragraph separates the index's record from …
Zorro909 Nov 6, 2026
1dd7d2b
README: the Mods paragraph splits basic use from alternative sources
Zorro909 Nov 6, 2026
ffd2adc
Remove the archived design mockup
Zorro909 Nov 6, 2026
d691c61
The mod index lives at Weft-Rust/weft-mods-index: repo links and the …
Zorro909 Nov 7, 2026
1ff3268
The empty Installed view drops its explanatory hint, and the ABI chec…
Zorro909 Nov 7, 2026
3a73656
A downloaded build is flushed through a writable handle before it ent…
Zorro909 Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: CI

on:
pull_request:
push:
branches: [master]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
rust:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

# Windows and macOS need nothing beyond the toolchain.
- name: Install Tauri system packages
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf

- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
with:
toolchain: stable
components: rustfmt, clippy

- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: src-tauri

# tauri::generate_context! wants the built frontend to exist.
- name: Placeholder frontend
shell: bash
run: mkdir -p dist && touch dist/index.html

- name: Format
working-directory: src-tauri
run: cargo fmt --check

- name: Clippy
working-directory: src-tauri
run: cargo clippy --all-targets -- -D warnings

# Includes bindings_are_current (fails if src/mods/bindings is out of date) and the shared
# rule cases in src/dev/fixtures/rules.json (rules_fixture.rs).
- name: Test
working-directory: src-tauri
run: cargo test

frontend:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: npm

- run: npm ci

# On its own, so a missing translation or a sentence assembled from pieces is labelled as
# such, not as a failed build (the build runs it again).
- name: Check translations
run: npm run check:i18n

# check:i18n, tsc and vite build
- name: Build
run: npm run build

- name: Unit tests
run: npm test

ui:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: lts/*
cache: npm

- run: npm ci

- name: Install Chromium
run: npx playwright install --with-deps chromium

# Every mock scenario in all four languages at 960×565, plus the safety and keyboard flows.
- name: UI tests
run: npm run test:ui

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: failure()
with:
name: playwright-traces
path: test-results/
retention-days: 7
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ desktop.ini
# Logs
logs
*.log
*.bak
*.bak
test-results/
playwright-report/
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ The builds aren't code-signed yet:

**Versions** -> **Install** -> back to **Home** -> **Play**. Delete a version with a double click on **Delete** (blocked while the game is running).

**Mods** -> pick the game version -> **Browse** -> **Install**. Each game version keeps its own mods. Mods are native programs that run with your privileges.

Besides the signed [Weft mod index](https://github.com/Weft-Rust/weft-mods-index), you can add GitHub repos under **Sources** or install a zip **From file**. Builds pinned in the index are Reviewed; others are Unreviewed or Unverified. See the [mod manager design](docs/mod-manager/PROPOSAL.md).

## Data

- **Game data** (worlds, settings): `%APPDATA%\Weft` - the launcher never reads or writes it.
- **Launcher data**: `%APPDATA%\dev.weft.launcher` - `versions/`, `logs/<tag>.log`, `cache/`, `downloads/`.
- **Game data** (worlds, settings): `%APPDATA%\Weft` - the launcher never writes it, and only lists its `mods` folder to warn about mods it can't manage.
- **Launcher data**: `%APPDATA%\dev.weft.launcher` - `versions/`, `logs/<tag>.log`, `cache/`, `downloads/`, and for mods `mods/store/` (downloads by SHA-256) and `profiles/<tag>/` (each version's mod set).

Newer versions may write saves older ones can't open - back up `saves` before downgrading.

Expand All @@ -40,7 +44,24 @@ npm run tauri dev # dev mode (plain `cargo run` won't work - UI is served by
npm run tauri build # installer
```

Before a PR: `cargo fmt --check && cargo clippy` in `src-tauri`, and `npm run build`. UI strings go through `src/i18n` - add keys to all four languages (en, pt-BR, es, ru).
Before a PR:

- `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test` in `src-tauri`.
- `npm run build`. It runs `npm run check:i18n` first.
- UI strings go through `src/i18n`: add every key to all four languages (en, pt-BR, es, ru), with the plural forms each needs. A key built from a prefix goes in the checker's `DYNAMIC` list.
- `npm test`: the unit tests (vitest).
- `npm run test:ui`: the UI tests (Playwright, `e2e/`). Run `npx playwright install chromium` once first, or point `PW_CHROMIUM` at an installed Chromium. Each run starts its own mock server on a free port; `PW_PORT=<port>` fixes the port, and `PW_REUSE=1` with it tests a server you already started.
- After changing an IPC type or command, regenerate `src/mods/bindings` with `cd src-tauri && cargo test export_bindings -- --ignored`. CI fails until it is committed.

CI (`.github/workflows/ci.yml`) runs all of these on every pull request.

### UI without Tauri

```bash
npm run dev:mock # http://localhost:1420/?scenario=mods
```

Runs the UI in a browser against a fake backend that follows the real backend's rules. Scenarios and URL flags are in [docs/dev-mock.md](docs/dev-mock.md).

## Releasing

Expand Down
60 changes: 60 additions & 0 deletions docs/dev-mock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Mock backend

`npm run dev:mock` serves the UI at http://localhost:1420/?scenario=mods against a fake backend
(`src/dev/mockBackend.ts`). The mock never ships in a production build.

Its rules are ported from the Rust backend (`src/dev/mockRules.ts`), and both are checked against
the shared cases in `src/dev/fixtures/` (`npm test`, `cargo test`). File system and network failures
are only simulated as the error code they end in; Rust tests them against real files and a local
HTTP server.

Pick a state with `?scenario=` and adjust it with flags, which combine:
`?scenario=mods&index=cached&revoked=cogwork&slow=1`. Flags in the URL override the ones a scenario
sets. Both lists come from `src/dev/scenarios.ts`; `npm test` fails if this page misses one.

## Scenarios

| Scenario | What it shows | Flags it sets |
|---|---|---|
| `mods` | The default: a mod built for another version (with a fitting update), an update, an off prerelease, a source, a local file and a file in the player's own folder. | — |
| `empty` | The newest version has no mods; the older one has mods to copy. | — |
| `fresh` | No game version installed. | — |
| `offline` | No network: the cached index and a source's cached mods. | `index=cached` `source=offline` |
| `crash` | The game stops while loading Structures, which has an update. | `crash=loadFailed` |
| `knownfile` | From file picks a catalogued build (Cogwork 0.2.0). | `file=known` |
| `revoked` | The installed Cogwork build is revoked. | `revoked=cogwork` |
| `ratelimited` | GitHub rate-limits the source; its last mods are shown. | `source=ratelimited` |
| `indexdown` | No index on the server (404) and none saved: no Reviewed mods. | `index=unavailable` |
| `stale` | The index signature is over 30 days old. | `index=stale` |
| `invalid` | The new index's signature doesn't verify; the last good one is shown. | `index=invalid` |
| `rollback` | The new index is older than one already accepted; the last good one is shown. | `index=rollback` |
| `missingdep` | Hearth needs Cogwork (off); Lanterns needs Glowmoss (not installed); Railways needs Signals (no build). | — |
| `lateblock` | Play itself finds Cogwork revoked; Hearth, which needs it, blocks the start. | `prelaunch=revoke` |
| `long` | Very long mod names, ids and repos. | `long=1` |
| `shadowed` | Cogwork is revoked and the player's own folder has a copy: Play is blocked until that file goes. | `revoked=cogwork` `shadow=cogwork` |
| `slowrefresh` | A refresh revokes Cogwork at once while the source lantern-dev/lanterns takes three seconds. | `slowsource=lantern-dev/lanterns` `refreshrevokes=cogwork` |
| `chain` | Chimneys → Hearth → Cogwork, both installed and off; Junctions → Railways → Signals, which has no build. | `chain=1` |

## Flags

| Flag | Values | Effect |
|---|---|---|
| `index` | `fresh`, `stale`, `cached`, `unavailable`, `unreachable`, `invalid`, `rollback`, `unsaved` | State of the signed index. `unavailable`: none saved, server answers 404. `unreachable`: none saved, no network. `unsaved`: fresh but not saved (`cacheError`). |
| `source` | `ok`, `notfound`, `ratelimited`, `offline` | State of the added GitHub source. |
| `nomods` | `noReleases`, `noModFiles`, `unreadable`, `noDigest`, `disagrees`, `invalid` | The source has no usable mods, for this reason. |
| `race` | `stale`, `error` | The first refresh answers last, with an older catalogue or an error. The page keeps the newer answer. |
| `slowsource` | `<repo>` | That source takes three seconds; the index is shown before it answers. |
| `refreshrevokes` | `<mod id>` | The next refresh's index revokes that mod's installed builds, once. |
| `revoked` | `<mod id>` | Revokes that mod's installed builds. |
| `abi` | `error` | The version's ABI can't be found. |
| `fail` | `install`, `network`, `toolarge`, `dep`, `meanwhile`, `catalog`, `sources` | Comma-separated. `install`: the hash check fails. `network`: the download breaks off midway. `toolarge`: the zip is refused before downloading. `dep`: a dependency's download fails. `meanwhile`: a newer Glowmoss appears after the first plan, so that plan is refused and planned again. `catalog`: the catalogue fails to load. `sources`: the sources list is damaged. |
| `file` | `unverified`, `known`, `notamod`, `revoked`, `revokedlate`, `wrongplatform` | What From file picks; `revokedlate` is revoked during the install. |
| `shadow` | `<mod id>` | The player's own folder has a copy of that mod. Tests delete it with `window.mockPlayer.deleteFile(name)`. |
| `revocations` | `messy` | Uppercase hashes, no reason or time, and one entry that isn't a hash: only that one is dropped. |
| `chain` | `1` | Adds Chimneys and Junctions. |
| `crash` | `wrongVersion`, `loadFailed`, `notAMod`, `wrongName`, `missingDep`, `initPanicked`, `exit` | The game stops with this failure; `exit` names no mod. |
| `prelaunch` | `revoke`, `storemissing` | Play finds Cogwork revoked, or the store's copy of Structures gone (`ModStoreMissing`). |
| `long` | `1` | Adds very long names, ids and repos. |
| `slow` | `1` | Everything takes four times as long. |
| `fast` | `1` | Everything takes a quarter as long. |
| `profile` | `fail`, `failafterplay` | Reading a version's set fails: always, or only the first read after Play. |
83 changes: 83 additions & 0 deletions docs/mod-manager/AUTHORING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Publishing a Weft mod

For mod authors: how to ship a mod the launcher can install, first from your own GitHub repo
(players see it as **Unreviewed**), then in the signed Weft mod index (**Reviewed**). The exact
rules are in [INDEX_FORMAT.md](INDEX_FORMAT.md); this page is the path through them.

## 1. The zip

One `<id>.zip` per build, where `<id>` is your mod id: `[a-z0-9_]{1,64}`, not `base`, and equal to
your crate name (the engine refuses a mismatch). Inside, nothing but:

```text
<id>.dll Windows
lib<id>.so Linux
lib<id>.dylib macOS
assets/… textures, lang files, and assets/textures/gui/icon.png
```

No other files at the top, no folders above these, no `..`, absolute paths or links. Ship the
libraries for the platforms you build, and list them in `platforms`. Limits: 512 MB, 20,000 entries
and 1 GB unpacked ([all limits](INDEX_FORMAT.md#size-limits)).

## 2. The release

- **Tag** each build `<id>-v<version>`, for example `lanterns-v1.2.0`, to keep several mods in one
repo apart. Any tag works if it is 1–100 letters, digits, `.`, `_` or `-` and doesn't start with
`.`.
- **Version** is semver (`1.2.0`, `1.3.0-beta.1`). A prerelease on GitHub is a prerelease in the
launcher too.
- Make the release (not a draft) with `<id>.zip` and `<id>.mod.json` as its assets. GitHub records
a SHA-256 digest for each asset; a zip without one is skipped.
- A new build is a new version and a new release, every time. If two releases share a version or
a zip hash, the older one's copy is skipped.

## 3. `<id>.mod.json`

Describe the build exactly as GitHub has it: the example and every rule are in
[INDEX_FORMAT, `<id>.mod.json`](INDEX_FORMAT.md#idmodjson-for-unreviewed-sources), and
[`schema/mod.schema.json`](schema/mod.schema.json) checks the shape. The fields that trip people:

- `asset.url` is the zip's `browser_download_url`, character for character, and `asset.size` and
`asset.sha256` are the zip's.
- `weft.abi` is the FNV-1a-64 hash of the raw bytes of `crates/abi/src/lib.rs` at the Weft commit
you built against, as 16 lowercase hex digits. Players get your build only on Weft versions with
the same ABI.
- `deps` lists every mod yours needs to load, by id.
- `provenance` from your own repo is shown as your claim, never as a record: leave it `null`, or
describe your attestation (step 4).

Then add your repo in the launcher (Mods → Sources).

### When a source shows no mods

The Sources dialog shows one reason: that of the release that passed the most checks. The checks run in this order:

| Reason | Means |
| --- | --- |
| `noReleases` | The repo has no published releases. |
| `noModFiles` | No release has an `<id>.zip` together with its `<id>.mod.json`. |
| `unreadable` | An `<id>.mod.json` couldn't be read as JSON, or isn't a description. |
| `noDigest` | GitHub has no SHA-256 digest for the zip, so nothing could be checked. |
| `disagrees` | The description doesn't match GitHub's record of the release: tag, repo, asset name, URL, size or hash, or the id isn't the zip's. |
| `invalid` | It matches, but breaks a [rule every entry follows](INDEX_FORMAT.md#rules-the-launcher-enforces). |

## 4. Into the signed index

A build in the index is **Reviewed**: someone read it, and the index signature pins its bytes, so a
changed release can't change what players install.

1. Build in GitHub Actions and attest the zip with
[`actions/attest-build-provenance`](https://github.com/actions/attest-build-provenance), from a
workflow in `.github/workflows/`.
2. Open a pull request on `Weft-Rust/weft-mods-index` that adds the version to your mod's entry
(the mod too, the first time): the same fields as your `<id>.mod.json`, with `source.commit`
the full commit the release was built from.
3. A reviewer reads the source at that commit, runs `gh attestation verify` on the zip, and adds
`reviewed` and `provenance`. The launcher shows a build record only when that record is complete
([INDEX_FORMAT, Provenance](INDEX_FORMAT.md#provenance)).
4. Once merged, CI signs the index and Pages serves it; players get it on their next refresh.

If a build turns out to be harmful or broken, ask for its `sha256` to be revoked, with a reason.
Launchers learn of it when they next read the index, and turn the build off the next time its
version's mods are read or before Play. A running game keeps it until it stops.
Loading
Loading