Skip to content

Mod Manager for Weft - #3

Closed
Zorro909 wants to merge 159 commits into
Weft-Rust:masterfrom
Zorro909:feat/mod-manager
Closed

Zorro909 wants to merge 159 commits into
Weft-Rust:masterfrom
Zorro909:feat/mod-manager

Conversation

@Zorro909

@Zorro909 Zorro909 commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

This change adds a Mod Manager to the Launcher, which can download and install Mods from publicly hosted indexes (like Zorro909/weft-mods-index, which should be moved to Weft-Rust/weft-mods-index)

For mods to be added, they need to be vetted by a Maintainer (a rough security check). By default builds done with Zorro909/weft-mod-action will have their builds code attested using standard Github features.

That way, the user can be presented with a rough overview of the risk.
Mods are always version-specific, so they are only enabled, when their corresponding version is launched.

Installing local mods is possible using the "From file" button on the mods page, useful for modders or mods that aren't on the index yet.

Screenshots can be viewed in the suggestions-thread: https://discord.com/channels/1566072542412673104/1567585976954527866

The proposal, the HTML mockup, and the index format the launcher will
read from Zorro909/weft-mods-index.
Each installed game version gets its own mod set, handed to the game
with --mods. A mod that is off is left out of that folder, because the
engine runs code even from mods it was told to skip.

Mods come from the signed Weft mod index (Reviewed), from GitHub repos
the player adds (Unreviewed), or from a zip on disk (Unverified, with a
typed confirmation). Every download is pinned by SHA-256 and its zip
layout is checked before it is stored. Play is refused when a turned-on
mod was built for another game version, and when the game still exits
while loading mods, Home names the mod from the log.

Also: a shared download module for versions and mods, an i18n key check
that runs before every build, and a mock backend (npm run dev:mock) that
runs the UI in a browser.
Embed keys/index.pub of Zorro909/weft-mods-index and keep a signed copy
of the live index as a test fixture, so a wrong key or a contract check
that drops real entries fails the tests.
The proposal names the modules, commands and tier as they are in the
code and lists what shipped; the README covers the Mods tab, where mod
data lives, and the browser mock.
FailureKind is now an enum. An init panic takes its mod from the mod's own
"[mod:X] panicked" line before it, or the nearest "loading mod X". A
library path with spaces in it still gives the mod id. Every failure kind
has a fixture in the launcher's log format.

Adds ts-rs and url, and tempfile and http for tests.
Structure
- Rules on plain data: plan.rs (install plans, dependency resolution, the
  enable cascade, removal, copy preview) and profile.rs (statuses, updates,
  revocations). store.rs, install.rs and inspect.rs do the disk work on a
  Paths root; service.rs holds the steps that need the app; commands.rs is
  argument checks and locking.

Safety
- Asset URLs are parsed: https, github.com, segments exactly
  [owner, repo, releases, download, tag, <id>.zip], owner/repo matching the
  source without case, canonical form, no backslash or percent-encoding.
- Garbage collection deletes nothing if a lock file can't be read, and skips
  store objects an install has reserved. Downloads use unique temp files.
- sha256 arguments are checked at the command boundary; the id "base" is
  reserved (ModIdReserved).
- The index is parsed entry by entry: a bad mod, version, channel or platform
  costs only that entry. Index versions need a "reviewed" record.
  provenanceVerified is true only for provenance from the signed index.
- The verified index and its signature are cached as one generation; a pair
  that no longer verifies falls back to the previous one.
- The enable cascade and plans refuse revoked dependencies.
- Mod changes check "game running" again inside the mods lock, and
  launch_version holds that lock from the pre-Play check until the game is
  registered as running.
- Download errors keep their meaning (ModDownloadNotFound, ModTooLarge,
  DiskFull, RateLimited, Network). <id>.mod.json is read with a streaming
  64 KB cap, and source releases are paged (5 x 100).

Contract
- New commands plan_mod_install and preview_copy_mod_profile.
- Updates keep the on/off state; copies merge into a populated set; a
  catalogued local file installs from its own bytes; sources store GitHub's
  spelling and compare without case; SourceStatus carries freshness, checked
  time and the refresh error next to cached data; ModsBlocked names mods;
  CopyResult.leftBehind is CopyItem[].
