Skip to content

Add config-extensible allowlist for vanilla DLC/expansion bundle folders - #14

Merged
TheValiantOne merged 1 commit into
mainfrom
feature/dlc-folder-allowlist-config
Aug 9, 2026
Merged

Add config-extensible allowlist for vanilla DLC/expansion bundle folders#14
TheValiantOne merged 1 commit into
mainfrom
feature/dlc-folder-allowlist-config

Conversation

@TheValiantOne

Copy link
Copy Markdown
Owner

Summary

  • FileMerger.IsVanillaDlcBundleFolder now has a two-arg overload — IsVanillaDlcBundleFolder(string path, IEnumerable<string> additionalFolderNames) — that ORs the existing DLC[0-9]*/ep[0-9]/bob regex with an exact, case-insensitive match against a caller-supplied allowlist of extra folder names. The single-arg overload now forwards to it with an empty list, so all existing behavior/tests are unchanged.
  • New AdditionalVanillaDlcFolderNames App.config setting (comma-separated exact folder names, empty by default) added to both WitcherScriptMerger/App.config and WitcherScriptMerger.Headless/App.config, so a future DLC/expansion whose folder codename isn't recognized yet (e.g. CD Projekt Red's "Songs of the Past", announced in 2026 with no public folder name at time of writing) can be picked up via config instead of a code change.
  • The setting is parsed and read once, at FileMerger.GetUnpackedFiles' call site only — IsVanillaDlcBundleFolder itself never touches AppState/AppState.Settings, staying a pure, static, directly unit-testable function. This matters here specifically: AppSettings's constructor calls Environment.Exit(1) when it can't find a config file next to the entry assembly, which would kill the entire dotnet test process if touched from code a test exercises (see WitcherScriptMerger.Core/CLAUDE.md and WitcherScriptMerger.Tests/CLAUDE.md).
  • Deliberately stays a strict, exact-match allowlist — never existence-based auto-discovery. Vortex's own witcher3dlc mod type deploys ordinary user mods into <GameDir>\DLC\<modname>\content\..., the identical on-disk shape as real vanilla DLC content, so treating "any folder under DLC" as a vanilla merge baseline would risk silently merging a conflict against a mod's own bundle instead of vanilla's.

A real bug found (and fixed) via code review

Running the code-review skill on this diff caught a genuine, pre-existing bug in the base regex this new overload ORs against: VanillaDlcBundleFolderPattern ("(DLC[0-9]*|ep[0-9]|bob)$") was matched against the full path with only an end anchor. Since .NET's Regex.IsMatch has no implicit start anchor, this matched any folder name merely ending in one of those substrings — not just a folder name that is one of them (optionally + digits). Confirmed: "ImmersiveDLC" (ends in "DLC") and "Step1" (ends in "ep1") both incorrectly matched. This is exactly the Vortex-mod-folder-name-collision risk this feature's own allowlist rule exists to prevent, so it was fixed as part of this PR: the regex is now "^(DLC[0-9]*|ep[0-9]|bob)$", matched against just the extracted folder-name segment (not the raw path) — which also resolves a related inconsistency where the regex checked the raw path while the allowlist checked an already separator-trimmed segment. Regression-tested via IsVanillaDlcBundleFolder_FolderNameMerelyEndsInPattern_ReturnsFalse.

Also fixed on review: allowlist config entries are now trimmed of stray trailing directory separators (e.g. a user typing SongsOfThePast\), not just whitespace, matching the normalization already applied to the path side.

