Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
68 changes: 62 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ game-binary constraints now live with the runtime, in
first-run Welcome onboarding, and in-app self-update for the Windows
installer plus Linux AppImage (Velopack).
The app is user-usable:
create profiles, import mods (folder/archive, Nexus/Untracked) or link an
create or clone profiles, import mods (folder/archive, Nexus/Untracked) or
link an
external mod folder without copying it, manage
the mod list (enable/disable/reorder/policy/remove), configure Settings
(discovery paths + mod-repo location), download Nexus mods ("Mod manager
Expand Down Expand Up @@ -233,6 +234,25 @@ src/ Modificus Curator -- the mod manager app (.NET 10 + Avalonia 12)
runs) + reloads from the authoritative active id; a
staged draft is edited in place (the persisted profile is
not mutated until Save); Add Profile starts a blank draft;
Clone profile (between Add and Delete, shown only for an
active persisted profile) copies it through the focused
`IProfileCloner.CloneProfile` in one persisted operation
(new id + creation timestamp, the stable non-localized
` (Copy N)` family name -- a copy of `Testing (Copy 1)`
belongs to the `Testing` family, highest-existing-plus-
one, never gap reuse, case-insensitive, never colliding
with an existing readable name; description, complete mod
membership incl. enabled/order/locks/policies with pinned
version ids, + launch settings; mod files stay shared in
the repository + `staged/` starts empty, rebuilt by
ordinary launch staging), then requests the clone active
+ reloads it into the clean editor (immediate +
non-destructive, no confirmation; the new banner name is
the success feedback; dirty edits resolve through the
same Save / Don't save / Cancel transition first; a
failure keeps the source active + unchanged and surfaces
only the localized generic error; no `ProfileCreated`, so
no DMF offer ever fires for a clone);
Save existing calls the atomic `UpdateProfile`, Save new
calls `CreateProfile(name, description, launchSettings)` +
requests it active; Save is disabled while metadata or
Expand All @@ -244,9 +264,11 @@ src/ Modificus Curator -- the mod manager app (.NET 10 + Avalonia 12)
atomic write as the Save button and proceeds only on
success; Don't save reloads authority and proceeds);
running-state gates
disable switching/Add/Delete while Darktide runs (active-
profile metadata + launch-settings edits stay enabled);
a draft hides the shared Add/Delete action row AND
disable switching/Add/Clone/Delete while Darktide runs
(active-profile metadata + launch-settings edits stay
enabled; Clone carries `ToolTip.ShowOnDisabled` so the
disabled reason stays readable);
a draft hides the shared Add/Clone/Delete action row AND
disables Add at the command level (defense in depth so
a programmatic call cannot start a second draft);
after a successful create + activation,
Expand Down Expand Up @@ -1410,6 +1432,13 @@ src/ Modificus Curator -- the mod manager app (.NET 10 + Avalonia 12)
directly from its external folder, no version
resolution; the focused ProfilesRoot read feeds the
game-dir ownership prefix check) + SetModPolicy transitions + the
focused IProfileCloner capability (CloneProfile: one
persisted copy of the aggregate with a new id +
timestamp + the stable ` (Copy N)` family name,
complete mod membership + launch settings, an empty
staged/ scaffold, no ProfileCreated; ProfileService
implements it alongside IProfileService, both DI
interfaces forwarding to the same singleton) + the
profile-scoped load-order lock (ModListEntry.OrderLocked:
a locked entry keeps its exact zero-based index across
SetModOrder, so a reorder projects the requested ordering
Expand Down Expand Up @@ -1899,6 +1928,18 @@ src/ Modificus Curator -- the mod manager app (.NET 10 + Avalonia 12)
container keeps the resolved latest, unreferenced
superseded versions still dropped, empty-container
removal unchanged)
+ ProfileCloneTests: the clone contract --
the exact persisted copy across a fresh service
instance (description, every mod-entry field,
every policy kind, every launch setting, new id +
creation timestamp), the empty staged/ scaffold
vs. a staged source, independence in both
directions, the full generated-name table
(copy-of-copy, case-insensitive, a numbering gap,
a nonnumeric suffix), the unknown-source throw
creating nothing, no ProfileCreated, + the
IProfileService/IProfileCloner same-singleton
DI mapping
Modificus.Curator.Mods.Tests/ xUnit tests for the mod repository + import
(incl. the linked-folder add + linked-container prune,
+ the display-metadata AddVersion/Import pass-through
Expand Down Expand Up @@ -2098,7 +2139,18 @@ src/ Modificus Curator -- the mod manager app (.NET 10 + Avalonia 12)
ProfilesViewModelTests (profile create/save/cancel/
delete/switch, no-active states, running-state gates,
dirty navigation, banner/picker, inline launch-settings
validation + atomic save, DMF prompt timing after create)
validation + atomic save, DMF prompt timing after
create; + the clone flow: active-only visibility +
draft/no-active/running gates with direct-invocation
refusal, clean success (one clone + one activation +
authoritative reload + clean editor), the Save /
Don't save / Cancel dirty branches, the localized
generic failure preserving the source, + the clone
tooltip's culture refresh) + ProfilesViewXamlTests
(the Add/Clone/Delete action-row order, the clone
command/visibility/tooltip bindings +
ShowOnDisabled, the drawn content-copy Path icon +
the localized label, the clone resx keys)
+ the LaunchSettingsEditorViewModelTests (existing-
settings load, add/remove rows, inline localized
validation -- empty/`=`/NUL name, NUL value,
Expand Down Expand Up @@ -2606,7 +2658,11 @@ dotnet run --project src/ui --configuration Release # app shell window
`UpdateProfile(id, name, description, launchSettings)`: the editable write
boundary; ordered env-var entries + game args; validated up front via the
shared `LaunchSettingsValidator`, applied at launch; `GetLaunchSettings` is the
focused read the launch path uses),
focused read the launch path uses) + the focused `IProfileCloner.CloneProfile`
capability (one persisted copy of the profile aggregate with a new id +
timestamp + the stable ` (Copy N)` family name, complete mod membership +
launch settings, mod files staying shared in the repository + an empty
`staged/` scaffold, no `ProfileCreated`),
**Steam** (Steam + Darktide + Proton discovery via Steam's CompatToolMapping with the appinfo recommended-runtime fallback + the automatic/manual mode policy + `Rediscover` + `IsGameRunning`),
**Integrations** (the Nexus v1 client/auth +
`IModAcquisitionService` the download + extract + place orchestrator +
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ and it runs unmodified).
profiles, dependency resolution, mod-source integrations). The backend
libraries (Profiles, Mods, Steam, Integrations, Relay-client, General) and
the UI (a left navigation rail with five destinations: Profiles, Mods, Nexus
Integrations, Preferences, Settings; plus profile management and the Launch
Integrations, Preferences, Settings; plus profile management, including
cloning a profile into an independent copy (named "Copy N", with mod files
shared rather than duplicated), and the Launch
flow) are in place. The app is user-usable. The Mods list offers a persisted
Compact and Detailed row density; Detailed rows show a Nexus summary and a
cached thumbnail when one is available, with a neutral placeholder otherwise.
Expand Down
12 changes: 12 additions & 0 deletions docs/architecture/MODIFICUS-CURATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,18 @@ for the full contract (env-var table, logging, the hook-ready handshake).
update of an entry already in the profile is a strict no-op that preserves
the user's current order, enabled state, policy, and lock. Existing profile
entries are not migrated.
- **Cloning:** the focused `IProfileCloner.CloneProfile` capability persists an
independent copy of a profile in one operation (never reconstructed through
repeated `AddMod`, which would replay fresh-add policy such as DMF
placement). The copy gets a new id + creation timestamp, a generated
` (Copy N)` family name (stable data, never localized; `Testing (Copy 1)`
belongs to the `Testing` family), and the source's description, complete mod
membership (enabled state, order, locks, policies incl. pinned version
ids), and launch settings. Mod files stay in the shared repository (the
clone references the same containers; nothing is duplicated), and the
staged tree is not copied: the clone receives an empty `staged/` scaffold
that ordinary launch staging rebuilds. Cloning raises no
`ProfileCreated`, so the DMF offer never fires for a clone.
- Mods are stored **once, in a unified repository** keyed by `(source, identity)`
per UUID container. Profiles reference a mod by `(containerId, policy)` and
store no mod files of their own. See [Mod repository](#mod-repository).
Expand Down
38 changes: 37 additions & 1 deletion docs/architecture/ui-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ a UI-layer singleton that the shell (and other view models) inject:
│ │ strip; mirrors session state
│ │
│ ├── ProfilesViewModel ─── the active profile editor (name + description
│ │ │ + inline launch settings) + banner/picker
│ │ │ + inline launch settings) + banner/picker/clone
│ │ │
│ │ └── LaunchSettingsEditorViewModel the reusable inline launch-settings
│ │ rows (env vars + args + toggles)
Expand Down Expand Up @@ -195,6 +195,42 @@ running-state change does not reload the list (the list stays put while the
game runs; edits land on the profile the user will launch next). Active-id
changes rebuild the list from the new profile.

