fix: seed redist markers again when the registry lost them (#793)

* fix: redist markers a replaced registry lost are seeded again

droiddeck-seed-redists recorded what it seeded in .droiddeck-redists and
trusted that record from then on. A Proton update replaces its
default_pfx system.reg but leaves our record beside it, and every new
prefix copies both, so new games got a record of markers their registry
never had: Steam then ran their install scripts for real. Batman: Arkham
City's AMD Dual-Core Optimizer installer hangs there, leaving the game
on "Running install script" on every launch.

Each recorded marker is now looked up in the registry, in both views
where both were written, and seeded again when it is gone; a record of
markers nobody wants any more is rewritten without them.

* Check every VC runtime architecture before keeping the vcrt marker

vcrt writes Installed for x86, x64 and arm64 in both registry views, but
present() only looked at x86. If only an x64 or arm64 value was gone the
runtime would not be seeded again. Require all six values and test the
loss of the arm64 Wow6432Node one.
This commit is contained in:
Tjebbe Kerstens authored and GitHub committed 2026-10-14 13:52:07 -04:00
1 parent 6528a7ed64
commit 2e65efcfa7
2 files changed
+103

No files matched your search

@@ -139,6 +139,51 @@ unseed() {
return 1
}
# The ledger lines whose values the registry still holds, in both views where we wrote both. A Proton
# update replaces its default_pfx system.reg but not our ledger beside it, and new prefixes copy both.
present() { awk -F'\t' '
NR == FNR {
path = $1
if (path != "vcrt") {
sub(/^HKEY_LOCAL_MACHINE\\\\/, "", path)
if (path == $1)
next
}
n++
line[n] = $0
need[n] = 0
if (path == "vcrt") {
for (a = split("x86 x64 arm64", arch, " "); a > 0; a--) {
runtime = "microsoft\\\\visualstudio\\\\14.0\\\\vc\\\\runtimes\\\\" arch[a]
key[n, ++need[n]] = "[software\\\\" runtime "]" SUBSEP "installed"
key[n, ++need[n]] = "[software\\\\wow6432node\\\\" runtime "]" SUBSEP "installed"
}
} else {
both = substr(path, 1, 10) == "Software\\\\"
path = tolower(path)
key[n, ++need[n]] = "[" path "]" SUBSEP tolower($2)
if (both)
key[n, ++need[n]] = "[software\\\\wow6432node\\\\" substr(path, 11) "]" SUBSEP tolower($2)
}
for (j = 1; j <= need[n]; j++) wanted[key[n, j]] = 1
next
}
/^\[/ { section = tolower(substr($0, 1, index($0, "]"))) }
/^"/ {
value = substr($0, 2)
value = tolower(substr(value, 1, index(value, "\"=") - 1))
if ((section SUBSEP value) in wanted)
seen[section SUBSEP value] = 1
}
END {
for (i = 1; i <= n; i++) {
ok = 1
for (j = 1; j <= need[i]; j++) if (!(key[i, j] in seen)) ok = 0
if (ok) print line[i]
}
}
' - "$1"; }
for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/system.reg \
/mnt/droiddeck-sd/steamapps/common/Proton*/files/share/default_pfx*/system.reg \
/root/Games/*/steamapps/common/Proton*/files/share/default_pfx*/system.reg \
@@ -166,6 +211,17 @@ for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/syste
printf '%s\n' "${kept[@]}" >"$seeded"
echo "== took Vulkan runtime markers out of $reg"
fi
if [ ${#had[@]} -gt 0 ]; then
declare -A held=()
while IFS= read -r line; do [[ $line == *$'\t'* ]] && held[$line]=1; done < <(printf '%s\n' "${!had[@]}" | present "$reg")
if [ ${#held[@]} -lt ${#had[@]} ]; then
echo "== $((${#had[@]} - ${#held[@]})) recorded redist markers are gone from $reg"
had=()
for line in "${!held[@]}"; do had[$line]=1; done
printf '%s\n' "${!had[@]}" >"$seeded"
fi
unset held
fi
missing=()
for line in "${want[@]}"; do [ -n "${had[$line]:-}" ] || missing+=("$line"); done
[ ${#missing[@]} -eq 0 ] && continue
+47
View File
@@ -100,6 +100,53 @@ class SeedRedistsTest(unittest.TestCase):
self.run_seed()
self.assertIn('"VCRedist2022"=dword:00000001', self.reg.read_text())
def test_markers_a_replaced_registry_lost_are_seeded_again(self):
self.game()
stock = self.reg.read_text()
self.run_seed()
ledger = (self.reg.parent / ".droiddeck-redists").read_text()
self.assertIn("VCRedist2022", ledger)
self.reg.write_text(stock)
self.run_seed()
reg = self.reg.read_text()
self.assertEqual(reg.count('"VCRedist2022"=dword:00000001'), 2, "both views again")
self.assertIn("[Software\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\x86]", reg)
self.assertEqual(sorted((self.reg.parent / ".droiddeck-redists").read_text().splitlines()), sorted(ledger.splitlines()))
self.run_seed()
self.assertEqual(reg, self.reg.read_text())
def test_a_marker_kept_in_only_one_view_is_seeded_again(self):
self.game()
self.run_seed()
wow = "[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022]"
blocks = self.reg.read_text().split("\n\n")
self.reg.write_text("\n\n".join(b for b in blocks if not (b.startswith(wow) and '"VCRedist2022"' in b)))
self.assertEqual(self.reg.read_text().count('"VCRedist2022"'), 1)
self.run_seed()
reg = self.reg.read_text()
self.assertEqual(reg.count('"VCRedist2022"'), 3, "the one left plus both views again")
self.assertTrue(any(b.startswith(wow) and '"VCRedist2022"' in b for b in reg.split("\n\n")))
def test_a_vc_runtime_another_architecture_lost_is_seeded_again(self):
self.run_seed()
arm64 = "[Software\\\\Wow6432Node\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\arm64]"
blocks = self.reg.read_text().split("\n\n")
self.reg.write_text("\n\n".join(b for b in blocks if not b.startswith(arm64)))
self.assertNotIn(arm64, self.reg.read_text())
self.run_seed()
reg = self.reg.read_text()
self.assertIn(arm64, reg)
self.assertEqual(reg.count("[Software\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\x86]"), 2)
def test_a_marker_written_in_one_view_only_is_kept_as_it_is(self):
self.game(script=GAME_SCRIPT.replace("Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022", "SOFTWARE\\\\AGEIA Technologies").replace("VCRedist2022", "PhysX Version"))
self.run_seed()
once = self.reg.read_text()
self.assertEqual(once.count('"PhysX Version"=dword:00000001'), 1)
result = self.run_seed()
self.assertEqual(once, self.reg.read_text())
self.assertNotIn("gone", result.stdout)
def test_an_evaluator_script_nested_deeper_is_read(self):
script = self.tmp / "legacycompat/evaluatorscript_409710.vdf"
script.parent.mkdir(parents=True)