Skip to content

Add WSM_<key> environment-variable override for AppSettings - #13

Merged
TheValiantOne merged 1 commit into
mainfrom
feature/env-var-settings-override
Aug 9, 2026
Merged

Add WSM_<key> environment-variable override for AppSettings#13
TheValiantOne merged 1 commit into
mainfrom
feature/env-var-settings-override

Conversation

@TheValiantOne

Copy link
Copy Markdown
Owner

Summary

Adds a generic WSM_<key> environment-variable override to AppSettings.Get/Get<T>, checked ahead of the existing ConfigurationManager-backed App.config <appSettings> lookup.

This is Unit C of a larger effort to build a Vortex extension for WSM. Vortex's own built-in Witcher 3 extension (a different, unrelated fork's integration) configures WSM today by hand-editing WitcherScriptMerger.exe.config XML directly (<add key="GameDirectory" value="..."/>, etc.) — fragile, with no lock/mutex, and race-prone against a concurrently-running WSM process that also reads that file. This override gives a new Vortex extension (or any other external caller) a clean, read-only alternative: set WSM_GameDirectory/WSM_ModsDirectory/etc. in the process environment before invoking WSM, with zero App.config writes at all.

Changes

  • WitcherScriptMerger.Core/AppSettings.cs

    • EnvironmentVariablePrefix = "WSM_" and static GetEnvironmentOverride(key) — a side-effect-free lookup (Environment.GetEnvironmentVariable("WSM_" + key)).
    • New private GetRawValue(key) centralizes the lookup: checks the env override first (returning immediately, before ever touching the cached Configuration object), then falls through to the original CachedConfig.HasFile/Settings[key].Value/"config file doesn't exist" logic, unchanged.
    • Get<T> and Get(string) both route through GetRawValue instead of duplicating the old CachedConfig.HasFile branch. Get<T>'s reflection-based Parse conversion is unchanged and applies identically to an env-sourced value as to a config-sourced one — no separate ad-hoc parser.
    • Generic by construction: works for any key via string concatenation, not an enumerated list — automatically covers GameDirectory, ModsDirectory, MergedModName, QuickBmsPath, QuickBmsPluginPath, WccLitePath, CheckBundleContents, and anything added later, with zero further code changes.
    • Set/Save are untouched — still only ever write to App.config. Added a comment on Set documenting a known, accepted asymmetry: if a WSM_<key> override is active, Set+Save still succeeds but Get/Get<T> keep returning the override afterward. This is existing/expected behavior per the task spec, not something this PR attempts to reconcile.
  • WitcherScriptMerger.Tests/AppSettingsTests.cs (new)

    • Coverage for GetEnvironmentOverride (unset → null; generic across every real <appSettings> key today plus a hypothetical future one) and for Get/Get<bool>/Get<int> end-to-end with an env override in effect, including an unparsable-value case confirming the existing safe-default (default(T)) behavior is preserved for env-sourced values too.
    • No test constructs a live AppSettings. AppSettings's constructor calls Environment.Exit(1) if it can't find a config file next to the entry assembly, which under dotnet test's testhost.dll host kills the whole test process, not just one test (see WitcherScriptMerger.Tests/CLAUDE.md's "AppState.Settings-safety constraints"). Instead:
      • GetEnvironmentOverride is exercised directly — it's static and never touches CachedConfig/AppState.
      • Get/Get<T> are exercised via RuntimeHelpers.GetUninitializedObject(typeof(AppSettings)), which skips the constructor. This is safe specifically because, with an env override active, GetRawValue returns before ever touching the lazily-initialized CachedConfig property — confirmed by reading the implementation, not assumed.
    • What's explicitly not unit-tested: "env var wins over an actually-present config-file value" (would require a live AppSettings) and the full CLI pipeline. Both are covered by the end-to-end run below instead.

Verification

  1. dotnet build WitcherScriptMerger.sln — succeeds, 0 warnings, 0 errors.

  2. dotnet test WitcherScriptMerger.sln — 63/63 pass (57 pre-existing + 6 new), including confirming the test process doesn't die (i.e. nothing accidentally forced AppSettings's real constructor to run).

  3. dotnet format whitespace WitcherScriptMerger.sln --verify-no-changes — clean.

  4. End-to-end scratch-tree run (proves the override actually works, not just in a unit test): built WitcherScriptMerger.Headless (its shipped App.config/.dll.config has GameDirectory/ModsDirectory both blank by default). Created a scratch tree under the OS temp dir:

    • Game\content\content0\scripts\testConflict.ws (vanilla)
    • Mods\mod1000_TestModA\...\testConflict.ws (changes one field)
    • Mods\mod2000_TestModB\...\testConflict.ws (changes a different, independent field)

    Set only WSM_GameDirectory/WSM_ModsDirectory (process environment) pointing at that scratch tree — no App.config edits at all — then ran WitcherScriptMerger.Headless.exe merge. Result: Merged 1 file(s), skipped 0. (exit code 0). Inspected the merged output: it correctly contains both mods' independent edits (proving a real 3-way merge ran against the scratch vanilla/mod files, not whatever App.config would otherwise have pointed at), UTF-16LE+BOM encoded matching vanilla. Re-checked the on-disk .dll.config afterward — GameDirectory/ModsDirectory were still blank, confirming zero config-file writes occurred. Scratch tree removed afterward.

Code review

Ran this repo's /code-review skill against the diff before opening this PR. Of 4 findings:

  • Fixed: the new test file's environment-cleanup helper previously cleared the WSM_<key> var unconditionally in its finally block instead of restoring whatever was there before — now captures and restores the original value.
  • Documented, not changed (per the task's own explicit instruction not to "fix" this): Set() has no awareness of an active env override, so Set+Save can silently have no visible effect on Get/Get<T> if an override is active for that key. Added a comment on Set calling this out explicitly.
  • Accepted as an intentional design tradeoff: the override is genuinely generic and therefore also applies to QuickBmsPath/WccLitePath (paths fed to Process.Start), not just directory settings — this is required by the task's explicit "must work for any key, zero per-key code" spec, and carries no more risk than the status quo (anyone who can set process environment variables for WSM can already edit App.config directly, which those same paths already come from).
  • Not addressed (pre-existing, unrelated to this change): a minor double-read of Paths.ModsDirectory in ModFile.GetModNameFromPath, out of scope for this PR.

AI-assisted development disclosure

This PR was substantially produced by an AI coding agent (Claude Code), per this repo's CONTRIBUTING.md. Commits carry the required Co-Authored-By/Claude-Session trailers.

Lets a caller like a Vortex extension point WSM at a game/mods directory
(WSM_GameDirectory, WSM_ModsDirectory, etc.) without hand-editing
WitcherScriptMerger.exe.config on disk - the fragile, lock-free pattern
Vortex's existing, unrelated-fork integration uses today. Get/Get<T> check
Environment.GetEnvironmentVariable("WSM_" + key) first, generically for any
key, before falling through to the existing ConfigurationManager-backed
lookup; Set/Save are untouched and still only ever write to App.config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
@TheValiantOne
TheValiantOne merged commit 5013ee3 into main Aug 9, 2026
1 check passed
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