Skip to content

Read and emit the WitcherScriptMerger.exe.config Vortex configures - #40

Open
TheValiantOne wants to merge 1 commit into
mainfrom
feature/vortex-exe-config-interop
Open

Read and emit the WitcherScriptMerger.exe.config Vortex configures#40
TheValiantOne wants to merge 1 commit into
mainfrom
feature/vortex-exe-config-interop

Conversation

@TheValiantOne

Copy link
Copy Markdown
Owner

The disconnect

Vortex's bundled game-witcher3 extension hardcodes the .NET Framework <exe>.exe.config name for the script merger's config (scriptmerger.ts's MERGER_CONFIG_FILE). It both reads and writes that file:

  • reads it for MergedModName (getMergedModName)
  • writes GameDirectory, VanillaScriptsDirectory and ModsDirectory into it when configuring a merger install (setMergerConfig)

A modern .NET app's own config is <assembly>.dll.config. So the two never meet: Vortex writes a file WSM never reads, and a user who "configures WSM through Vortex" changes nothing at all.

Observed on a real install:

[game-witcher] failed to ascertain merged mod name - using "mod0000_MergedFiles"
  {"code":"ENOENT","path":"...\The Witcher 3\WitcherScriptMerger\WitcherScriptMerger.exe.config"}

It falls back to a hardcoded guess. That guess happened to be right, which is why this stayed invisible. The same mergerToolDir is also where the extension moves MergeInventory.xml on profile store/restore, so the mis-resolution reaches further than just the mod name.

The fix, in two halves

Emit it. The WinForms csproj now produces WitcherScriptMerger.exe.config beside the usual .dll.config, at build and at publish — but only when absent. Vortex owns that file once it has written to it; clobbering it on every rebuild would discard the paths it configured.

Read it. AppSettings.GetRawValue falls back to the sidecar when a key is blank in our own config. Emitting a file nothing reads would just be a subtler version of the same disconnect — Vortex's writes have to actually take effect.

Precedence, first non-blank wins:

  1. WSM_<key> environment variable
  2. our own <AssemblyName>.dll.config
  3. the Vortex-managed sidecar

A fallback, not an override, deliberately. A non-blank value in our own config is an explicit choice — the GUI's settings screen writes there via Set/Save, and Vortex never writes MergedModName. Meanwhile the three keys Vortex does write are exactly the ones that ship blank meaning "derive from the working directory", so the sidecar only ever fills in what we'd otherwise guess.

ParseAppSettingValue(xml, key) is a pure static over the file's text — no filesystem, no AppState — so it's unit-testable per WitcherScriptMerger.Tests/CLAUDE.md's safety constraints. It returns null for anything it can't confidently read, so callers fall through to existing behavior rather than acting on a half-parsed file. The read is cached and never throws or prompts: it runs inside every settings read, including scan paths where an exception would surface as a merge failure.

One small robustness change: Settings[key] is now dereferenced with ?.. It returns null for a key absent from App.config, which used to throw and get swallowed by Get/Get<T>'s catch — observably identical for both, but now a key present only in the sidecar resolves instead of dying first.

Verification

19 new tests, 192 total, all passing. dotnet format whitespace --verify-no-changes clean.

Unit tests cover key lookup, case-sensitivity, blank-value and missing-key both yielding null (what makes the ?? fall-through correct), malformed/truncated/empty XML degrading rather than throwing, null/blank inputs, and that VortexSidecarFileName still matches the name Vortex hardcodes.

End-to-end against a scratch game tree with the real WinForms build, writing the sidecar exactly the way setMergerConfig does:

scenario result
own config blank, no env, no sidecar Can't find Mods directory — exit 1
own config blank, no env, sidecar present Merged 1 file(s), skipped 0. — exit 0
sidecar present + WSM_ModsDirectory bogus Can't find the Mods directory specified in the config file — env won
sidecar present + non-blank ModsDirectory in own config same — our own config won

I got the precedence test wrong on the first attempt (overrode GameDirectory while the sidecar also set ModsDirectory, which Paths.ModsDirectory uses directly, so it proved nothing) — the table above is the corrected run.

Two MSBuild bugs found and fixed while writing the target: $(PublishDir) is defined unconditionally by the SDK (defaulting to $(OutDir)publish\), so a single target choosing "PublishDir if set, else OutDir" silently wrote to the publish folder during an ordinary build and left the build output without the file — hence two separate targets. And a malformed comment block that broke the csproj parse.

WitcherScriptMerger.Headless deliberately does not emit this file: Vortex's extension only ever looks for a merger named WitcherScriptMerger.exe, so a WitcherScriptMerger.Headless.exe.config would be read by nothing.

Follow-up not in scope here

This makes the interop work once Vortex points at the real merger. On the install that motivated it, Vortex's W3ScriptMerger tool registration points at an empty <game>\WitcherScriptMerger\ folder while the actual WSM sits at the game root — that's a Vortex-side setting, not something this PR can fix.

AI-assisted development

Produced by Claude Code (Opus 5), including the log-driven diagnosis of the mismatch and the end-to-end runs above. Per CONTRIBUTING.md: every result reported here is from a real run against a real scratch tree.

Vortex's bundled game-witcher3 extension hardcodes the .NET Framework
"<exe>.exe.config" name for the script merger's config (scriptmerger.ts's
MERGER_CONFIG_FILE). It parses that file for MergedModName (getMergedModName)
and writes GameDirectory, VanillaScriptsDirectory and ModsDirectory into it when
configuring a merger install (setMergerConfig). A modern .NET app's own config is
"<assembly>.dll.config", so the two never met: Vortex wrote a file WSM never read,
and a user who "configured WSM through Vortex" changed nothing at all.

