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
12 changes: 7 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ Requirements, architecture, and technology choices are made fresh.

- **`main`** — production. Enginseer (the injected modding runtime + launcher) is
merged as the production seed; Magos Modificus is scaffolded (Phase 0:
.NET 10 + Avalonia 12 foundation — libraries are stubs).
.NET 10 + Avalonia 12 foundation — Profiles implemented in Phase 1; the other
libraries are stubs).
- **`poc`** — historical proof-of-concept, reference only. Not built upon.
- Development is branch + PR; no unreviewed merges to `main` (reviewed +
covered + qa'd + CI green).
Expand Down Expand Up @@ -65,14 +66,15 @@ magos-modificus/ Magos Modificus — the mod manager app (.NET 10 + Avalo
general/ Magos.Modificus.General — cross-cutting infra (logging bootstrap,
config loader, AddGeneral() DI ext)
config/ Magos.Modificus.Config — the MagosConfig schema + defaults (POCO)
profiles/ Magos.Modificus.Profiles — stub (interfaces + AddProfiles())
profiles/ Magos.Modificus.Profiles — profile data model, persistence, mods.lst generation
integrations/ Magos.Modificus.Integrations — stub
steam/ Magos.Modificus.Steam — stub
enginseer-client/ Magos.Modificus.EnginseerClient — stub (the v1 launch façade)
launcher/ Magos.Modificus.Launcher — stub (slim profile launcher exe;
the Steam non-steam-shortcut target)
tests/
Magos.Modificus.General.Tests/ xUnit tests for the general library
Magos.Modificus.Profiles.Tests/ xUnit tests for the profiles library
docs/ architecture, poc (frozen), reference
.github/workflows/ CI: mingw-build + msvc-build (Enginseer) + magos-build (Magos Modificus)
.gitignore ignores enginseer/target, enginseer/bin, .NET bin/obj, build artifacts, _local/
Expand Down Expand Up @@ -132,9 +134,9 @@ dotnet run --project magos-modificus/ui --configuration Release # bare Avalo
file/dir → defaults (first-run safe).
- **Logging** is Serilog (console + file) bridged into
`Microsoft.Extensions.Logging`; honors `Logging:Level` + `Logging:LogFile`.
- Library projects are **stubs** (interfaces + `Add<Library>()` only); real
implementations come in later phases. See
`docs/architecture/MAGOS-MODIFICUS.md`.
- Profiles is implemented (Phase 1); the other library projects are **stubs**
(interfaces + `Add<Library>()` only). Real implementations come in later
phases. See `docs/architecture/MAGOS-MODIFICUS.md`.
- **CI** (`magos-build.yml`) is scoped to `magos-modificus/**` + the workflow
file, matrixed on Windows + Ubuntu; gates on build + tests.

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ Magos has two components:
[`enginseer/README.md`](enginseer/README.md) for build + developer details.
- **Magos Modificus** — the mod manager app (UI, staging, load order, profiles,
dependency resolution). Phase 0 scaffold in place (.NET 10 + Avalonia 12
foundation, DI + logging + config + a bare UI window); library behavior is
stubbed for now. See [`magos-modificus/README.md`](magos-modificus/README.md)
for developer/build details.
foundation, DI + logging + config + a bare UI window); the Profiles library
is implemented (Phase 1), other library behavior is still stubbed. See
[`magos-modificus/README.md`](magos-modificus/README.md) for developer/build
details.

## Getting started

Expand Down
8 changes: 5 additions & 3 deletions magos-modificus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ Steam), and the "Launch Darktide" button that invokes the Enginseer launcher.

> **Status: Phase 0 scaffold.** The project layout, DI composition, structured
> logging, global config schema/loader, and a bare UI window are in place.
> Library implementations come in later phases — the domain libraries are
> currently stubs (interfaces + DI registration only). Target architecture:
> Library implementations come in later phases — Profiles is implemented
> (Phase 1); the other domain libraries are currently stubs (interfaces + DI
> registration only). Target architecture:
> [`../docs/architecture/MAGOS-MODIFICUS.md`](../docs/architecture/MAGOS-MODIFICUS.md).

