mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
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:
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 = []
|
||||
|
||||
@@ -137,7 +137,7 @@ class WinComponentsTest(unittest.TestCase):
|
||||
self.pick("openal")
|
||||
globals_ = GAME_ENV["win_components"].__globals__
|
||||
real = runpy.run_path(str(BIN / "droiddeck-wincomponents"))
|
||||
real["apply"].__defaults__ = (self.selection, self.store)
|
||||
real["apply"].__defaults__ = (self.selection, self.store, self.selection.parent / "no-pending.json", None)
|
||||
with mock.patch.dict(globals_, {"runpy": mock.Mock(run_path=lambda _path: real)}):
|
||||
self.assertEqual(GAME_ENV["win_components"](str(self.compat) + "/", "2571976725"), "openal32=n,b")
|
||||
self.assertEqual(GAME_ENV["win_components"]("", "2571976725"), "")
|
||||
@@ -166,5 +166,58 @@ class WinComponentsTest(unittest.TestCase):
|
||||
self.assertEqual(MODULE["overrides_value"](["a", "b=n", "a=b", "bad;name", "c=x"]), "b=n;a=b")
|
||||
|
||||
|
||||
class PendingWaitTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
root = Path(self.tmp.name)
|
||||
self.store = root / "store"
|
||||
self.pending = root / "pending.json"
|
||||
self.slept = []
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def install(self, cid):
|
||||
folder = self.store / cid
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
(folder / "component.json").write_text(json.dumps({"id": cid, "overrides": []}))
|
||||
|
||||
def clock(self):
|
||||
return len(self.slept) * 0.5
|
||||
|
||||
def wait(self, timeout=10.0, on_sleep=None):
|
||||
def sleep(seconds):
|
||||
self.slept.append(seconds)
|
||||
if on_sleep:
|
||||
on_sleep(len(self.slept))
|
||||
return MODULE["wait_for_pending"]("1262580", self.pending, self.store, timeout, sleep, self.clock)
|
||||
|
||||
def test_nothing_pending_means_no_wait(self):
|
||||
self.assertIsNone(self.wait())
|
||||
self.pending.write_text(json.dumps({"games": {"999": ["d3dx9"]}}))
|
||||
self.assertIsNone(self.wait())
|
||||
self.install("d3dx9")
|
||||
self.pending.write_text(json.dumps({"games": {"1262580": ["d3dx9"]}}))
|
||||
self.assertIsNone(self.wait())
|
||||
self.assertEqual([], self.slept)
|
||||
|
||||
def test_waits_until_the_component_lands(self):
|
||||
self.pending.write_text(json.dumps({"games": {"1262580": ["vcredist2019_dll"]}}))
|
||||
note = self.wait(on_sleep=lambda n: self.install("vcredist2019_dll") if n == 3 else None)
|
||||
self.assertEqual(3, len(self.slept))
|
||||
self.assertEqual("wincomp: 1262580 waited 1500ms for vcredist2019_dll", note)
|
||||
|
||||
def test_gives_up_cleanly_after_the_timeout(self):
|
||||
self.pending.write_text(json.dumps({"games": {"1262580": ["vcredist2019_dll", "d3dx9"]}}))
|
||||
self.install("d3dx9")
|
||||
note = self.wait(timeout=2.0)
|
||||
self.assertEqual(4, len(self.slept))
|
||||
self.assertEqual("wincomp: 1262580 launched without vcredist2019_dll (not ready)", note)
|
||||
|
||||
def test_a_damaged_pending_file_is_no_wait(self):
|
||||
self.pending.write_text("{not json")
|
||||
self.assertIsNone(self.wait())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user