Observed on a real install: Vortex logs

  [game-witcher] failed to ascertain merged mod name - using "mod0000_MergedFiles"
    {"code":"ENOENT","path":"...\The Witcher 3\WitcherScriptMerger\WitcherScriptMerger.exe.config"}

and falls back to a hardcoded guess. The same mergerToolDir is also where it moves
MergeInventory.xml on profile store/restore, so the mis-resolution reaches further
than the mod name.

Two halves:

- The WinForms csproj now emits WitcherScriptMerger.exe.config beside the usual
  .dll.config, at build and at publish, and only when absent - Vortex owns that
  file once it has written to it, so rebuilding must not discard the paths it
  configured. Two targets rather than one with AfterTargets="Build;Publish": the
  SDK defines $(PublishDir) unconditionally, so a single "PublishDir if set, else
  OutDir" target wrote to the publish folder during an ordinary build and left the
  build output without the file.
- AppSettings.GetRawValue now falls back to that sidecar when a key is blank in
  our own config. Emitting a file nothing reads would just be a subtler version of
  the same disconnect - Vortex's writes have to actually take effect.

Precedence is WSM_<key> env var, then our own non-blank config value, then the
sidecar. A fallback rather than an override, deliberately: a non-blank value in
our own config is an explicit choice (the GUI writes there via Set/Save, and
Vortex never writes MergedModName), while the three keys Vortex does write are
exactly the ones that ship blank meaning "derive from the working directory".
Settings[key] is now dereferenced with ?. so a key present only in the sidecar
resolves instead of throwing first - observably identical to the old behavior for
Get/Get<T>, which caught that exception anyway.

ParseAppSettingValue is a pure static over the file's text so it is unit-testable
without a filesystem, an AppSettings instance, or AppState; it returns null for
anything it cannot confidently read, and the read is cached and never throws or
prompts, since it runs inside every settings read including scan paths.

19 new tests (192 total). Verified end-to-end against a scratch game tree with the
WinForms build: with our own config blank and no env vars, a sidecar written the
way setMergerConfig writes it took effect (merge succeeded, exit 0) where the same
run without it failed with "Can't find Mods directory" (exit 1); a WSM_ModsDirectory
override and a non-blank ModsDirectory in our own config each correctly beat the
sidecar. dotnet format clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FP8H6rBLCGPBFRSVsF3Kgw
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