mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
fix: GOG games miss the registry entries their install script sets (#1159)
A GOG download only places the files, so games that look up their own registry keys (Fallout: New Vegas reads HKLM\Software\Bethesda Softworks\FalloutNV "Installed Path") said they were not installed. Every GOG launch now reads the setRegistry actions of the game's goggame-<id>.script and imports them into the prefix's 32-bit view through Proton's reg.exe, once per change of the script, so games already downloaded are fixed on their next launch.
This commit is contained in:
1 parent
6ef042487a
commit
79f43d8def
3 files changed
+215
-1
No files matched your search
@@ -47,7 +47,7 @@ Compatibility and performance vary by device; hardware validation is limited. De
|
||||
|
||||
## Credits and licence
|
||||
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc/<pid>/mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. The registry entries a GOG game's install script (`goggame-<id>.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc/<pid>/mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
|
||||
XR glasses support builds on WinNative's VITURE glasses work. VITURE protocol details were checked against [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) by [@wheaney](https://github.com/wheaney), including the VITURE protocol header it ships, [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) by [@badicsalex](https://github.com/badicsalex) and the [go-macos/iokit VITURE package](https://pkg.go.dev/github.com/go-macos/iokit/viture); the optional VITURE SDK belongs to VITURE and is not included. RayNeo support adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0); see [docs/third-party/rayneo](docs/third-party/rayneo/README.md). XREAL 1S support references [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) by [@rohitsangwan01](https://github.com/rohitsangwan01) (MIT) and [xr-tools](https://github.com/0xcaff/xr-tools) by [@0xcaff](https://github.com/0xcaff). The REDMAGIC glasses-display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube). VITURE, RayNeo, XREAL and REDMAGIC are trademarks of their owners; this project is not affiliated with them.
|
||||
|
||||
|
||||
@@ -28,8 +28,11 @@ build wrote is removed - with it the game exited while loading.
|
||||
droiddeck-store-launch <the arguments Steam gave the compat tool>
|
||||
"""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
@@ -364,6 +367,123 @@ def prepare_gog(sidecar, compat_data, launch_dir):
|
||||
return state == "ready"
|
||||
|
||||
|
||||
GOG_SCRIPTS = "goggame-*.script"
|
||||
GOG_REGISTRY_MARK = ".droiddeck-gog-registry"
|
||||
GOG_ROOTS = {"HKEY_LOCAL_MACHINE": "HKEY_LOCAL_MACHINE", "HKLM": "HKEY_LOCAL_MACHINE",
|
||||
"HKEY_CURRENT_USER": "HKEY_CURRENT_USER", "HKCU": "HKEY_CURRENT_USER",
|
||||
"HKEY_CLASSES_ROOT": "HKEY_CLASSES_ROOT", "HKCR": "HKEY_CLASSES_ROOT"}
|
||||
WINE_USER = "C:\\users\\steamuser"
|
||||
|
||||
|
||||
def gog_paths(folder, product):
|
||||
app = windows_path(str(folder))
|
||||
support = app + "\\support\\" + product
|
||||
return {"app": app, "productid": product, "support": support, "supportdir": support,
|
||||
"localappdata": WINE_USER + "\\AppData\\Local", "userdocs": WINE_USER + "\\Documents"}
|
||||
|
||||
|
||||
def gog_expand(text, paths):
|
||||
return re.sub(r"\{([A-Za-z]+)\}", lambda m: paths[m.group(1).lower()], text)
|
||||
|
||||
|
||||
def gog_number(data):
|
||||
if isinstance(data, bool):
|
||||
raise ValueError
|
||||
if isinstance(data, int):
|
||||
return data
|
||||
text = str(data).strip()
|
||||
try:
|
||||
return int(text, 0)
|
||||
except ValueError:
|
||||
return int(text, 10)
|
||||
|
||||
|
||||
def gog_value(kind, data):
|
||||
kind = str(kind).lower()
|
||||
if kind == "string":
|
||||
text = str(data)
|
||||
if any(c in text for c in "\r\n\0"):
|
||||
return None
|
||||
return '"%s"' % reg_escape(text)
|
||||
if kind == "dword":
|
||||
return "dword:%08x" % (gog_number(data) & 0xFFFFFFFF)
|
||||
if kind == "binary":
|
||||
return "hex:" + ",".join("%02x" % b for b in base64.b64decode(str(data), validate=True))
|
||||
return None
|
||||
|
||||
|
||||
def gog_entry(arguments, paths):
|
||||
if not isinstance(arguments, dict):
|
||||
return None
|
||||
try:
|
||||
root = GOG_ROOTS.get(str(arguments.get("root") or "").upper())
|
||||
subkey = gog_expand(str(arguments.get("subkey") or ""), paths).strip("\\")
|
||||
if root is None or not subkey or any(c in subkey for c in "\r\n\0"):
|
||||
return None
|
||||
section = "[%s\\%s]" % (root, subkey)
|
||||
if not arguments.get("valueType"):
|
||||
return section
|
||||
data = arguments.get("valueData")
|
||||
value = gog_value(arguments["valueType"], gog_expand(data, paths) if isinstance(data, str) else data)
|
||||
name = gog_expand(str(arguments.get("valueName") or ""), paths)
|
||||
if value is None or any(c in name for c in "\r\n\0"):
|
||||
return None
|
||||
return section + "\r\n" + ('"%s"' % reg_escape(name) if name else "@") + "=" + value
|
||||
except (KeyError, ValueError, TypeError, binascii.Error):
|
||||
return None
|
||||
|
||||
|
||||
def gog_registry(folder):
|
||||
entries, skipped = [], 0
|
||||
for script in sorted(Path(folder).glob(GOG_SCRIPTS)):
|
||||
product = script.name[len("goggame-"):-len(".script")]
|
||||
try:
|
||||
actions = json.loads(script.read_text(encoding="utf-8-sig")).get("actions")
|
||||
except (OSError, ValueError, AttributeError):
|
||||
continue
|
||||
paths = gog_paths(folder, product)
|
||||
for action in actions if isinstance(actions, list) else []:
|
||||
install = action.get("install") if isinstance(action, dict) else None
|
||||
if not isinstance(install, dict) or install.get("action") != "setRegistry":
|
||||
continue
|
||||
entry = gog_entry(install.get("arguments"), paths)
|
||||
if entry is None:
|
||||
skipped += 1
|
||||
else:
|
||||
entries.append(entry)
|
||||
return entries, skipped
|
||||
|
||||
|
||||
def apply_gog_registry(folder, compat_data, proton, timeout=90):
|
||||
entries, skipped = gog_registry(folder)
|
||||
note = " skipped=%d" % skipped if skipped else ""
|
||||
if not entries:
|
||||
return "none" + note
|
||||
if not compat_data or not proton or not Path(proton).is_file():
|
||||
return "no-proton" + note
|
||||
text = "Windows Registry Editor Version 5.00\r\n\r\n" + "".join(entry + "\r\n\r\n" for entry in entries)
|
||||
stamp = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
pfx = Path(compat_data) / "pfx"
|
||||
marker = pfx / GOG_REGISTRY_MARK
|
||||
try:
|
||||
if marker.is_file() and marker.read_text() == stamp and (pfx / "system.reg").is_file():
|
||||
return "ready" + note
|
||||
Path(compat_data).mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix="droiddeck-gog-") as tmp:
|
||||
source = Path(tmp) / "registry.reg"
|
||||
source.write_bytes(b"\xff\xfe" + text.encode("utf-16-le"))
|
||||
run = subprocess.run([proton, "run", "reg.exe", "import", windows_path(str(source)), "/reg:32"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout)
|
||||
if run.returncode or not (pfx / "system.reg").is_file():
|
||||
return "import-failed" + note
|
||||
marker.write_text(stamp)
|
||||
return "written=%d%s" % (len(entries), note)
|
||||
except subprocess.TimeoutExpired:
|
||||
return "timeout" + note
|
||||
except OSError:
|
||||
return "import-failed" + note
|
||||
|
||||
|
||||
def main(argv):
|
||||
if argv and argv[0] == "--exited":
|
||||
rest, code = argv[1:], 0
|
||||
@@ -384,6 +504,7 @@ def main(argv):
|
||||
launch_dir = os.environ.get("BL_LAUNCH_DIR", "")
|
||||
if store == "gog":
|
||||
prepare_gog(sidecar, compat_data, launch_dir)
|
||||
log("gog registry: " + apply_gog_registry(folder, compat_data, os.environ.get("BL_STORE_PROTON", "")))
|
||||
if store == "epic":
|
||||
handled = os.environ.get(EPIC_HANDLED) == "1" and (folder / EPIC_ARGS).is_file()
|
||||
if not handled:
|
||||
|
||||
@@ -314,3 +314,96 @@ class GogAchievements(unittest.TestCase):
|
||||
with patch.object(store, 'COMET_DIR', base), patch.object(store.subprocess, 'run', return_value=__import__('subprocess').CompletedProcess([], 1)):
|
||||
self.assertEqual('service-unavailable', store.provision_galaxy(str(base / 'compat'), str(base / 'proton')))
|
||||
self.assertFalse((base / 'compat/pfx/.droiddeck-comet-service').exists())
|
||||
|
||||
|
||||
def gog_script(folder, product, actions):
|
||||
(folder / ('goggame-%s.script' % product)).write_text(json.dumps({'actions': actions, 'baseProductId': product, 'version': 1}))
|
||||
|
||||
|
||||
def set_registry(**arguments):
|
||||
return {'install': {'action': 'setRegistry', 'arguments': arguments}, 'name': 'setRegistry'}
|
||||
|
||||
|
||||
class GogRegistry(unittest.TestCase):
|
||||
def test_the_scripts_registry_values_are_read_with_the_games_paths(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
folder = game(root, store_id='gog', ident='1454587428')
|
||||
gog_script(folder, '1454587428', [
|
||||
set_registry(root='HKEY_LOCAL_MACHINE', subkey='Software\\Bethesda Softworks\\FalloutNV',
|
||||
valueName='Installed Path', valueData='{app}\\', valueType='string'),
|
||||
set_registry(root='HKEY_LOCAL_MACHINE', subkey='Software\\GOG.com\\Games\\{productId}',
|
||||
valueName='buildId', valueData='42', valueType='dword'),
|
||||
set_registry(root='HKEY_CURRENT_USER', subkey='Software\\Game', valueName='', valueData='AAH/', valueType='binary'),
|
||||
set_registry(root='HKEY_LOCAL_MACHINE', subkey='Software\\Empty'),
|
||||
set_registry(root='HKEY_LOCAL_MACHINE', subkey='Software\\Game', valueName='x', valueData='1', valueType='qword'),
|
||||
set_registry(root='HKEY_LOCAL_MACHINE', subkey='Software\\Game', valueName='x', valueData='{nowhere}', valueType='string'),
|
||||
set_registry(root='HKEY_USERS', subkey='S-1-5', valueName='x', valueData='1', valueType='string'),
|
||||
{'install': {'action': 'Execute', 'arguments': {'executable': 'setup.exe'}}},
|
||||
'nonsense',
|
||||
])
|
||||
entries, skipped = store.gog_registry(folder)
|
||||
app = store.windows_path(str(folder)).replace('\\', '\\\\')
|
||||
self.assertEqual([
|
||||
'[HKEY_LOCAL_MACHINE\\Software\\Bethesda Softworks\\FalloutNV]\r\n"Installed Path"="%s\\\\"' % app,
|
||||
'[HKEY_LOCAL_MACHINE\\Software\\GOG.com\\Games\\1454587428]\r\n"buildId"=dword:0000002a',
|
||||
'[HKEY_CURRENT_USER\\Software\\Game]\r\n@=hex:00,01,ff',
|
||||
'[HKEY_LOCAL_MACHINE\\Software\\Empty]',
|
||||
], entries)
|
||||
self.assertEqual(3, skipped)
|
||||
|
||||
def test_a_game_without_a_script_starts_without_proton(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
folder = game(root, store_id='gog')
|
||||
with patch.object(store.subprocess, 'run') as run:
|
||||
self.assertEqual('none', store.apply_gog_registry(folder, root + '/compat', root + '/proton'))
|
||||
run.assert_not_called()
|
||||
|
||||
def test_values_are_imported_into_the_32_bit_view_once_until_the_script_changes(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
folder = game(root, store_id='gog', ident='1454587428')
|
||||
proton = Path(root) / 'proton'
|
||||
proton.write_text('selected Proton')
|
||||
data = Path(root) / 'compat'
|
||||
action = set_registry(root='HKLM', subkey='Software\\Bethesda Softworks\\FalloutNV',
|
||||
valueName='Installed Path', valueData='{app}', valueType='string')
|
||||
gog_script(folder, '1454587428', [action])
|
||||
imported = []
|
||||
def run(argv, **kwargs):
|
||||
self.assertEqual([str(proton), 'run', 'reg.exe', 'import'], argv[:4])
|
||||
self.assertEqual('/reg:32', argv[5])
|
||||
raw = Path(argv[4][2:].replace('\\', '/')).read_bytes()
|
||||
self.assertTrue(raw.startswith(b'\xff\xfe'))
|
||||
imported.append(raw[2:].decode('utf-16-le'))
|
||||
(data / 'pfx').mkdir(parents=True, exist_ok=True)
|
||||
(data / 'pfx/system.reg').write_text('Wine registry')
|
||||
return __import__('subprocess').CompletedProcess(argv, 0)
|
||||
with patch.object(store.subprocess, 'run', side_effect=run):
|
||||
self.assertEqual('written=1', store.apply_gog_registry(folder, str(data), str(proton)))
|
||||
self.assertTrue(imported[0].startswith('Windows Registry Editor Version 5.00\r\n'))
|
||||
self.assertIn('[HKEY_LOCAL_MACHINE\\Software\\Bethesda Softworks\\FalloutNV]', imported[0])
|
||||
self.assertEqual('ready', store.apply_gog_registry(folder, str(data), str(proton)))
|
||||
self.assertEqual(1, len(imported))
|
||||
action['install']['arguments']['valueData'] = '{app}\\'
|
||||
gog_script(folder, '1454587428', [action])
|
||||
self.assertEqual('written=1', store.apply_gog_registry(folder, str(data), str(proton)))
|
||||
self.assertEqual(2, len(imported))
|
||||
|
||||
def test_a_failed_import_is_tried_again_next_launch(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
folder = game(root, store_id='gog', ident='7')
|
||||
proton = Path(root) / 'proton'
|
||||
proton.write_text('selected Proton')
|
||||
gog_script(folder, '7', [set_registry(root='HKLM', subkey='Software\\Game', valueName='a', valueData='b', valueType='string')])
|
||||
with patch.object(store.subprocess, 'run', return_value=__import__('subprocess').CompletedProcess([], 1)):
|
||||
self.assertEqual('import-failed', store.apply_gog_registry(folder, root + '/compat', str(proton)))
|
||||
self.assertFalse((Path(root) / 'compat/pfx' / store.GOG_REGISTRY_MARK).exists())
|
||||
self.assertEqual('no-proton', store.apply_gog_registry(folder, root + '/compat', root + '/missing'))
|
||||
|
||||
def test_every_gog_launch_brings_the_registry_in(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
folder = game(root, store_id='gog')
|
||||
with patch.dict(os.environ, {'BL_LAUNCH_DIR': root, 'STEAM_COMPAT_DATA_PATH': root + '/compat/', 'BL_STORE_PROTON': '/chosen/proton'}), \
|
||||
patch.object(store, 'prepare_gog', return_value=False), patch.object(store, 'ask', return_value=None), \
|
||||
patch.object(store, 'apply_gog_registry', return_value='ready') as apply:
|
||||
store.main([str(folder / store.LAUNCHER)])
|
||||
apply.assert_called_once_with(folder, root + '/compat', '/chosen/proton')
|
||||
Reference in new issue
Block a user