## The Profiles destination (`ProfilesViewModel`)

The Profiles page edits the active profile only (name + description + the
inline launch-settings editor), hosts the persisted-profile banner + picker,
creates new drafts, and carries the Add / Clone / Delete action row for the
active persisted profile. Every voluntary active-profile change routes
through the session's `RequestActive` gate; the page never writes the
persisted profile outside the atomic `UpdateProfile` / `CreateProfile`
boundary.

**Cloning** (the Clone action, between Add and Delete) copies the active
persisted profile through the focused `IProfileCloner` capability:

- **Gating.** Clone is visible only for an active persisted profile and
disabled while a new draft is open, while a Save is in flight, and while
Darktide runs (the clone becomes active, which is a profile switch). All
gates are re-checked in the command body after the dirty-transition await;
the disabled button keeps its tooltip (`ToolTip.ShowOnDisabled`) so the
running-state reason stays available.
- **Dirty guard.** A dirty editor resolves through the same unsaved-changes
transition as navigation/switch/Add: Save persists the edits then clones
the saved profile, Don't save discards the edits then clones the previously
persisted profile, Cancel/ESC/X creates nothing and preserves the edits.
- **Activation.** On success the returned clone is requested active through
the session (with the page's own-reload suppression so the session event
handler does not treat it as an outside displacement), then one
authoritative reload opens the clone in a clean editor. Cloning is
immediate + non-destructive, so it needs no confirmation; the new banner
name is the success feedback.
- **DMF suppression.** `CloneProfile` raises no
`IProfileService.ProfileCreated` (the blank-profile signal behind the DMF
offer), so cloning a profile never queues the DMF prompt.
- **Failure.** An expected clone/read/write failure logs, keeps the source
active + unchanged, and surfaces the localized generic clone error in the
page's fixed error area; raw exception text is never shown.

## The shell (`ShellViewModel` + `MainWindow`)

The shell owns navigation across five hosted destinations (the
Expand Down
Loading