Skip to content

feat(mod-loader): own the load-order contract (mods.lst), drop DMF prepend - #14

Merged
ModifAmorphic merged 5 commits into
mainfrom
enginseer/load-order-contract
Jun 28, 2026
Merged

feat(mod-loader): own the load-order contract (mods.lst), drop DMF prepend#14
ModifAmorphic merged 5 commits into
mainfrom
enginseer/load-order-contract

Conversation

@ModifAmorphic

Copy link
Copy Markdown
Owner

What

Decouples Enginseer's mod loader from DMF by giving Enginseer its own load-order file. The loader becomes framework-agnostic: it loads exactly what's listed, in order.

Why

Historically mod_load_order.txt was consumed by DML (the community Darktide-Mod-Loader); Enginseer's mod_loader/ replaced DML. DMF never read it — only the loader did. The loader nonetheless carried one DMF-specific assumption: it auto-prepended dmf as the first entry (table.insert(order, 1, "dmf")).

This PR removes that coupling. The file is renamed mod_load_order.txtmods.lst (authored by the future Magos Modificus app; Enginseer's loader is the sole consumer). Magos writes the complete order — DMF included, first — and the loader loads what's listed, no injection. DMF-first becomes a Magos policy (dependency resolution), not a loader invariant.

Changes

  • mod_manager.lua: reads mods.lst via read_content_to_table("mods", "lst"); deletes the dmf auto-prepend; removes the dead enabled field from the _mods entry shape (set but never read; DMF reads only id/name/handle/_state/_settings.developer_mode); comments updated to the new contract.
  • Tests: rewritten to assert verbatim-order loading (no prepend); tests needing DMF list it explicitly; order = {} → "nothing loads"; added dedicated missing-and-empty mods.lst cases (98/98).
  • Docs: ENGINSEER.md, MOD_LOADER-DMF.md, enginseer/README.md, root README.md, AGENTS.md, loader module headers, and shell comments (trampoline.h, dllmain.c) all updated. Left untouched: docs/reference/* references to mod_load_order.txt (those describe the community DML toolchain — accurate historical fact).
  • Out of scope (untouched): DMF IO re-rooting (_adapt_dmf_io), Rust discovery/, C shell/ logic, launcher/, CI.

Behavior change

Missing/empty mods.lst → no mods load (graceful, no crash). Previously a missing file still loaded DMF via the prepend. Manual users must now list dmf first in mods.lst (documented in README) — until Magos Modificus ships and authors the file automatically.

Verification

  • qa: PASSmake mod-loader-test 98/98; make test (40/40 C, 10/10 Rust, 98/98 Lua); make build; make check; clippy clean. All 7 acceptance criteria met; targeted cases validated (order preservation, DMF-not-first respected, bad-mod isolation, missing/empty paths, blank/comment-line handling).
  • code-reviewer: APPROVE WITH NITS — independent verification of the load-bearing claims (incl. against vendored DMF source); both nits resolved (ENGINSEER.md typo, empty-path test coverage).

Follow-ups (not in this PR)

  • Live validation against the real engine — this change is logic-equivalent at the offline-harness level but hasn't run in-game (the rename means the mod root must contain mods.lst).
  • Dev mods/ dir: the gitignored local mods/ still has mod_load_order.txt; rename it to mods.lst locally before any live smoke test (out of PR scope — gitignored).
  • Unblocks: the Magos Modificus load-order library (which will author mods.lst per profile).

…epend

Decouple Enginseer's mod loader from DMF: it now reads its own load-order file (mods.lst, authored by Magos Modificus) instead of the community mod_load_order.txt, and no longer injects DMF as the first entry. The order file is authoritative for both what loads and the order; the loader is framework-agnostic. Magos lists dmf first so DMF's init() still defines new_mod/get_mod before user mods run.

mod_manager.lua: read mods.lst (Mods.file.read_content_to_table("mods", "lst")); delete the table.insert(order, 1, "dmf") prepend; drop the now-dead enabled field from the _mods entry shape (DMF reads only id/name/handle). Missing/empty mods.lst -> empty _mods -> nothing loads (graceful via the existing or {}).

Tests rewritten to the new contract: the loader loads exactly the listed mods in order (no injection); tests needing DMF list it explicitly; tests using order={} for a DMF-only bootstrap now expect nothing to load. Added a dedicated missing/empty mods.lst -> load-nothing test. Header/source comments in init/lifecycle/file updated.

97/97 mod-loader Lua tests pass (was 96; +1 new).
Bring every in-scope doc in line with the Enginseer load-order contract change: the loader reads mods.lst (authored by Magos Modificus) authoritatively and injects nothing — DMF is first because Magos lists it first, not because the loader prepends it. Missing/empty mods.lst means load nothing.

Updated: docs/architecture/MOD_LOADER-DMF.md (core model, two-roots, SCAN description, DMF load sequence, deferred-bootstrap step), docs/architecture/ENGINSEER.md (loader bullet, staging-dirs prose, env-var table, out-of-scope), enginseer/README.md (two-roots mod-root contents), README.md (where-mods-go layout + prose), AGENTS.md (directory-structure commentary).

Out of scope and intentionally untouched: C shell comments (trampoline.h, dllmain.c) and docs/poc + docs/reference (frozen / community-framework reference) — flagged for the lead.
Comment-only: three shell comments describing the mod root contents still referenced mod_load_order after the Enginseer load-order contract rename. Updated to mods.lst so the comments match the code in this PR. No logic changed.
The 'missing/empty mods.lst' test only drove the missing-file path (read_content_to_table returns false). Split into two honest cases: missing file (returns false) and empty file (file exists, returns {}). Both collapse through the same `or {}` line into an empty order, so both yield empty _mods, no load attempted, _state="done". Reviewer noted this isn't a real coverage gap (same code path), but the test now matches its titles.
Collapse the triple space after 'flag).' on the staging-dirs line to a single space (artifact introduced by the earlier load-order-contract edit).
@ModifAmorphic
ModifAmorphic merged commit 1ccb891 into main Jun 28, 2026
2 checks passed
@ModifAmorphic
ModifAmorphic deleted the enginseer/load-order-contract branch June 28, 2026 22:45
ModifAmorphic added a commit that referenced this pull request Jun 29, 2026
## What

Adds `docs/architecture/MAGOS-MODIFICUS.md` — the canonical target
architecture for the upcoming Magos Modificus (the mod-manager app)
build, plus a small repo-wide cleanup of stale `mod-manager/` →
`magos-modificus/` references. Doc-only; no code.

## Why

Magos Modificus is the second of the project's two components, sitting
on top of the Enginseer runtime (built). This doc is the source of truth
the build decomposes from — it locks the architecture decisions captured
during the kickoff, so the implementation work has a stable contract to
build against. Builds on the merged #14 (the `mods.lst` load-order
contract).

## Decisions locked in this doc

- **Tech:** C# / .NET 10 (LTS) + Avalonia 11; `Microsoft.Extensions.*`
DI + logging; SOLID/DRY; libraries namespaced by domain.
- **Layout:** flat project folders under `magos-modificus/` (`ui/`, the
domain libraries, `launcher/`, `config/`).
- **Enginseer contract consumed:** `magos_launcher.exe` subprocess (flag
> env > default); Magos authors the mod root + `mods.lst` (regenerated
per launch; enable/disable by omission); fire-and-forget launch in v1
(no live control — awaits a future Enginseer IPC contract).
- **Profiles + DMF:** DMF is a normal, optional mod (not required) with
exactly two exceptions — a new-profile "add latest DMF?" prompt (sourced
from GitHub Releases so it needs no Nexus key), and no loader-side
auto-place (Magos writes it first via dependency resolution).
- **Mod sources:** Nexus Mods primary; GitHub Releases + local as
alternatives.
- **Shared mod storage (in v1):** shared-first allocation,
diverge-when-needed, resolved by version-policy intent (pinned vs
auto-update) — not after-the-fact dedup. Built in from v1 so the storage
model is uniform.
- **Launch:** Windows trivial; **Linux = native Magos +
Proton-at-launch** — Magos owns Steam/compatdata/Proton discovery +
escape hatch (fail-fast with a UI prompt) + sets
`STEAM_COMPAT_DATA_PATH` and invokes Proton; **Enginseer is unchanged on
Linux** (no Linux helper, no new flag).
- **v1 scope cut:** in/out enumerated (out = Enginseer live-control,
dependency-view, conflict detection).

## Files

- `docs/architecture/MAGOS-MODIFICUS.md` (new, ~270 lines).
- `docs/architecture/README.md` — index: links the new doc; fixed
`mod-manager/` → `magos-modificus/`.
- `AGENTS.md`, root `README.md`, `magos-modificus/README.md` — stale
`mod-manager` → `magos-modificus` references cleaned up.

## Notes

- Doc-only — no build/test impact.
- 6 commits, rebased on current `main` (the #14 merge).
- For review: the load-bearing sections are **Shared mod storage** (the
shared-first model + 4-case resolution table), **Launch → Linux** (the
discovery + `STEAM_COMPAT_DATA_PATH` design), and **v1 scope**.
ModifAmorphic added a commit that referenced this pull request Jun 29, 2026
…#16)

## What

Phase 0 of the Magos Modificus build — the .NET 10 + Avalonia 12
foundation all later phases build on. Project structure + DI +
structured logging + a global config schema/loader + a bare UI window +
CI. Library projects are **stubs** (interfaces + DI registration only);
real implementations come in Phases 1+.

Builds on the merged architecture doc (#15) and the Enginseer load-order
contract (#14, also merged).

## What's in it

- **Solution + 8 projects** under `magos-modificus/` (flat layout, per
the arch doc): `ui/` (Avalonia 12 exe + DI composition root), `general/`
(logging bootstrap, config loader, DI extensions), `config/`
(`MagosConfig` schema + defaults), and stub libraries `profiles/`,
`integrations/`, `steam/`, `enginseer-client/`, `launcher/`. Plus an
xUnit test project.
- **DI** (`Microsoft.Extensions.DependencyInjection`): each library
exposes `Add<Library>()`; the composition root wires them all;
resolution is verified at startup (5 domain services resolve — real, not
vacuous).
- **Structured logging** (`Microsoft.Extensions.Logging` + Serilog
console + file), honoring the config's level + log file.
- **Global config** (`MagosConfig`): Logging (level/file),
ProfilesBaseFolder, SharedModsFolder, EnginseerRuntimeDir —
platform-appropriate defaults; loaded from JSON; first-run-safe
(parent-dir guard + regression test).
- **CI** (`.github/workflows/magos-build.yml`): Win + Linux matrix,
path-filtered to `magos-modificus/**`; existing mingw/msvc (Enginseer)
workflows untouched.
- **Docs**: dev-facing `magos-modificus/README.md` + `AGENTS.md`
directory-structure update + arch-doc status synced.

## Verification trail

- **coder** bootstrapped .NET 10 locally (so every version resolved via
NuGet, not memory); build 0/0, 7/7 tests, headless Xvfb run shows
config-load + DI-wired + log-file-written; caught + fixed a real
first-run `DirectoryNotFoundException` during the smoke run.
- **qa → PASS** — all 10 acceptance criteria met with file:line
evidence; version spot-check (Avalonia 12.0.5, CommunityToolkit.Mvvm
8.4.2, Serilog 4.3.1, xunit 2.9.3, M.E.* 10.0.9 — all latest stable);
Avalonia.Diagnostics omission verified sound (its 11.3.18 targets 11.x →
would mismatch 12.x core).
- **code-review → APPROVE WITH NITS** — caught one stale pin qa missed
(`coverlet.collector` 6.0.4 → bumped to 10.0.1), plus the arch-doc
status staleness and trivial cleanups; all folded into this PR.
- **CI**: run #28344634825 green on Win + Linux; latest push (review
fixes) running as #28345590519 (csproj version bump + field/element
removal + doc line — zero-risk against the green build).

## One open decision for the reviewer

The `enginseer-client` library's namespace is
**`Magos.EnginseerClient`**; all seven sibling libraries are
`Magos.Modificus.*`. Code review flagged the inconsistency and suggested
`Magos.Modificus.EnginseerClient` for consistency. It's defensible
either way (the distinct namespace reads as "Magos's client for
Enginseer"; the folder `enginseer-client/` already diverges from
siblings). Left as-is for a deliberate ruling — trivial to rename if
preferred.

## Known follow-ups (acceptable for Phase 0; later phases)

- Avalonia.Diagnostics — re-add when a 12.x version ships (currently
11.x lags core).
- Malformed-JSON config test; auto-create of profile/shared dirs; log
rotation; `--warnaserror` / coverage gate in CI; stub-`Add<Library>()`
registration unit tests.
ModifAmorphic added a commit that referenced this pull request Jul 1, 2026
## What

Phase 1 Profiles library for Magos Modificus — the profile data model,
persistence, `mods.lst` generation, and config integration. The
foundation Enginseer-client (Phase 1's capstone) and the UI (Phase 3)
build on. Minimal scope: no shared-mod storage, no version policy, no
auto-sort, no dependency resolution, no mod download (those are Phase 2
/ Integrations / later).

Spec: `docs/architecture/MAGOS-MODIFICUS.md` → Profiles + the `mods.lst`
contract in `docs/architecture/MOD_LOADER-DMF.md`.

## What's in it

- **Data model:** `Profile` (Id/Name/CreatedAt/Mods), `ProfileSummary`
(Id/Name), `ModListEntry` (Name/Enabled/Order — **immutable record**,
init-only).
- **`IProfileService`:** CRUD (create/get/list/rename/delete), mod-list
management (GetModList/SetModOrder/SetModEnabled/AddMod/RemoveMod), and
`PrepareModRoot(id) → string` (the `--mod-path` seam — ensures the mod
root + writes `mods.lst`).
- **Persistence:** `<ProfilesBaseFolder>/<guid>/{profile.json, mods/}`;
auto-creates dirs on first run (closes the Phase-0 gap).
- **`mods.lst`:** enabled mods in `Order`, one per line, UTF-8 **without
BOM** (correctness-critical — a BOM prefixes the first mod name in the
Lua loader), trailing newline; empty/all-disabled → 0-byte file;
faithful to stored order (no auto-sort, no DMF-first enforcement —
that's a higher layer).
- **41 tests** (black-box, via the `AddProfiles()` DI path; persistence
proven across service instances).

## Verification trail

- **coder** — implemented to spec; 49 tests pass (41 Profiles + 8
General); ~98% line coverage on `ProfileService`; no new NuGet deps
(in-box `System.Text.Json`); versions all latest-stable.
- **qa → PASS** — all 7 acceptance criteria met; all 7 deviations sound
+ tested; **`mods.lst` no-BOM independently byte-level-verified + the
rationale confirmed against the Lua loader code path** (`file.lua` `%s`
trim doesn't strip a BOM → it would prefix the first mod name); version
pins latest-stable (coverlet 10.0.1 — Phase-0 miss resolved).
- **code-review → APPROVE WITH NITS** — independently traced the
loader's `mods.lst` consumption path (confirmed contract faithfulness);
confirmed the Phase 2 storage-swap seam holds (no path/shared-vs-local
leakage through the interface or data types); all deviations sound. All
nits folded in: `ModListEntry` → immutable record (was a mutable class
with a misleading immutability doc claim); `ListProfiles` sorted by
Name; storage-agnostic `RemoveMod` doc; `tests/` doc listing updated.
- **CI** — gates Win + Linux on this PR (the workflow is PR-gated
against main); Linux build/test green locally (49/49).

## Phase 2 storage-swap seam (by design)

`IProfileService` is shaped so Phase 2 swaps storage (per-profile dirs →
shared-first + staging) **without changing the interface**:
`PrepareModRoot(id) → string` abstracts "give me the `--mod-path`"; all
storage paths stay private to `ProfileService`; `ModListEntry` will grow
fields (version policy, source) but stays immutable. Phase 2 watch-items
already flagged for that rewrite: atomic `profile.json` writes,
concurrency, mod-name case-canonicalization for the Windows FS.

## Notes

- Doc-only label updates included (README/AGENTS reflect Profiles
implemented; other libraries remain stub until their Phase 1 tracks
land).
- Builds on the merged Phase 0 scaffold (#16) + the `mods.lst`
load-order contract (#14).
ModifAmorphic added a commit that referenced this pull request Jul 8, 2026
🤖 I have created a release *beep* *boop*
---


## 0.1.0 (2026-07-08)


### Features

* **component-a:** Hybrid Rust+C discovery + shell + launcher
([#1](#1))
([491e5d1](491e5d1))
* **magos-modificus:** implement Phase 1 Enginseer-client launch façade
([#20](#20))
([7950ed1](7950ed1))
* **magos-modificus:** implement Phase 1 Integrations (GitHub Releases
client)
([#19](#19))
([781d65c](781d65c))
* **magos-modificus:** implement Phase 1 Profiles library
([#17](#17))
([f355ceb](f355ceb))
* **magos-modificus:** implement Phase 1 Steam discovery library
([#18](#18))
([8f6ec00](8f6ec00))
* **magos-modificus:** implement Phase 2 shared-first mod storage
([#22](#22))
([cf2af80](cf2af80))
* **magos-modificus:** Phase 3 Track B mod-list, import, source model
([#29](#29))
([5075cee](5075cee))
* **magos-modificus:** Phase 3 Track C launch + Settings + escape-hatch
+ base-folder mod loading
([#32](#32))
([c595700](c595700))
* **magos-modificus:** Phase 4 Stage 1 nxm scheme handler + IPC
([#34](#34))
([0017529](0017529))
* **magos-modificus:** Phase 4 Stage 2 Nexus auth + Integrations dialog
([#35](#35))
([0790a8e](0790a8e))
* **magos-modificus:** Phase 4 Stage 3 Nexus mod acquisition
([#36](#36))
([d01105f](d01105f))
* **magos-modificus:** Phase 4 Stage 4 Nexus update-check service
([#39](#39))
([1749e76](1749e76))
* **magos-modificus:** Phase 4 Stage 5 mod-list update badges + per-mod
update
([#43](#43))
([423e146](423e146))
* **magos-modificus:** Phase 4 Stage 6 DMF new-profile/auth prompt
([#44](#44))
([994b4f8](994b4f8))
* **magos-modificus:** scaffold .NET 10 + Avalonia 12 app + libraries
([#16](#16))
([d1fac91](d1fac91))
* **mod-loader:** own the load-order contract (mods.lst), drop DMF
prepend
([#14](#14))
([1ccb891](1ccb891))
* **release:** add Curator release pipeline
([#49](#49))
([01517e4](01517e4))
* **runtime:** engine-context proven — trampoline, Enginseer v1,
launcher fail-fast
([#4](#4))
([4565ba8](4565ba8))
* **runtime:** Enginseer v2 — mod loader + launcher config + logging
([#5](#5))
([1e65b3f](1e65b3f))
* **runtime:** package Enginseer with the runtime; relocate build files
to runtime/
([#6](#6))
([7221bdb](7221bdb))
* **ui:** Phase 3 Track A — app shell + profile management
([#27](#27))
([f7f8250](f7f8250))
* **ui:** Phase 3 Track D — Preferences + i18n + custom title bars +
icon
([#28](#28))
([c921650](c921650))


### Bug Fixes

* **enginseer:** DMF integration fixes — IO re-root, load timing,
destroy
([#7](#7))
([401759c](401759c))
* **magos-modificus:** multi-format archive import (zip + 7z + rar)
([#41](#41))
([ee4f5c6](ee4f5c6))
* **magos-modificus:** search all Steam libraries for the compatdata
prefix
([#21](#21))
([895fa2b](895fa2b))
* **steam:** detect running Darktide via /proc argv[0] under Proton
([#23](#23))
([c5f38c4](c5f38c4))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ModifAmorphic <86930443+ModifAmorphic@users.noreply.github.com>
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