Skip to content

chore(release): merge release/1.1.0 into main for 1.1.0 - #235

Merged
ModifAmorphic merged 11 commits into
mainfrom
release/1.1.0
Aug 21, 2026
Merged

chore(release): merge release/1.1.0 into main for 1.1.0#235
ModifAmorphic merged 11 commits into
mainfrom
release/1.1.0

Conversation

@ModifAmorphic

Copy link
Copy Markdown
Owner

Merges the release/1.1.0 line into main for the 1.1.0 release. The branch carries the squashed 1.1.0 feature PRs; this merge preserves those commits as-is (merge commit, no squash).

Commits included, oldest first:

ModifAmorphic and others added 11 commits August 17, 2026 15:58
…owngrade to latest stable

The production GithubSource was built with prerelease: true, so the
in-app updater offered pre-release versions to every user. Build the
feed source with prerelease: false and set AllowVersionDowngrade so an
install on a newer pre-release is offered the latest stable release
and self-heals onto the release track (Velopack applies it as a
full-package update).

Fixes #226
…owngrade policy

The update-source docs still described the prerelease-included feed and the options-less UpdateManager construction. The behavior change landed in the prior commit (fixes #226): the production GithubSource excludes pre-releases and the manager allows version downgrade to the latest stable release.
…through-filter

The Mods destination gains two view-projection features: a visibility
toggle that hides disabled mods and a search box filtering rows by name
(case-insensitive ordinal substring). The state is session-transient
(never persisted, survives reloads and navigation, clears on a profile
switch) and lives beside the authoritative full list as a VisibleMods
projection; the density coordinator keeps receiving the full snapshot so
thumbnails and metadata hydrate regardless of visibility.

Reordering works through the projection: Move Up/Down cross to the
adjacent visible unlocked row and drag targets are computed among
visible rows, then committed to the stored order through the generalized
ModReorderPlanner (locked rows keep their exact indices; hidden rows
never anchor the insertion, shift at most one slot, and keep their
relative order; all-visible input reproduces the previous planner
output). The view's gesture math now indexes the visible collection so
container lookups stay aligned with the ItemsSource.

The no-matches state (active filter/search + non-empty full list +
empty projection) is exclusive with the no-mods/add-hints empty state.
…through-filter

AGENTS.md, the UI architecture doc, and the UI reference now describe
the VisibleMods projection (session-transient filter/search state, the
exclusive no-matches empty state, full-snapshot hydration), the
visibility-aware ModReorderPlanner entry point and ReorderRequest rank
semantics, the toolbar search box + hide-disabled toggle, and the new
ModListFilterTests / ModReorderPlannerTests coverage.
Post-review polish: SearchText docs say empty/whitespace (the property
is non-nullable), HasSearchText docs match its whitespace-shows-clear
behavior, and the search TextBox loses an x:Name nothing referenced
(the clear affordance needed no code-behind).
…rectory (#229)

## Why

Curator served mods from profile staging
(`<profiles>\<id>\staged\mods\`)
outside the game directory. Mods that resolve game-directory-relative
paths
break under that shape: Simple Assets and Simple Audio cannot load the
DLL
they ship with and cannot resolve the darktide.exe binary. Fixes #230.

## What

- Mods now deploy into the Darktide game directory by default: each
modded
launch stages the profile, then points `<game>/mods` (an NTFS junction
on
Windows, a symlink on Linux) at the active profile's staged mods tree
and
hands Relay the game directory as `--mod-path`. The engine sees the real
  game-dir mods root, so mods that resolve game-directory-relative paths
  (for example SimpleAudio) work; vanilla launches from Steam remain
  untouched because nothing loads mods without Relay.
- Ownership of the game-dir mods slot is decided by a staging marker
  (`.curator.json`, rewritten every staging pass) plus a profiles-root
prefix check, never by the entry being a link. A foreign entry is never
deleted or modified: the launch returns `GameDirConflict` and the UI
asks
whether to rename it aside (`mods_<timestamp>`, collision-bumped,
receipt
  recorded in app-state, best-effort README inside) and retry once, or
cancel. A one-line notice reports the new folder name after the rename.
- New global experimental preference "Load mods from Curator's profile
directory (experimental)" restores the previous launch shape (mods
served
  from staging, Curator-owned game-dir link removed best-effort).
- Renamed-folder receipts persist in app-state (`RenamedModsFolders`).
- Docs updated across AGENTS.md, README, the architecture doc, and the
  reference docs: no patched game files, no copies, one self-identifying
  link, vanilla Steam launches stay vanilla.

## CI

- The PR gate runs on pull requests into any branch (previously main
only),
  so release-branch PRs get the same gate.
- The format job was simplified: a single checkout of the PR head, one
  unconditional format step, and one remaining conditional
  (workflow_dispatch runs format without committing).

## Verification

- `dotnet build` + `dotnet test` Release green across all eight test
  projects (2,067 tests); `dotnet format --verify-no-changes` clean.
- Claim ladder, mod-path switch, external-mode removal, conflict shape,
and
Linux path translation covered by real-filesystem tests;
junction-specific
  behavior additionally exercised on Windows by the PR matrix.
…lay at launch (#231)

Resolves #228.

## What changed

Relay v1.1.0 can replace its built-in mod manager via `--mod-manager
<file>`. Curator now supports that end to end:

- **Generic detection** (`IProfileService.GetActiveModManager`): an
enabled profile mod whose resolved staging target is a `base` folder
containing `mod_manager.lua` is the active alternate mod manager (the
Darktide Mod Loader family convention; e.g. AML, Nexus mod 246).
Detection is content-based, manager-agnostic, and shares the staging
resolver, so the answer always matches what `PrepareModRoot` stages. At
most one manager per profile (the existing base-name collision block).
Yields null when the manager file is missing from the resolved target:
Relay hard-refuses a configured-but-missing file, so Curator never hands
it one.
- **Staging and mods.lst are unchanged**: the manager mod stages and
lists like any ordinary mod. AML installs as `base/` with
`mod_manager.lua` + an empty `base.mod`; the empty descriptor passes the
existing import validation (a Vortex install marker), and AML-shaped
archives import with base name `base`.
- **Launch flag**: when a manager is active, `--mod-manager <path>` is
emitted immediately after the `--mod-path` pair (verbatim on Windows,
`Z:\`-translated on Linux). The value is projected onto the same
effective mod root `--mod-path` was formulated from: the game dir under
default hosting, the staged root under the external-hosting preference.
`RELAY_MOD_MANAGER` is now a reserved profile env-var name so a profile
cannot point Relay at a different manager than the staged one.
- **Mods-page banner**: a full-width, non-dismissible caution banner
between the import card and the row list is shown exactly while the
active profile has an enabled manager, reading "{name} is managing
ordering of mods. Changes to mod order in Curator will not be reflected
in game." Reorder and lock controls stay fully functional (Curator keeps
writing mods.lst as the working fallback the moment the manager is
disabled). Banner state is re-derived on every Reload and on the
enable-toggle path, from the same derivation the launch path consumes.
- **Docs**: AGENTS.md, architecture doc, reference docs (profiles,
relay-client, ui), and the README cover the detection rule, the flag
projection, and the banner.

## Tests

New coverage across Profiles (detection matrix: untracked/Nexus/linked,
disabled, unresolvable, missing file, capitalized `Base`, unknown
profile, ordinary staging), Mods (AML archive shape validates +
imports), RelayClient (flag presence/position/value on both platforms,
projection in both hosting modes, Z:\ translation, no-flag cases), and
UI (banner on/off/name formatting, toggle-driven refresh in both
directions).
Downloads from Nexus had no visible presence in the app: a "Mod manager
download" click gave zero indication while large mods downloaded for
many seconds, so users retried, which spawned console errors from the
nxm handler ("Curator is not running; launching it") plus duplicate
doomed launches, and a profile switch mid-download silently redirected
the mod. The nxm IPC accept loop was also processing each download
inline, so the single pipe instance stayed occupied for the whole
transfer and the handler misread a busy pipe as a dead one.

This gives Curator one serial download engine behind every Nexus
download, with downloads rendered as rows in the mod list.

- The nxm handler becomes an enqueue adapter: the accept loop's routed
handler gates (game domain, auth, active profile) and returns within
milliseconds; the pipe frees immediately. The handler exe gains a
cold-start pre-check so a burst of clicks with Curator closed launches
it once instead of five times, and the per-attempt connect timeout
doubles to 1s.
- A new download queue coordinator (`IModDownloadQueue`, ui/Session)
owns the pipeline: serial FIFO worker, dedupe by game domain + mod id +
file id (a second click joins and pulses the existing row), dequeue-time
auth recheck, and an exact local-repository hit check on the newly
persisted `ModVersion.FileId` so re-clicking an already-acquired file is
an instant reference-add with zero network.
- Downloads render as rows: one shared status template hosted three
times, morphing the target mod's row in place when it is visible,
appended below the list otherwise (new mods, cross-profile targets,
filtered rows), with progress, cancel, retry, and inline failure text.
Download rows never enter the visible-rows projection or the reorder
machinery.
- Version policy follows the clicked file: the head file tracks latest,
an older file pins to that version, and an existing profile entry gets
its policy rewritten so the click always wins. `ModVersion.IsLatest` now
keys on the remote publish timestamp instead of import recency, so
deliberately downloading an old version no longer re-points every
profile tracking the container (Fixes #232). The Nexus file id is
persisted on each version (both import branches, self-healing by
attrition).
- The DMF prompt's premium download and the premium mod-update path
(manual installs plus the opt-in automatic batch) now run through the
same queue; the separate update installer, its one-at-a-time
coordinator, and the update-cell spinner are deleted.
`UpdateCoordinator`, `IModUpdateInstaller`, and the busy-mirror row
state are gone; the queue's serial worker is the single install gate and
the row morph is the busy surface.
- The acquisition surface widened to serve the queue:
`NexusAcquisitionResult` carries the release tag and whether the file is
the head release, progress reports include the total from the response
content length, and `ResolveLatestNexusAsync` resolves the head file
without downloading.
- Docs updated end to end (AGENTS.md, README, docs/reference,
docs/architecture).

The old update-installer test semantics all found homes in the queue and
automatic-batch suites; nothing was dropped. Full solution: 2169 tests
passing, format clean.
## What changed

- New **updates-only** toolbar toggle on the Mods page: a
session-transient filter that narrows the row list to rows flagged with
an available update. It composes with the hide-disabled filter and the
name search (AND), feeds the same no-matches empty state, never
persists, and clears on an active-profile change.
- `ModListViewModel.Reload` now hydrates the persisted known-update
flags before rebuilding the visible projection (the projection reads
`UpdateAvailable`, and freshly built rows default it to false), and a
landed update check reprojects, so the filter tracks live flag changes.
- New toolbar button after the hide-disabled toggle: Material "update"
glyph, one stable shape with the `selected` class marking the active
filter.
- Six new `ModListFilterTests` cases: the projection + Reload-ordering
regression, AND-composition, live reprojection on a landed check,
session-transient lifecycle (survives reload, clears on profile switch),
no-matches/add-hint exclusivity, and the tooltip flip.
- Docs updated to match: `AGENTS.md`,
`docs/architecture/ui-architecture.md`, `docs/reference/ui.md`.
- Nexus page description copy refresh (vanilla-install callout, Linux
install/uninstall sections) and the `ModRow_UpdateTooltipOpenFiles`
rewording ("Newer version available. Open the Nexus files page to
download").

## Why

The per-row update-action cell already carries the flagged-update
signal, but with long lists the flagged rows scatter through the list.
The toggle surfaces them at the list level in one click.
@ModifAmorphic
ModifAmorphic merged commit 2596a51 into main Aug 21, 2026
4 checks passed
@ModifAmorphic ModifAmorphic linked an issue Aug 21, 2026 that may be closed by this pull request
@ModifAmorphic
ModifAmorphic deleted the release/1.1.0 branch August 21, 2026 18:05
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.

In-app update check offers pre-releases (1.0.0) Add already-imported mods to a profile without re-downloading (mod library view)

1 participant