Skip to content

Commit 5075cee

Browse files
feat(magos-modificus): Phase 3 Track B mod-list, import, source model (#29)
Fills the shell content area (Track A's empty placeholder) with the mod-list for the active profile, plus the mod source/version model that backs it. ## Backend model - `ModSource` (None / Nexus / GitHub) recorded on the shared entry; versions are raw strings everywhere (GitHub release tags and Nexus file versions are not SemVer). - Allocation is plain string equality, which removes the `// TODO(phase4)` version-equality wart. - `IModImportService` (folder or `.zip` import into the shared store). - `IModOrderResolver` + identity stub (the auto-sort seam; real dependency algorithm deferred). ## Mod-list UI - View / enable-disable / remove (confirmed) / reorder (up-down) / per-mod policy (Latest or Pinned to a version string), with a read-only source badge per row. - Auto-sort toggle (identity stub, no-op). ## Import flow - Split-button "Add Mod" (zip default, folder via the flyout) plus drag-and-drop; both feed a sequential per-mod modal collecting Source + Version + URL. - Drag-and-drop is wired but messaged as Windows-only (Avalonia 12.0.x lacks Linux XDND, wont-backport; PR AvaloniaUI/Avalonia#20926 lands it in 12.1). Folder import is reachable via the picker on all platforms. ## Tests + docs - 426 tests (backend model + services + VM logic against hand-rolled fakes; view mechanics are live-verified). - Reference, architecture, and AGENTS.md docs updated in place.
1 parent c921650 commit 5075cee

42 files changed

Lines changed: 4674 additions & 191 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@ Requirements, architecture, and technology choices are made fresh.
3131
merged as the production seed; Magos Modificus has the backend libraries
3232
implemented (Phases 1–2) and the Phase 3 UI under construction: Track A (the app
3333
shell + profile management: dropdown switch, persisted active profile,
34-
create/rename/delete dialog, switch-blocked-while-running) and Track D (global
34+
create/rename/delete dialog, switch-blocked-while-running), Track D (global
3535
Preferences: theme + font scale + language, with the i18n infrastructure:
3636
`Strings.resx` + `LocalizationService` for dynamic culture switching, all Track
37-
A UI strings backfilled to resource keys) are wired; mod-list UI (Track B) +
38-
Launch behavior (Track C) are still pending, and the Launcher is a stub
39-
(Phase 5).
37+
A UI strings backfilled to resource keys), and Track B (the mod-list UI: view
38+
mods with source/version badges, enable/disable, remove-with-confirm, reorder,
39+
per-mod Latest/Pinned policy, auto-sort identity stub, and local folder/`.zip`
40+
import via file picker + drag-and-drop, over the `ModSource` + raw-string version
41+
model + `IModImportService`) are wired; Launch behavior (Track C) is still
42+
pending, and the Launcher is a stub (Phase 5).
4043
- **`poc`** — historical proof-of-concept, reference only. Not built upon.
4144
- Development is branch + PR; no unreviewed merges to `main` (reviewed +
4245
covered + qa'd + CI green).
@@ -80,10 +83,15 @@ magos-modificus/ Magos Modificus — the mod manager app (.NET 10 + Avalo
8083
profiles/ Magos.Modificus.Profiles — profile data model, persistence,
8184
shared-first staging (ProfileService.PrepareModRoot builds the
8285
staged/ symlink projection + mods.lst) + SetModPolicy transitions
86+
+ the auto-sort seam (IModOrderResolver/IdentityModOrderResolver,
87+
identity stub now; real dependency-driven resolver later)
8388
shared-mods/ Magos.Modificus.SharedMods — the global shared mod store
8489
(ISharedModStore manifest) + the version-policy model
85-
(ModVersionPolicy: PinnedPolicy/LatestPolicy) + allocation
86-
resolution (AllocationResolver)
90+
(ModVersionPolicy: PinnedPolicy/LatestPolicy; version is a raw
91+
string tag) + the mod-source provenance model (ModSource:
92+
NoneSource/NexusSource/GitHubSource + ModSourceParser URL parsing)
93+
+ allocation resolution (AllocationResolver, string-equality pins)
94+
+ the local-import service (IModImportService: folder/.zip → store)
8795
integrations/ Magos.Modificus.Integrations — GitHub Releases client
8896
(IGitHubClient: ListReleases/GetLatestRelease/DownloadAssetAsync
8997
via IHttpClientFactory, typed exceptions, optional PAT)
@@ -175,8 +183,10 @@ dotnet run --project magos-modificus/ui --configuration Release # app shell
175183
+ `IsGameRunning``WinProcessLookup` via process comm on Windows,
176184
`LinuxProcessLookup` via `/proc` argv[0] under Proton), **Integrations**
177185
(Phase 1: GitHub Releases client), **Enginseer-client** (Phase 1: the launch
178-
façade), **SharedMods** (Phase 2: shared mod store + version-policy model +
179-
allocation resolution). **General** carries cross-cutting infra: logging,
186+
façade), **SharedMods** (Phase 2: shared mod store + version-policy model
187+
+ allocation resolution; Phase 3 Track B backend: version is a raw string tag,
188+
the mod-source provenance model `ModSource` + `ModSourceParser`, and the local-
189+
import service `IModImportService`; allocation uses string-equality pins). **General** carries cross-cutting infra: logging,
180190
`ConfigLoader` (load + `Save` write-back for Preferences), and `AppStateStore`
181191
(runtime app-state: the active-profile id, persisted to a separate
182192
`app-state.json`, not `MagosConfig`). **Phase 3 Track A UI** (the shell + profile
@@ -186,7 +196,11 @@ dotnet run --project magos-modificus/ui --configuration Release # app shell
186196
**Phase 3 Track D** (global Preferences: theme + font scale + language, plus
187197
the i18n infrastructure: `Strings.resx` + `LocalizationService` for dynamic
188198
culture switching, all Track A UI strings backfilled to resource keys) is
189-
wired. Next: Track B (mod-list) and Track C (launch); the **Launcher** is a
199+
wired. **Phase 3 Track B** (the mod-list UI: view mods with source/version
200+
badges, enable/disable, remove-with-confirm, reorder, per-mod Latest/Pinned
201+
policy, auto-sort identity stub, and local folder/`.zip` import via file picker
202+
+ drag-and-drop, over the `ModSource` + raw-string version model +
203+
`IModImportService`) is wired. Next: Track C (launch); the **Launcher** is a
190204
stub (Phase 5). See `docs/architecture/MAGOS-MODIFICUS.md`.
191205
- **CI** (`magos-build.yml`) is scoped to `magos-modificus/**` + the workflow
192206
file, matrixed on Windows + Ubuntu; gates on build + tests.

docs/architecture/MAGOS-MODIFICUS.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,23 +184,46 @@ mod where possible while preserving per-profile version control where it's
184184
needed. Building this in from v1 (rather than retrofitting dedup later) keeps
185185
the storage model uniform.
186186

187-
Each modin the shared store and in a profile carries a version policy:
187+
Each mod, in the shared store and in a profile, carries a version policy:
188188
**pinned `<version>`** (frozen at a specific release) or **latest (auto-update)**
189189
(tracks the newest release). A profile's mod is resolved against the shared
190190
copy by policy pair:
191191

192192
| Shared | Profile | Resolution |
193193
| --- | --- | --- |
194-
| pinned `v1.0.1` | pinned `v1.0.1` | **share** same pin |
195-
| pinned `v1.0.1` | pinned `v2.0.1` | **diverge** different pins → profile copy |
196-
| latest (auto-update) | latest (auto-update) | **share** both track latest; shared is updated to latest |
197-
| latest (auto-update) | pinned `v2.0.1` | **diverge** shared will move, profile won't → profile copy |
194+
| pinned `v1.0.1` | pinned `v1.0.1` | **share**, same pin |
195+
| pinned `v1.0.1` | pinned `v2.0.1` | **diverge**, different pins → profile copy |
196+
| latest (auto-update) | latest (auto-update) | **share**, both track latest; shared is updated to latest |
197+
| latest (auto-update) | pinned `v2.0.1` | **diverge**, shared will move, profile won't → profile copy |
198198

199199
Rule: **share** iff both pinned to the same version OR both auto-update;
200-
otherwise **diverge**. The resolution is by *policy intent*, not current version
201-
a shared auto-update mod and a profile pinned to today's same version still
200+
otherwise **diverge**. The resolution is by *policy intent*, not current version:
201+
a shared auto-update mod and a profile pinned to today's same version still
202202
diverge, because the shared one will move on the next release.
203203

204+
The pin `<version>` is a **raw release tag string** (e.g. `v1.0.1`, `1.2`,
205+
`1.0.0-beta`), not a parsed `System.Version`. GitHub release tags + Nexus file
206+
versions are arbitrary strings, not SemVer, so the share check is exact string
207+
equality (`"1.0"` and `"1.0.0"` are genuinely different pins). There is no
208+
version ordering at this layer; "newer" is decided later (Phase 4) by fetching
209+
the latest release tag and checking string inequality.
210+
211+
Each shared-store entry also carries a **source** (Local / Nexus / GitHub) so a
212+
pinned version is legible ("WeaponTweaks *(GitHub owner/repo)* pinned to
213+
`1.2`"). The UI collects URLs; the model stores the canonical identity (Nexus
214+
mod id; GitHub owner/repo) via a pure parser. Local / untracked mods default to
215+
the `none` source.
216+
217+
**Import flow:** adding a mod to the active profile goes through
218+
`IModImportService` (the UI never touches the filesystem). The import service
219+
places the files (recursive copy for a folder, `ZipFile.ExtractToDirectory` for
220+
a `.zip`) into `<SharedModsFolder>/<modName>/`, upserts the shared-store entry
221+
with the declared source + version + path, then the caller adds the profile
222+
reference via `IProfileService.AddMod`. First import of a mod name establishes
223+
the shared copy (the shared-first staging); a re-import upserts (replaces files
224+
+ metadata). Remote acquisition (Nexus / GitHub API clients, auto-fetch) stays
225+
in Phase 4.
226+
204227
**Staging:** at launch (alongside regenerating `mods.lst`), Magos materializes
205228
the profile's mod root (the `--mod-path` dir) from the resolved set — shared
206229
mods linked/referenced from the shared store, diverged mods as profile-local

docs/reference/magos-modificus/profiles.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,30 @@ Method behavior:
9999
without symlink permissions / Developer Mode). The staging layer never
100100
silently copies.
101101

102-
`ModVersionPolicy` (PinnedPolicy/LatestPolicy) and `AllocationResolver` live in
103-
the [shared-mods](shared-mods.md) library; Profiles consumes them.
102+
`ModVersionPolicy` (PinnedPolicy/LatestPolicy), `ModSource`, and
103+
`AllocationResolver` live in the [shared-mods](shared-mods.md) library; Profiles
104+
consumes them.
105+
106+
### `IModOrderResolver` + `IdentityModOrderResolver`
107+
108+
The auto-sort seam. The mod-list UI's auto-sort toggle resolves an order via
109+
this interface, then applies it through `IProfileService.SetModOrder`.
110+
111+
```csharp
112+
public interface IModOrderResolver
113+
{
114+
IReadOnlyList<string> ResolveOrder(IReadOnlyList<ModListEntry> mods);
115+
}
116+
117+
public sealed class IdentityModOrderResolver : IModOrderResolver; // identity stub
118+
```
119+
120+
The current implementation is the **identity stub** (`IdentityModOrderResolver`):
121+
it returns names in their current `ModListEntry.Order` (a no-op). The real
122+
dependency-driven auto-sort algorithm lands in a later phase; this interface is
123+
the DI-swappable seam so the UI wires against the abstraction now and the real
124+
resolver drops in later without a UI change. Pure + deterministic (stable on
125+
ties).
104126

105127
## DI registration
106128

@@ -114,6 +136,9 @@ public static IServiceCollection AddProfiles(this IServiceCollection services);
114136
yields a resolvable `IProfileService`; the composition root also calls it.
115137
- `TryAddSingleton<SymlinkCreator>(_ => Directory.CreateSymbolicLink)` — the BCL
116138
default. `TryAdd` so a test may pre-register a throwing/fake delegate.
139+
- `TryAddSingleton<IModOrderResolver, IdentityModOrderResolver>()`: the auto-
140+
sort identity stub. `TryAdd` so a test (or the real dependency-driven resolver,
141+
when it lands) may pre-register an override.
117142
- `AddSingleton<IProfileService, ProfileService>()` — the filesystem-backed
118143
implementation (internal). Resolves `MagosConfig`, `ISharedModStore`,
119144
`SymlinkCreator`, and `ILogger<ProfileService>` from the container.

0 commit comments

Comments
 (0)