import importlib.machinery import importlib.util import json import os from pathlib import Path import tempfile import threading import time import unittest from unittest.mock import patch BIN = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin' def load(name): loader = importlib.machinery.SourceFileLoader(name.replace('-', '_'), str(BIN / name)) spec = importlib.util.spec_from_loader(loader.name, loader) module = importlib.util.module_from_spec(spec) loader.exec_module(module) return module store = load('droiddeck-store-launch') def load_path(path, name): loader = importlib.machinery.SourceFileLoader(name, str(path)) spec = importlib.util.spec_from_loader(name, loader) module = importlib.util.module_from_spec(spec) loader.exec_module(module) return module compat = load('steam-compatibility') def game(root, store_id='epic', ident='8b6a0e14'): folder = Path(root) / 'Games/Stores/Epic/Metalstorm' folder.mkdir(parents=True) (folder / '.droiddeck-store.json').write_text(json.dumps({'store': store_id, 'id': ident, 'title': 'Metalstorm', 'exe': 'Metalstorm.exe'})) (folder / '.droiddeck-launch.bat').write_text('@echo off\r\n') return folder class Detection(unittest.TestCase): def test_the_launcher_among_steams_arguments_names_the_game(self): with tempfile.TemporaryDirectory() as root: folder = game(root) found = store.find_store_game(['/usr/bin/steam-runtime', '"%s/.droiddeck-launch.bat"' % folder, '-flag']) self.assertIsNotNone(found) self.assertEqual(folder, found[0]) self.assertEqual('epic', found[1]['store']) self.assertEqual('8b6a0e14', found[1]['id']) def test_anything_else_is_left_alone(self): with tempfile.TemporaryDirectory() as root: folder = game(root) self.assertEqual(folder, store.find_store_game([str(folder / 'Metalstorm.exe')])[0]) self.assertIsNone(store.find_store_game([str(Path(root) / 'elsewhere/readme.txt')])) (folder / '.droiddeck-store.json').unlink() self.assertIsNone(store.find_store_game([str(folder / '.droiddeck-launch.bat')])) def test_only_epic_asks_for_a_code(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog') sc = json.loads((folder / '.droiddeck-store.json').read_text()); sc['cloud'] = False (folder / '.droiddeck-store.json').write_text(json.dumps(sc)) os.environ['BL_LAUNCH_DIR'] = str(Path(root) / 'session') try: with patch.object(store, 'ask', return_value={'ready': False, 'reason': 'off'}) as ask: self.assertEqual(0, store.main([str(folder / '.droiddeck-launch.bat')])) self.assertEqual('gog-achievements', ask.call_args.args[1]['op']) finally: del os.environ['BL_LAUNCH_DIR'] class Asking(unittest.TestCase): def test_a_request_is_answered_through_the_resp_folder(self): with tempfile.TemporaryDirectory() as root: channel = Path(root) / 'stores' def app(): req = channel / 'req' for _ in range(100): names = [p for p in req.glob('*.json')] if req.exists() else [] if names: request = json.loads(names[0].read_text()) self.assertEqual({'op': 'epic-code', 'store': 'epic', 'id': 'x'}, request) names[0].unlink() tmp = channel / 'resp' / (names[0].name + '.tmp') tmp.write_text(json.dumps({'ok': True, 'code': True, 'reason': 'ok'})) os.rename(tmp, channel / 'resp' / names[0].name) return time.sleep(0.02) t = threading.Thread(target=app) t.start() answer = store.ask(channel, {'op': 'epic-code', 'store': 'epic', 'id': 'x'}, timeout=3) t.join() self.assertEqual({'ok': True, 'code': True, 'reason': 'ok'}, answer) def test_no_answer_withdraws_the_request(self): with tempfile.TemporaryDirectory() as root: channel = Path(root) / 'stores' self.assertIsNone(store.ask(channel, {'op': 'epic-code'}, timeout=0.2)) self.assertEqual([], list((channel / 'req').iterdir())) def test_a_stale_code_is_dropped_before_asking(self): with tempfile.TemporaryDirectory() as root: folder = game(root) code = folder / '.droiddeck-epic-code' code.write_text('old') self.assertFalse(store.drop_stale(folder, now=code.stat().st_mtime + 60)) self.assertTrue(store.drop_stale(folder, now=code.stat().st_mtime + store.STALE_SECONDS + 1)) self.assertFalse(code.exists()) class Prefix(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() root = Path(self.tmp.name) self.compat = root / 'compatdata/3785150007' (self.compat / 'pfx').mkdir(parents=True) self.reg = self.compat / 'pfx/user.reg' self.reg.write_text('WINE REGISTRY Version 2\n\n[Software\\\\Wine] 1\n"Version"="win10"\n') def tearDown(self): self.tmp.cleanup() def test_wines_browser_is_the_hand_off_by_its_path(self): self.assertEqual('browser-set', store.provision_prefix(str(self.compat))) text = self.reg.read_text() self.assertIn('[Software\\\\Wine\\\\WineBrowser] ', text) self.assertIn('"Browsers"="/usr/local/bin/droiddeck-open-url,xdg-open"', text) store.provision_prefix(str(self.compat)) self.assertEqual(text, self.reg.read_text()) def test_an_earlier_builds_overlay_pointer_is_removed(self): self.reg.write_text(self.reg.read_text() + '\n[Software\\\\Epic Games\\\\EOS] 2\n"OverlayPath"="Z:\\\\x"\n"Other"="kept"\n') self.assertEqual('browser-set old-overlay-removed', store.provision_prefix(str(self.compat))) text = self.reg.read_text() self.assertNotIn('OverlayPath', text) self.assertIn('"Other"="kept"', text) def test_no_prefix_yet(self): self.assertEqual('prefix-not-created', store.provision_prefix(self.tmp.name + '/compatdata/1')) self.assertEqual('no-prefix', store.provision_prefix('')) class Choices(unittest.TestCase): def test_the_sidecars_epic_choices_are_honoured(self): with tempfile.TemporaryDirectory() as root: folder = game(root) sidecar = json.loads((folder / '.droiddeck-store.json').read_text()) self.assertEqual({'eos': True, 'offline': False}, store.options(sidecar)) self.assertEqual({'eos': True, 'offline': False}, store.options({'epic': {'v': 2, 'overlay': True}})) sidecar['epic'] = {'eos': True, 'offline': True} sidecar['cloud'] = False (folder / '.droiddeck-store.json').write_text(json.dumps(sidecar)) (folder / '.droiddeck-epic-code').write_text('left over') compat = Path(root) / 'compatdata/1' (compat / 'pfx').mkdir(parents=True) reg = compat / 'pfx/user.reg' reg.write_text('WINE REGISTRY Version 2\n\n[Software\\\\Epic Games\\\\EOS] 1\n"OverlayPath"="Z:\\\\x"\n"Other"="kept"\n') os.environ['BL_LAUNCH_DIR'] = str(Path(root) / 'session') os.environ['STEAM_COMPAT_DATA_PATH'] = str(compat) try: self.assertEqual(0, store.main([str(folder / '.droiddeck-launch.bat')])) finally: del os.environ['BL_LAUNCH_DIR'] del os.environ['STEAM_COMPAT_DATA_PATH'] self.assertFalse((Path(root) / 'session/stores/req').exists()) self.assertFalse((folder / '.droiddeck-epic-code').exists()) text = reg.read_text() self.assertNotIn('OverlayPath', text) self.assertIn('"Other"="kept"', text) class Browser(unittest.TestCase): def test_a_web_address_is_handed_to_the_app(self): xdg = load_path(BIN / 'droiddeck-open-url', 'open_url') with tempfile.TemporaryDirectory() as root: os.environ['BL_LAUNCH_DIR'] = root try: self.assertEqual(0, xdg.main(['https://www.epicgames.com/activate'])) finally: del os.environ['BL_LAUNCH_DIR'] reqs = list((Path(root) / 'stores/req').glob('*.json')) self.assertEqual(1, len(reqs)) self.assertEqual({'op': 'open-url', 'url': 'https://www.epicgames.com/activate'}, json.loads(reqs[0].read_text())) self.assertEqual('https://www.epicgames.com', xdg.where('https://www.epicgames.com/activate?code=SECRET')) class ExeTarget(unittest.TestCase): def test_a_game_started_by_its_exe_is_found_from_a_subfolder(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207664643') (folder / 'bin/x64').mkdir(parents=True) exe = folder / 'bin/x64/ELDERBORN.exe' exe.write_text('MZ') found = store.find_store_game(['"%s"' % exe]) self.assertEqual(folder, found[0]) self.assertEqual('gog', found[1]['store']) self.assertIsNone(store.find_store_game(['/root/Games/Other/x.exe'])) class CloudHooks(unittest.TestCase): def test_a_store_game_with_cloud_saves_asks_before_and_after(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207664643') bat = str(folder / '.droiddeck-launch.bat') os.environ['BL_LAUNCH_DIR'] = str(Path(root) / 'session') try: original = store.ask asked = [] store.ask = lambda channel, request, timeout=5.0: asked.append((request, timeout)) or {'ok': True, 'files': 2, 'reason': 'ok'} self.assertEqual(store.STORE_GAME, store.main([bat])) self.assertEqual(0, store.main(['--exited', bat])) finally: store.ask = original del os.environ['BL_LAUNCH_DIR'] self.assertEqual('gog-achievements', asked[0][0]['op']) self.assertEqual({'op': 'cloud-down', 'store': 'gog', 'id': '1207664643'}, asked[1][0]) self.assertEqual(store.CLOUD_TIMEOUT, asked[1][1]) self.assertEqual('cloud-up', asked[2][0]['op']) def test_a_first_launch_creates_the_prefix_before_the_cloud_download(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='epic', ident='fa4240e5') sidecar = json.loads((folder / store.SIDECAR).read_text()) sidecar['epic'] = {'eos': False, 'offline': True} (folder / store.SIDECAR).write_text(json.dumps(sidecar)) proton = Path(root) / 'proton' proton.write_text('selected Proton') compat = Path(root) / 'compatdata/1' order = [] def run(argv, **kwargs): order.append(argv) (compat / 'pfx').mkdir(parents=True) (compat / 'pfx/user.reg').write_text('WINE REGISTRY Version 2\n') return __import__('subprocess').CompletedProcess(argv, 0) env = {'BL_LAUNCH_DIR': root, 'STEAM_COMPAT_DATA_PATH': str(compat), 'BL_STORE_PROTON': str(proton)} with patch.dict(os.environ, env), patch.object(store.subprocess, 'run', side_effect=run), \ patch.object(store, 'ask', side_effect=lambda channel, request, timeout=5.0: order.append(request) or {'result': 'ok'}): self.assertEqual(store.STORE_GAME, store.main([str(folder / store.LAUNCHER)])) self.assertEqual([str(proton), 'run', 'cmd.exe', '/d', '/c', 'exit'], order[0]) self.assertEqual({'op': 'cloud-down', 'store': 'epic', 'id': 'fa4240e5'}, order[-1]) self.assertEqual('present', store.create_prefix(str(compat), str(proton))) self.assertEqual(2, len(order)) def test_a_prefix_that_is_not_made_leaves_the_deferred_download(self): with tempfile.TemporaryDirectory() as root: proton = Path(root) / 'proton' proton.write_text('selected Proton') with patch.object(store.subprocess, 'run', return_value=__import__('subprocess').CompletedProcess([], 1)): self.assertEqual('failed', store.create_prefix(root + '/compatdata/1', str(proton))) with patch.object(store.subprocess, 'run', side_effect=__import__('subprocess').TimeoutExpired('proton', 1)): self.assertEqual('timeout', store.create_prefix(root + '/compatdata/1', str(proton))) self.assertEqual('no-proton', store.create_prefix(root + '/compatdata/1', root + '/missing')) self.assertEqual('no-proton', store.create_prefix('', str(proton))) def test_cloud_off_or_amazon_runs_proton_as_before(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1') sidecar = json.loads((folder / '.droiddeck-store.json').read_text()) sidecar['cloud'] = False (folder / '.droiddeck-store.json').write_text(json.dumps(sidecar)) self.assertEqual(0, store.main([str(folder / '.droiddeck-launch.bat')])) self.assertFalse(store.cloud_on({'store': 'amazon', 'id': 'x'})) self.assertTrue(store.cloud_on({'store': 'epic', 'id': 'x'})) def test_both_launchers_wait_for_proton_only_for_a_store_game(self): for sh in (compat.LAUNCHER_SH, compat.EXTRA_WRAPPER_SH): self.assertIn('bl_run ${BL_TASKSET:-} /usr/local/bin/droiddeck-game-env', sh) self.assertNotIn('exec ${BL_TASKSET:-} /usr/local/bin/droiddeck-game-env', sh) self.assertIn('[ $rc -eq 10 ] && BL_STORE_EXIT=1', compat.BL_STORE_SETUP) self.assertIn('droiddeck-store-launch --exited --code "$rc" "${BL_STORE_ARGS[@]}"', compat.BL_STORE_SETUP) self.assertIn('exec "$@"', compat.BL_STORE_SETUP) BNET_EXE = '/root/.local/share/Steam/steamapps/compatdata/2596113170/pfx/drive_c/Program Files (x86)/Battle.net/Battle.net.exe' class BattleNet(unittest.TestCase): def test_a_games_shortcut_is_handed_to_droiddeck_battlenet(self): self.assertEqual([store.BATTLENET, 'launch', 'Fen'], store.battlenet(['waitforexitandrun', BNET_EXE, '--exec=launch Fen'], env={})) self.assertEqual([store.BATTLENET, 'launch', 'OSI'], store.battlenet(['"%s"' % BNET_EXE, '--exec="launch OSI"'], env={})) self.assertEqual([store.BATTLENET, 'open'], store.battlenet([BNET_EXE], env={})) def test_anything_else_and_its_own_starts_are_left_alone(self): other = BNET_EXE.replace('2596113170', '123') self.assertIsNone(store.battlenet([other, '--exec=launch Fen'], env={})) self.assertIsNone(store.battlenet(['/x/Diablo IV/Diablo IV.exe'], env={})) self.assertIsNone(store.battlenet([BNET_EXE, '--exec=launch Fen'], env={'BL_BATTLENET': '1'})) self.assertEqual([store.BATTLENET, 'open'], store.battlenet([BNET_EXE, '--exec=launch Fen;rm'], env={})) def test_the_launch_runs_there_and_the_wrapper_ends_with_it(self): with patch.dict(os.environ, {}, clear=False), patch.object(store.subprocess, 'call', return_value=0) as call: os.environ.pop('BL_BATTLENET', None) self.assertEqual(store.RAN, store.main([BNET_EXE, '--exec=launch Pro'])) call.assert_called_once_with([store.BATTLENET, 'launch', 'Pro']) with patch.dict(os.environ, {}, clear=False), patch.object(store.subprocess, 'call', return_value=1): os.environ.pop('BL_BATTLENET', None) self.assertEqual(store.RAN_FAILED, store.main([BNET_EXE, '--exec=launch Pro'])) self.assertIn('[ $rc -eq 11 ] && exit 0', compat.BL_STORE_SETUP) self.assertIn('[ $rc -eq 12 ] && exit 1', compat.BL_STORE_SETUP) self.assertLess(compat.BL_STORE_SETUP.index('droiddeck-store-launch "$@"'), compat.BL_STORE_SETUP.index('[ $rc -eq 11 ] && exit 0')) class Launchers(unittest.TestCase): def test_both_proton_launchers_ask_before_proton_starts(self): sh = compat.LAUNCHER_SH self.assertIn('bl_store_launch() {', sh) self.assertLess(sh.index('bl_store_launch "$@"'), sh.index('droiddeck-game-env "$depot/proton"')) self.assertIn('bl_store_launch "$@"', compat.EXTRA_WRAPPER_SH) self.assertIn('bl_store_launch() {', compat.BL_STORE_SETUP) self.assertIn('waitforexitandrun', compat.BL_STORE_SETUP) if __name__ == '__main__': unittest.main() class GogAchievements(unittest.TestCase): def test_cloud_off_still_prepares_the_gog_companion(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog') sidecar = json.loads((folder / store.SIDECAR).read_text()) sidecar['cloud'] = False (folder / store.SIDECAR).write_text(json.dumps(sidecar)) with patch.dict(os.environ, {'BL_LAUNCH_DIR': root, 'STEAM_COMPAT_DATA_PATH': root + '/compat', 'BL_STORE_PROTON': '/chosen/proton'}), patch.object(store, 'ask', return_value={'ready': True}) as ask, patch.object(store, 'provision_galaxy', return_value='ready') as provision: self.assertEqual(0, store.main([str(folder / store.LAUNCHER)])) ask.assert_called_once() provision.assert_called_once_with(root + '/compat', '/chosen/proton') def test_unavailable_companion_does_not_provision_or_block_the_game(self): with patch.object(store, 'ask', return_value=None), patch.object(store, 'provision_galaxy') as provision: self.assertFalse(store.prepare_gog({'id': '1', 'achievements': False}, '/pfx', '/session')) provision.assert_not_called() def test_a_fresh_prefix_is_bootstrapped_before_marking_success(self): with tempfile.TemporaryDirectory() as root: base = Path(root) tools = base / 'tools'; tools.mkdir() for name in ('GalaxyCommunication.exe', 'update-permissions.exe', 'version'): (tools / name).write_text(name) proton = base / 'proton'; proton.write_text('selected Proton') data = base / 'compat' seen = [] def run(argv, **kwargs): self.assertEqual([str(proton), 'run', 'cmd.exe', '/d', '/c'], argv[:5]) bat = Path(argv[5][2:].replace('\\', '/')) text = bat.read_text() seen.append(text) self.assertIn('goto existing', text) self.assertIn('update-permissions.exe', text) result = text.split('echo ready>"')[1].split('"')[0] Path(result[2:].replace('\\', '/')).write_text('ready') (data / 'pfx').mkdir(parents=True) (data / 'pfx/system.reg').write_text('Wine registry') return __import__('subprocess').CompletedProcess(argv, 0) with patch.object(store, 'COMET_DIR', tools), patch.object(store.subprocess, 'run', side_effect=run): self.assertEqual('ready', store.provision_galaxy(str(data), str(proton))) self.assertTrue((data / 'pfx/.droiddeck-comet-service').is_file()) self.assertEqual('ready', store.provision_galaxy(str(data), str(proton))) self.assertEqual(1, len(seen)) __import__('shutil').rmtree(data / 'pfx') self.assertEqual('ready', store.provision_galaxy(str(data), str(proton))) self.assertEqual(2, len(seen)) def test_failed_service_install_is_not_marked_done(self): with tempfile.TemporaryDirectory() as root: base = Path(root) for name in ('GalaxyCommunication.exe', 'update-permissions.exe', 'version', 'proton'): (base / name).write_text(name) 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_patch_registration_uses_authentic_build_and_the_prefix_registry_executable(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='7') (folder / 'goggame-7.id').write_text(json.dumps({'buildId': 'opaque-0001'})) proton = Path(root) / 'proton'; proton.write_text('selected Proton') data = Path(root) / 'compat' executable = data / 'pfx/drive_c/windows/system32/reg.exe' executable.parent.mkdir(parents=True); executable.write_bytes(b'MZreg') (data / 'pfx/system.reg').write_text('registry') imported = [] def run(argv, **kwargs): self.assertEqual([str(proton), 'run', str(executable), 'import'], argv[:4]) text = Path(argv[4][2:].replace('\\', '/')).read_bytes()[2:].decode('utf-16-le') imported.append(text) return __import__('subprocess').CompletedProcess(argv, 0) record = {'id': '7', 'build': 'opaque-0001', 'version': 'RC-final'} with patch.object(store.subprocess, 'run', side_effect=run): self.assertTrue(store.apply_gog_registry(folder, str(data), str(proton), installation=record).startswith('written=')) self.assertIn('[HKEY_LOCAL_MACHINE\\Software\\GOG.com\\Games\\7]', imported[0]) self.assertIn('"BUILDID"="opaque-0001"', imported[0]) self.assertIn('"VER"="RC-final"', imported[0]) with self.assertRaises(ValueError): store.apply_gog_registry(folder, str(data), str(proton), installation=dict(record, build='invented')) 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_whose_support_installer_cannot_run_gets_the_install_registry(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207658924') (folder / 'goggame-1207658924.info').write_text(json.dumps({'language': 'English', 'languages': ['en-US']})) gog_script(folder, '1207658924', [ {'install': {'action': 'Execute', 'arguments': {'executable': 'galaxy_the_witcher_enhanced_edition_2.1.0.15.exe'}}}, ]) entries, skipped = store.gog_registry(folder) app = store.windows_path(str(folder)).replace('\\', '\\\\') self.assertEqual([ '[HKEY_LOCAL_MACHINE\\Software\\CD Projekt Red\\The Witcher]\r\n"InstallFolder"="%s\\\\"' % app, '[HKEY_LOCAL_MACHINE\\Software\\CD Projekt Red\\The Witcher]\r\n"RegionVersion"="US"', '[HKEY_LOCAL_MACHINE\\Software\\CD Projekt Red\\The Witcher]\r\n"IsDjinniInstalled"=dword:00000001', '[HKEY_LOCAL_MACHINE\\Software\\CD Projekt Red\\The Witcher]\r\n"Language"="3"', '[HKEY_CURRENT_USER\\Software\\CD Projekt RED\\Witcher\\Settings]\r\n"TextLanguage"="3"', '[HKEY_CURRENT_USER\\Software\\CD Projekt RED\\Witcher\\Settings]\r\n"VoiceLanguage"="3"', ], entries) self.assertEqual(0, skipped) def test_a_players_language_choice_is_not_overwritten(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207658924') (folder / 'goggame-1207658924.info').write_text(json.dumps({'languages': ['en-US']})) proton = Path(root) / 'proton' proton.write_text('selected Proton') data = Path(root) / 'compat' (data / 'pfx').mkdir(parents=True) (data / 'pfx/user.reg').write_text( '[Software\\\\OtherApp] 1\n"InstallFolder"="C:\\\\Other"\n\n' '[Software\\\\CD Projekt RED\\\\Witcher\\\\Settings] 1\n"TextLanguage"="5"\n') imported = [] def run(argv, **kwargs): imported.append(Path(argv[4][2:].replace('\\', '/')).read_bytes()[2:].decode('utf-16-le')) (data / 'pfx/system.reg').write_text('Wine registry') return __import__('subprocess').CompletedProcess(argv, 0) with patch.object(store.subprocess, 'run', side_effect=run): store.apply_gog_registry(folder, str(data), str(proton)) text = imported[0] self.assertNotIn('"TextLanguage"', text) self.assertIn('"VoiceLanguage"', text) self.assertIn('"InstallFolder"', text) def test_malformed_language_metadata_falls_back_to_english(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207658924') (folder / 'goggame-1207658924.info').write_text(json.dumps({'languages': 3})) entries, _ = store.gog_registry(folder) self.assertIn('"Language"="3"', entries[3]) def test_a_native_hklm_value_does_not_replace_the_32_bit_fallback(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207658924') (folder / 'goggame-1207658924.info').write_text(json.dumps({'languages': ['en-US']})) pfx = Path(root) / 'pfx' pfx.mkdir() (pfx / 'system.reg').write_text( '[Software\\\\CD Projekt Red\\\\The Witcher] 1\n"InstallFolder"="C:\\\\Native"\n') entries, _ = store.gog_registry(folder, store.gog_registry_values(pfx)) self.assertTrue(any('"InstallFolder"' in entry for entry in entries)) (pfx / 'system.reg').write_text( '[Software\\\\Wow6432Node\\\\CD Projekt Red\\\\The Witcher] 1\n"InstallFolder"="C:\\\\32bit"\n') entries, _ = store.gog_registry(folder, store.gog_registry_values(pfx)) self.assertFalse(any('"InstallFolder"' in entry for entry in entries)) def test_a_deleted_fallback_value_is_imported_again(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207658924') (folder / 'goggame-1207658924.info').write_text(json.dumps({'languages': ['en-US']})) proton = Path(root) / 'proton' proton.write_text('selected Proton') data = Path(root) / 'compat' pfx = data / 'pfx' pfx.mkdir(parents=True) imported = [] def run(argv, **kwargs): imported.append(argv) (pfx / 'system.reg').write_text( '[Software\\\\Wow6432Node\\\\CD Projekt Red\\\\The Witcher] 1\n' '"InstallFolder"="C:\\\\Game"\n"RegionVersion"="US"\n' '"IsDjinniInstalled"=dword:00000001\n"Language"="3"\n') (pfx / 'user.reg').write_text( '[Software\\\\CD Projekt RED\\\\Witcher\\\\Settings] 1\n' '"TextLanguage"="3"\n"VoiceLanguage"="3"\n') return __import__('subprocess').CompletedProcess(argv, 0) with patch.object(store.subprocess, 'run', side_effect=run): self.assertEqual('written=6', store.apply_gog_registry(folder, str(data), str(proton))) self.assertEqual('none', store.apply_gog_registry(folder, str(data), str(proton))) (pfx / 'system.reg').write_text( '[Software\\\\Wow6432Node\\\\CD Projekt Red\\\\The Witcher] 1\n' '"RegionVersion"="US"\n"IsDjinniInstalled"=dword:00000001\n"Language"="3"\n') self.assertEqual('written=1', store.apply_gog_registry(folder, str(data), str(proton))) self.assertEqual(2, len(imported)) def test_the_install_language_selects_the_titles_language_id(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207658924') (folder / 'goggame-1207658924.info').write_text(json.dumps({'languages': ['pl-PL']})) entries, _ = store.gog_registry(folder) self.assertIn('"Language"="5"', entries[3]) def test_a_script_that_sets_registry_keeps_its_own_values(self): with tempfile.TemporaryDirectory() as root: folder = game(root, store_id='gog', ident='1207658924') (folder / 'goggame-1207658924.info').write_text(json.dumps({'languages': ['en-US']})) gog_script(folder, '1207658924', [ set_registry(root='HKLM', subkey='Software\\CD Projekt Red\\The Witcher', valueName='InstallFolder', valueData='{app}\\', valueType='string'), ]) entries, _ = store.gog_registry(folder) app = store.windows_path(str(folder)).replace('\\', '\\\\') self.assertEqual([ '[HKEY_LOCAL_MACHINE\\Software\\CD Projekt Red\\The Witcher]\r\n"InstallFolder"="%s\\\\"' % app, ], entries) 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')