- AppError.code is an ErrorCode enum app-wide, with the same wire strings.
- Every IPC type, the command names and the event names are generated into
  src/mods/bindings; bindings_are_current fails when they are stale.
The examples are complete, valid JSON, and a test parses them with the
launcher's own code. The source section is now "Unreviewed sources", with a
full <id>.mod.json (asset.url, absolute icon) and every agreement check
sources.rs makes. A "Launcher behaviour" section covers size limits,
rollback refusal, the cache, skipped entries, the URL rule and the required
review. The tag rule matches the code.
The on-disk layout and module table match the code. The trust table has
three tiers; Revoked is a status any build can have, and Unverified means a
local file only.
Closed unions for Platform, ProgressStage, FailureKind and ErrorCode,
SourceStatus, provenanceVerified, the full catalogue build in a file's
catalogue match, CopyItem left-behind entries, and the new
plan_mod_install and preview_copy_mod_profile commands. Command and event
names live in one module used by the API, the hooks and the mock.
A numeric prerelease identifier now ranks below an alphanumeric one
(1.0.0-2 < 1.0.0-1a), as in mods::version. Repos compare without case.
Adds stable installed-list ordering, copy donors and repo parsing, with
tests using the backend's vectors.
t() takes keys derived from en.json, so a typo fails tsc. Counted
messages use Intl.PluralRules with _one/_few/_many/_other forms. Sentence
fragments become whole sentences with placeholders. Russian uses one
glossary: Reviewed Одобрен, Unreviewed Не проверен, Unverified Не
верифицирован. check-i18n also checks plural sets per language, keys used
in src, and that every backend error code has a message.
checks.ts builds the install dialogs' rows from the backend's plan: a
provenance claim nobody verified is unknown, GitHub's digest is not the
index's pin, dependencies say what will happen to each. status.ts holds
status lines, blocking fixes, crash resolution and result notices.
One ticketed coordinator per version drops stale profile answers.
Actions return done, failed or skipped, refuse a duplicate while one
runs, and hide key formats behind isBusy, isCopying and progressFor.
Notices queue per version. A crash notice stays until the blamed mod is
off, removed or replaced.
Updates, fitting builds and copied sets go through review dialogs with
the native-code warning. Home and Versions say when mods block a version
and offer one-click fixes. Dialogs keep their footer visible and their
focus inside; tablists use roving tabindex; quiet banners fold into one
row; truncated values are reachable by keyboard and hover.
Plans, cascades, revocation, merging copies, self-consistent known
files, three progress stages and URL flags that combine with scenarios.
SCENARIOS and FLAGS are one table, listed in the README and checked by a
test.
types.ts and ipc.ts re-export the bindings the backend generates, and
the i18n check reads error codes from the generated ErrorCode union.
A Playwright suite renders every mock scenario in all four languages at
960x565 and walks the install, launch-safety and keyboard flows. A CI
workflow runs fmt, clippy, cargo test, the build, unit tests and the UI
tests on pull requests.
The index module becomes format, signature, cache and fetch. The signing
time comes from the minisign trusted comment and is reported as signedAt.
Rollback protection compares (generated, signedAt) against the newest pair
ever accepted, kept in cache/mod-index.state.json so that deleting the
cached index doesn't reset it. An index signed more than 30 days ago is
stale but still used, and one dated more than a day ahead is refused.

Index versions must now have a semver version, full commit hashes and an
RFC 3339 published time, as INDEX_FORMAT already said; long summaries are
cut to 140 characters. The test fixtures are re-signed with a new
throw-away key so they carry timestamps.
The operations move from commands.rs into service.rs and take a Ctx
(paths, mods lock, reservations, held catalogue, running games,
progress), so they run in tests without Tauri. Every command is one call.

- Installs are staged, fetched and then committed under the mods lock,
  after checking that the version is still installed and not running, no
  build was revoked in the latest catalogue, and resolving again against
  the latest profile needs nothing more and changes nothing else.
  Otherwise the new ModChangedMeanwhile error, and nothing is written.
- A mod that needs a missing dependency is installed turned off
  (InstallPlan.turnsOn, ChangeResult.installedOff); copies follow the
  same rule per mod, and CopyPreview lists the dependencies, the mods it
  turns on and the ones that stay off.
