Read and emit the WitcherScriptMerger.exe.config Vortex configures - #40
Open
TheValiantOne wants to merge 1 commit into
Open
Read and emit the WitcherScriptMerger.exe.config Vortex configures#40TheValiantOne wants to merge 1 commit into
TheValiantOne wants to merge 1 commit into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The disconnect
Vortex's bundled
game-witcher3extension hardcodes the .NET Framework<exe>.exe.configname for the script merger's config (scriptmerger.ts'sMERGER_CONFIG_FILE). It both reads and writes that file:MergedModName(getMergedModName)GameDirectory,VanillaScriptsDirectoryandModsDirectoryinto 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:
It falls back to a hardcoded guess. That guess happened to be right, which is why this stayed invisible. The same
mergerToolDiris also where the extension movesMergeInventory.xmlon 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.configbeside 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.GetRawValuefalls 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:
WSM_<key>environment variable<AssemblyName>.dll.configA 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 writesMergedModName. 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, noAppState— so it's unit-testable perWitcherScriptMerger.Tests/CLAUDE.md's safety constraints. It returnsnullfor 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 returnsnullfor a key absent fromApp.config, which used to throw and get swallowed byGet/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-changesclean.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 thatVortexSidecarFileNamestill matches the name Vortex hardcodes.End-to-end against a scratch game tree with the real WinForms build, writing the sidecar exactly the way
setMergerConfigdoes:Can't find Mods directory— exit 1Merged 1 file(s), skipped 0.— exit 0WSM_ModsDirectorybogusCan't find the Mods directory specified in the config file— env wonModsDirectoryin own configI got the precedence test wrong on the first attempt (overrode
GameDirectorywhile the sidecar also setModsDirectory, whichPaths.ModsDirectoryuses 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.Headlessdeliberately does not emit this file: Vortex's extension only ever looks for a merger namedWitcherScriptMerger.exe, so aWitcherScriptMerger.Headless.exe.configwould 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
W3ScriptMergertool 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.