## Tech stack
Expand All @@ -32,13 +33,14 @@ magos-modificus/
ui/ Magos.Modificus.UI Avalonia executable + DI composition root
general/ Magos.Modificus.General cross-cutting infra: logging, config loader, DI
config/ Magos.Modificus.Config the MagosConfig schema + defaults (POCO)
profiles/ Magos.Modificus.Profiles stub
profiles/ Magos.Modificus.Profiles implemented (Phase 1)
integrations/ Magos.Modificus.Integrations stub
steam/ Magos.Modificus.Steam stub
enginseer-client/ Magos.Modificus.EnginseerClient stub (launch façade)
launcher/ Magos.Modificus.Launcher stub (slim Steam-shortcut launcher)
tests/
Magos.Modificus.General.Tests/ xUnit tests for the general library
Magos.Modificus.Profiles.Tests/ xUnit tests for the profiles library
```

Each library exposes an `Add<Library>()` extension method on
Expand Down
15 changes: 15 additions & 0 deletions magos-modificus/magos-modificus.sln
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Magos.Modificus.General.Tests", "tests\Magos.Modificus.General.Tests\Magos.Modificus.General.Tests.csproj", "{A3D79190-AD1E-428E-A0B0-224AE2A5A2BF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Magos.Modificus.Profiles.Tests", "tests\Magos.Modificus.Profiles.Tests\Magos.Modificus.Profiles.Tests.csproj", "{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -157,6 +159,18 @@ Global
{A3D79190-AD1E-428E-A0B0-224AE2A5A2BF}.Release|x64.Build.0 = Release|Any CPU
{A3D79190-AD1E-428E-A0B0-224AE2A5A2BF}.Release|x86.ActiveCfg = Release|Any CPU
{A3D79190-AD1E-428E-A0B0-224AE2A5A2BF}.Release|x86.Build.0 = Release|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Debug|x64.ActiveCfg = Debug|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Debug|x64.Build.0 = Debug|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Debug|x86.ActiveCfg = Debug|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Debug|x86.Build.0 = Debug|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Release|Any CPU.Build.0 = Release|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Release|x64.ActiveCfg = Release|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Release|x64.Build.0 = Release|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Release|x86.ActiveCfg = Release|Any CPU
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -171,5 +185,6 @@ Global
{7B06B902-3D0E-4336-8B0C-ABDB967AF85F} = {740C5202-8AD2-6D86-0CA9-F95B1A2D697C}
{5952A950-2AE3-43BC-8891-28D81B2B638E} = {FA674B5A-3394-926C-2B1E-70E5B00E4A5C}
{A3D79190-AD1E-428E-A0B0-224AE2A5A2BF} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{B4E0C2A1-1F2D-4A3E-9B5C-7D6E8F901A23} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
88 changes: 88 additions & 0 deletions magos-modificus/profiles/IProfileService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace Magos.Modificus.Profiles;

/// <summary>
/// Profile + per-profile mod-list management. Owns the profile data model,
/// its on-disk persistence, and the projection of the mod list into
/// <c>mods.lst</c> for the Enginseer runtime.
/// </summary>
/// <remarks>
/// <para><b>Phase 1 → Phase 2 stability:</b> this interface is designed so the
/// storage implementation can swap (per-profile dirs → shared-first + staging)
/// without changing the surface:</para>
/// <list type="bullet">
/// <item><see cref="PrepareModRoot"/> abstracts "give me the <c>--mod-path</c>" —
/// Phase 1 returns the per-profile <c>mods/</c> dir; Phase 2 returns a staged
/// dir built from shared-first resolution.</item>
/// <item><see cref="ModListEntry"/> will grow fields (version policy, source) but
/// the Phase 1 fields stay.</item>
/// <item>No storage details (paths, shared-vs-local) leak through the interface.</item>
/// </list>
/// </remarks>
public interface IProfileService
{
/// <summary>All known profiles, as lightweight summaries.</summary>
IReadOnlyList<ProfileSummary> ListProfiles();

/// <summary>Loads the full profile (metadata + mod list).</summary>
/// <exception cref="KeyNotFoundException"><paramref name="id"/> is unknown.</exception>
Profile GetProfile(Guid id);

/// <summary>
/// Creates a new profile: generates the id, scaffolds its directory + mod
/// root, and persists an empty <c>profile.json</c>.
/// </summary>
/// <returns>The newly-created profile.</returns>
Profile CreateProfile(string name);

/// <summary>Renames the profile (display label only; id and dir are unchanged).</summary>
/// <exception cref="KeyNotFoundException"><paramref name="id"/> is unknown.</exception>
void RenameProfile(Guid id, string newName);

/// <summary>Removes the profile entry and its entire on-disk directory tree.</summary>
/// <exception cref="KeyNotFoundException"><paramref name="id"/> is unknown.</exception>
void DeleteProfile(Guid id);

/// <summary>The profile's mod list (in stored order, not load order).</summary>
/// <exception cref="KeyNotFoundException"><paramref name="id"/> is unknown.</exception>
IReadOnlyList<ModListEntry> GetModList(Guid id);

/// <summary>
/// Reassigns <see cref="ModListEntry.Order"/> so the profile's mods follow
/// <paramref name="modNamesInOrder"/>. Mods not mentioned keep their relative
/// order, appended after the listed ones; names in the list that aren't in
/// the profile are ignored. No mods are added or removed.
/// </summary>
/// <exception cref="KeyNotFoundException"><paramref name="id"/> is unknown.</exception>
void SetModOrder(Guid id, IReadOnlyList<string> modNamesInOrder);

/// <summary>Toggles <see cref="ModListEntry.Enabled"/> for a single mod.</summary>
/// <exception cref="KeyNotFoundException">
/// <paramref name="id"/> is unknown, or <paramref name="modName"/> is not in the profile's list.
/// </exception>
void SetModEnabled(Guid id, string modName, bool enabled);

/// <summary>
/// Adds a mod entry to the end of the list (<see cref="ModListEntry.Enabled"/>
/// = true). <b>List entry only — does NOT fetch or install mod files.</b>
/// Idempotent: adding a name already in the list is a no-op.
/// </summary>
/// <exception cref="KeyNotFoundException"><paramref name="id"/> is unknown.</exception>
void AddMod(Guid id, string modName);

/// <summary>
/// Removes the mod entry and the mod's local files, if any. A missing local
/// copy for a listed mod is graceful (not a crash).
/// </summary>
/// <exception cref="KeyNotFoundException">
/// <paramref name="id"/> is unknown, or <paramref name="modName"/> is not in the profile's list.
/// </exception>
void RemoveMod(Guid id, string modName);

/// <summary>
/// Ensures the profile's mod root exists and writes <c>mods.lst</c> from the
/// current mod list (enabled mods, in <see cref="ModListEntry.Order"/>).
/// Idempotent. Returns the <c>--mod-path</c> to pass to the Enginseer launcher.
/// </summary>
/// <exception cref="KeyNotFoundException"><paramref name="id"/> is unknown.</exception>
string PrepareModRoot(Guid id);
}
4 changes: 4 additions & 0 deletions magos-modificus/profiles/Magos.Modificus.Profiles.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\config\Magos.Modificus.Config.csproj" />
</ItemGroup>

</Project>
33 changes: 33 additions & 0 deletions magos-modificus/profiles/ModListEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Magos.Modificus.Profiles;

/// <summary>
/// A single mod entry within a profile's mod list — the source of truth that
/// <see cref="IProfileService.PrepareModRoot"/> projects into <c>mods.lst</c>.
/// </summary>
/// <remarks>
/// <para>Immutable: all properties are init-only. Mutations go through the
/// <see cref="IProfileService"/> methods, which rebuild the changed entry
/// (via <c>with</c> expressions) and persist — a consumer can't silently edit
/// an entry returned from <see cref="IProfileService.GetModList"/> and have it
/// look persisted when it isn't.</para>
/// <para>Phase 1 shape only. Version / source / policy fields land with
/// Integrations and Phase 2 (shared-first storage); the Phase 1 fields stay
/// stable when they arrive, so this type will <em>grow</em> but not break.</para>
/// </remarks>
public sealed record ModListEntry
{
/// <summary>The mod folder name — the value written to <c>mods.lst</c>.</summary>
public string Name { get; init; } = string.Empty;

/// <summary>
/// Whether the mod is active. Disabled mods are omitted from
/// <c>mods.lst</c> (enable-by-omission, per the loader contract).
/// </summary>
public bool Enabled { get; init; }

/// <summary>
/// Position within the load order; lower loads first. <see cref="int"/>
/// rather than the list index so partial reordering is stable.
/// </summary>
public int Order { get; init; }
}
31 changes: 31 additions & 0 deletions magos-modificus/profiles/Profile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Magos.Modificus.Profiles;

/// <summary>
/// A Magos Modificus profile — a named, owned set of mods + load order. The
/// aggregate root persisted to <c>&lt;ProfilesBaseFolder&gt;/&lt;Id&gt;/profile.json</c>.
/// </summary>
/// <remarks>
/// Identity is <see cref="Id"/> (a <see cref="Guid"/>, stable across renames);
/// the on-disk directory is keyed by it. <see cref="Name"/> is a display label,
/// not unique and not used as a path.
/// </remarks>
public sealed class Profile
{
/// <summary>Stable identity; also the on-disk directory name.</summary>
public Guid Id { get; init; }

/// <summary>Display name. Renamable via <see cref="IProfileService.RenameProfile"/>.</summary>
public string Name { get; set; } = string.Empty;

/// <summary>When the profile was first created (UTC).</summary>
public DateTimeOffset CreatedAt { get; init; }

/// <summary>
/// The profile's mods, in no particular storage order — load order comes
/// from each entry's <see cref="ModListEntry.Order"/>. Exposed as a
/// <see cref="IReadOnlyList{T}"/> of immutable entries: neither the list
/// nor its entries can be edited in place — changes go through the
/// <see cref="IProfileService"/> methods, which rebuild + persist.
/// </summary>
public IReadOnlyList<ModListEntry> Mods { get; set; } = Array.Empty<ModListEntry>();
}
27 changes: 0 additions & 27 deletions magos-modificus/profiles/ProfileModule.cs

This file was deleted.

Loading
Loading