- Revocations are recorded in the lock file and outlive the index.
  InstalledMod.depsNotInstalled tells a missing dependency from one that
  is off.
- remove_mod keeps the entry in removed.json, and its store file, until
  the version's next change; restore_mod puts it back.
- store::admit hashes the file itself and checks the zip and platform,
  also for a build already in the store; a damaged stored copy is fetched
  again. Lock files are validated on read.
- prepare_launch uses only the held or cached catalogue and cached ABI.
- forget_version takes the mods lock. Hashing and copying run in
  spawn_blocking.
- Redirects stay on GitHub hosts over https; icons are read with a
  512 KB cap and must be PNGs.
A source's <id>.mod.json is refused unless its id is a mod id. When
several releases publish the same mod, the newest release names it and a
version or zip published again later replaces nothing. Summaries are cut
like the index's, and the ABI file is read with a 1 MB cap.
commands.ts now maps every command to its arguments and result
(CommandSignatures), read from each command's Rust declaration, and every
event to its payload (EventPayloads). install-progress gets a typed
InstallProgress payload and GameStateEvent.state becomes running/stopped.

api.ts sends every command through call(), typed by that map; modsApi
keeps its names and gains restore, and launcherApi covers the version and
launch commands. listenTo() types event payloads the same way.
The engine's fatal mod errors are logged by the weft binary at ERROR, as
[weft], and an init panic reads '<id>: init panicked'. The fixtures now
use those lines, the mod of an init panic comes from that prefix, and
only ERROR lines are read as failures. The --mods argument gets a test.
Deleting it outside the mods lock could race garbage collection; the
fresh download's rename replaces it atomically instead.
INDEX_FORMAT lists what the parser requires and the default of every
other field, the rules it now enforces (semver, full commits, RFC 3339
dates, mod ids in sources) and what it doesn't, how source releases merge,
the signing time, stale, the rollback state file, redirects and the
store's checks. PROPOSAL drops its obsolete mod.json example for a link,
fixes the stale module, message and file-name details, and describes
turnsOn, remembered revocations and when they apply, the commit-time
checks and Undo. The doc tests read PROPOSAL's examples too.

The releases list client gets the same GitHub-only redirect policy.
If the catalogue refreshed during the download no longer lists the build,
the install fails with ModChangedMeanwhile rather than writing it.
The contract's new fields and the ModChangedMeanwhile code live in
types.pending.ts until the bindings catch up. asAppError no longer calls
an unrecognised failure Io, and modsApi gains restore and openLog.
…gned index, held to INDEX_FORMAT by a doc test
…on; it comes back before focus returns to the opener
…emove on the line under it: it wraps once, not into a narrow column
…'s bright seams no longer show between rows and cards
… view under a crash notice at 565px) and the details share one wrapping line; a crash's opened log line scrolls past four lines
…ed: the count is plain dark-amber text on the selected tab, not a padded amber block
…nt in bright amber (10.5:1): amber on the pale tab could not be both amber and readable
…ty in the rules, runtime behaviour grouped; size limits are a table; the publishing walkthrough and noMods table move to AUTHORING
… full tag rule and the ABI hash, and says when a revocation takes effect
…, architecture boundaries, trust and one Remaining work section, without the shipped notes, the stale repo name, the 64 MB figure or launcher-side attestation checks
…s move to docs/dev-mock.md, which the scenarios test reads
…ction; the mock's comments point at docs/dev-mock.md
…e a table, rollback without a signing time follows the pair order; the icon rule, cache paragraph and revocation sentence are shorter, and the publishing redirect section is gone
…ry registered command), the log templates and repeated zip and age rules; the plan-token and cancellation bullets are shorter, and diagnosis states the init-panic exception
…rlap recipe that doesn't work; the Fields intro points at the rejection rules; a source can't mark itself Reviewed; no dangling 'except reviewed'
…index URL (weft-rust.github.io/weft-mods-index/v1/) follow it
…k's detail just names the declared interface
…ers the store: Windows refuses to flush a read-only one (Access is denied)
@Zorro909
Zorro909 marked this pull request as draft November 7, 2026 01:20
@Zorro909 Zorro909 closed this Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant