Windows components: picked automatically by each game's source (#554)

Each game's Windows components now come from the list its own source
trusts, and the ones that list names turn on by themselves. The user's own
on/off switches always win; each automatic row says why it is on.

- Steam games: the Steamworks Shared redistributables Steam installs with
  the game (appmanifest SharedDepots, else the client's appinfo.vdf).
  Steam keeps its install scripts marked as already run.
- GOG: the build manifest's dependencies. Epic: the manifest's
  prerequisite (Unreal's maps to VC++ and DirectX). Amazon: fuel.json
  PostInstall. Older GOG/Epic installs get their list fetched once with the
  saved sign-in; game files are never downloaded again.
- Custom games (the + button, Add all, migrated folders): one shared Steam
  match per game, also used for artwork. An appid from the game's files
  turns components on; a name-only match stays Recommended; no match falls
  back to the folder scan.
- Steam app info for apps the client never saw comes from an anonymous
  Steam PICS lookup (CM websocket, anonymous logon, product info), cached
  per appid; the user's Steam sign-in is never used.
- Missing components download in the background on one low-priority
  worker that pauses during sessions and waits on battery saver, heat or a
  metered network unless the user asked. A game Steam installs during a
  session is readied right away so it can be played without ending the
  session; the launch waits up to 10 s only for a component still on its
  way.
- Page: "On for this game" with reasons, a quiet "Checking what this game
  needs..." only for games never worked out, rows that ease in, pieces that
  need a download greyed out while offline. The intro names no other app.
- The folder scan skips Unity's *_Data/Managed (Mono, not .NET Framework).
This commit is contained in:
The412Banner authored and GitHub committed 2026-10-10 21:15:56 -04:00
1 parent d45933e559
commit a2e596a200
35 files changed
+3810 -146

No files matched your search

@@ -32,6 +32,10 @@ from pathlib import Path
STORE = Path("/opt/droiddeck/wincomponents")
SELECTION = Path.home() / ".config/droiddeck/wincomponents.json"
# Components the app is still downloading for a game Steam installed during this session:
# {"games": {"<appid>": ["vcredist2019_dll", ...]}}. A launch waits a little for them.
PENDING = Path.home() / ".config/droiddeck/wincomponents-pending.json"
PENDING_WAIT = 10.0
# What this prefix holds from us, so a component the user turns off is taken out again.
STATE = ".droiddeck-wincomponents.json"
# A file ours replaced that was not Proton's (a real installer's, the user's own), put back when the
@@ -518,15 +522,47 @@ def save_state(compat, state):
os.replace(temp, compat / STATE)
def apply(compat_path, app_id, selection=SELECTION, store=STORE):
def pending_for(app_id, pending=PENDING):
"""The component ids the app is still getting ready for app_id; empty when none (or no file)."""
try:
with open(pending, encoding="utf-8") as source:
ids = json.load(source).get("games", {}).get(str(app_id), [])
except (OSError, ValueError, AttributeError):
return []
return [i for i in ids if valid_id(i)] if isinstance(ids, list) else []
def wait_for_pending(app_id, pending=PENDING, store=STORE, timeout=PENDING_WAIT, sleep=time.sleep, clock=time.monotonic):
"""Waits (at most timeout seconds) for the components still being got ready for app_id to be in
the store or dropped from the pending list. No wait at all when nothing is pending. Returns a
note for the log, or None."""
def missing():
return [cid for cid in pending_for(app_id, pending) if component(store, cid) is None]
left = missing()
if not left:
return None
asked = list(left)
start = clock()
while left and clock() - start < timeout:
sleep(0.5)
left = missing()
waited = int((clock() - start) * 1000)
if left:
return "wincomp: %s launched without %s (not ready)" % (app_id, ",".join(left))
return "wincomp: %s waited %dms for %s" % (app_id, waited, ",".join(asked))
def apply(compat_path, app_id, selection=SELECTION, store=STORE, pending=PENDING, wait=wait_for_pending):
"""Brings the prefix in line with the game's picks. Returns (overrides, notes)."""
compat = Path(compat_path)
pfx = compat / "pfx"
# A game Steam installed during this session may have components still on their way.
waited = wait(app_id, pending, store) if wait else None
ids = wanted(app_id, selection)
state = load_state(compat)
if not ids and not state["files"] and not state.get("registry"):
return [], []
notes, overrides, keep = [], [], {}
notes, overrides, keep = ([waited] if waited else []), [], {}
copied = 0
installed = []
values = []