Test plan

  • dotnet build WitcherScriptMerger.sln — succeeds, exactly the same 5 pre-existing warnings as main, no new ones (confirmed via --no-incremental full rebuild).
  • dotnet test WitcherScriptMerger.sln — all 67 tests pass (16 new: extra-name allowlist matching incl. a trailing path separator, empty-list non-regression against every existing regex case, case-insensitivity for extra names, an extra name absent from the list still returning false, a null-list guard, and the anchoring-bug regression above).
  • dotnet format whitespace WitcherScriptMerger.sln --verify-no-changes — clean.
  • E2E: a full merge CLI run through actual bundle-content unpacking wasn't possible in this environment — no quickbms.exe/wcc_lite.exe available, and bundle-content conflicts are never even detected during ModFileIndex.BuildAsync's scan without a real QuickBMS install (QuickBms.GetBundleContentPaths gracefully returns Array.Empty<string>() without it), so the vanilla-bundle-search code this PR touches is unreachable via the full pipeline without QuickBMS. Instead, ran a disposable, non-committed scratch console app (per the pattern WitcherScriptMerger.Tests/CLAUDE.md documents for exactly this situation) that builds a real scratch filesystem tree (DLC\DLC1, DLC\SongsOfThePast, DLC\ImmersiveDLC, DLC\SomeVortexMod, each with a content\bundles\*.bundle file) and calls the real, shipped FileMerger.IsVanillaDlcBundleFolder two-arg overload via FileMerger.GetUnpackedFiles' exact directory-discovery LINQ shape. Confirmed: without the config entry, the synthetic SongsOfThePast folder is excluded; with AdditionalVanillaDlcFolderNames=SongsOfThePast, it's included as a vanilla-baseline candidate; ImmersiveDLC/SomeVortexMod are never matched either way. This is a real exercise of the shipped code against real Directory.GetDirectories() output, just stopping short of the QuickBMS-dependent unpack step.

AI disclosure

This PR was substantially produced with Claude Code (Claude Sonnet 5), per CONTRIBUTING.md's AI-assisted-development disclosure requirement. Commits carry Co-Authored-By/Claude-Session trailers.

https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah

FileMerger.IsVanillaDlcBundleFolder now has a two-arg overload that ORs the
existing DLC1/DLC2/.../ep1/ep2/bob regex with an exact, case-insensitive
match against a caller-supplied list of extra folder names, so a future
DLC/expansion whose folder codename isn't in the built-in regex yet (e.g.
CD Projekt Red's "Songs of the Past", announced 2026, no folder name public
yet) can be recognized via config instead of a code change. Wired up via a
new "AdditionalVanillaDlcFolderNames" App.config setting (comma-separated
exact folder names, empty by default), read once at FileMerger.
GetUnpackedFiles' call site and passed in - IsVanillaDlcBundleFolder itself
stays a pure, static, AppState-free function, since touching AppState.
Settings from code a test exercises can crash the whole dotnet test process
(see WitcherScriptMerger.Core/CLAUDE.md and WitcherScriptMerger.Tests/
CLAUDE.md). Deliberately stays a strict allowlist, never existence-based
auto-discovery: Vortex's "witcher3dlc" mod type deploys ordinary user mods
into the identical GameDirectory\DLC\<modname>\content\... shape as real
vanilla DLC content.

Code review on this change caught a real, pre-existing bug in the base
regex it ORs against: VanillaDlcBundleFolderPattern was end-anchored but
matched against the full path with no start anchor, so it matched any
folder name merely *ending* in a recognized substring (e.g. "ImmersiveDLC"
or "Step1"), not just a folder name that IS one - exactly the kind of
arbitrary mod-folder-name collision this allowlist's own "no
auto-discovery" rule exists to prevent. Fixed by matching a fully
"^...$"-anchored pattern against just the extracted folder-name segment
instead, which also resolves a related inconsistency (regex checked the
raw path, the allowlist checked a separator-trimmed segment).

Verified: dotnet build/test/format all clean, 67 tests passing (16 new -
extra-name matching, case-insensitivity, empty-list non-regression, the
anchoring-bug regression, and a null-list guard). Also ran a disposable,
non-committed scratch console app exercising the real two-arg overload
against a real scratch filesystem tree, confirming a synthetic
"SongsOfThePast" DLC folder is excluded without the config entry and
included with it, while an "ImmersiveDLC"/"SomeVortexMod" folder is never
matched either way - a full `merge` CLI run wasn't possible in this
environment since QuickBMS/wcc_lite aren't available (bundle-content
conflict detection itself requires QuickBMS during scanning, before this
code even runs).

AI-assisted: substantially produced with Claude Code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
@TheValiantOne
TheValiantOne merged commit 8702e7b 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