Skip to content

Mod Staging

Fievetl edited this page Jul 4, 2026 · 2 revisions

Mod Staging

The three forms

ModStager accepts exactly what the game's own mod loader accepts:

  • Folder: a directory laid out like a mod (with modinfo.json), copied recursively into the staging directory.
  • Zip: a .zip archive, copied as-is; the game's mod loader unpacks it like any other zip mod.
  • Dll: a compiled mod assembly, copied as-is.

Paths can point at any of the three; Atlas resolves each one, verifies it exists, and copies it into a scratch staging folder that the embedded server loads mods from.

AtlasMods path resolution

Paths declared via [assembly: AtlasMods(...)] or [AtlasWorld(Mods = ...)] are resolved relative to the test assembly's own output directory, not the source tree. If your mod lives at samples/SampleMod and your test assembly builds to samples/Sample.Scenarios/bin/Debug/net10.0/, the path is "../../../../SampleMod" (up through net10.0, the configuration folder, bin, and the project folder, then down into the mod).

If a path does not resolve to an existing folder, .zip, or .dll, ModStager.Stage throws AtlasSetupException naming every missing path. See Troubleshooting.

The MSBuild sugar: ProjectReference + AtlasMod=true

Hand-writing a relative path gets fragile as soon as build configuration or output layout changes. If your mod-under-test is a project in the same solution, tag the ProjectReference instead:

<ProjectReference Include="..\MyMod\MyMod.csproj">
  <AtlasMod>true</AtlasMod>
</ProjectReference>

Pixnop.Atlas.XUnit ships a buildTransitive MSBuild target (WriteAtlasModManifest, defined in build/Atlas.E2E.targets) that runs automatically on every build of every package consumer. It reads _ResolvedProjectReferencePaths (the item MSBuild produces after resolving each ProjectReference to its actual build output) filtered to entries whose AtlasMod metadata is true, and writes the resolved output path of each into atlas-mods.generated.txt, one absolute path per line, directly in $(TargetDir) next to the test assembly.

Ordering: attribute paths, then manifest paths

AttributeMapper.Map reads atlas-mods.generated.txt when present and appends its paths after any paths declared via [assembly: AtlasMods(...)] or [AtlasWorld(Mods = ...)]. Both sources are staged the same way; the ordering only matters if your mod loader cares about dependency-sort input order, which in practice it does not since the game's own dependency resolution reorders mods regardless.

Stale-file semantics

The manifest is written with WriteLinesToFile(Overwrite=true), so re-builds do not append stale entries. If no ProjectReference carries AtlasMod=true in the current build (for example, after removing the metadata), any leftover manifest from a previous build is deleted rather than left behind with outdated content. This always-run write/delete pair is deliberate: with no Inputs/Outputs declared on the target, there is no up-to-date skip, so the stale-manifest cleanup is reliable on incremental builds. The cost is a cheap no-op (one item transform, one Exists check) when no reference is tagged, which is the common case for most consumers of the package.

The target is gated on '$(TargetFramework)' != '' so it only runs on the inner per-TFM passes of a multi-targeting build, where $(TargetDir) is actually meaningful.

Why the bridge is staged separately

AtlasBridge.dll (the harness ModSystem that captures ICoreServerAPI and hands it to the engine, see Architecture) is staged through a separate code path, ModStager.StageBridge, rather than being folded into the general Stage call used for mods-under-test:

  • It is always exactly one file, known at build time, never user-supplied, so it does not need the folder/zip/dll dispatch that user mod paths do.
  • Its staging failures are diagnostically distinct from a missing mod path: a failed bridge copy means Atlas itself cannot function (no way to reach ICoreServerAPI), not that the user mistyped a path. StageBridge wraps file system failures (locked file, permissions, missing source) in AtlasSetupException, naming both the source and destination and carrying the file system error as the inner exception, so the failure reads as a setup problem rather than an opaque host crash.

Clone this wiki locally