From a8444830802410ff9f3fa8ea67ec064795b467b6 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Fri, 9 Oct 2026 00:09:39 -0400 Subject: [PATCH] Stores: Epic overlay off by default, switches saved on disk, registry writes checked With the overlay pointer set Metalstorm exits while loading; without it the game signs in and runs. The overlay now defaults to off: the sidecar's epic block carries a version, and a block from before it (the old default wrote overlay=true unasked) reads as off, in the app and in droiddeck-store-launch alike. The overlay is fetched only for a game that turns it on. The card's switches read the sidecar fresh, write it and read it back, and show what was read back. droiddeck-store-launch waits for a wineserver still holding the prefix before appending to user.reg - one that exits rewrites the file and drops the line - reads the value back and logs what the file holds. Its docstring is raw. The game-PATH xdg-open logs each call as scheme://host only and accepts steam://openurl. --- .../launcher/stores/StoreGameSidecar.kt | 29 ++++++-- .../launcher/stores/StoreLaunchRequests.kt | 6 +- .../launcher/stores/epic/EpicBackend.kt | 2 - .../droiddeck/launcher/ui/FrontEndSetup.kt | 23 ++++--- .../launcher/stores/StoreGameSidecarTest.kt | 24 ++++++- docs/progress-log.md | 18 +++++ .../usr/local/bin/droiddeck-store-launch | 67 ++++++++++++++++--- .../usr/local/lib/droiddeck/browser/xdg-open | 14 ++++ tools/tests/test_store_launch.py | 12 ++-- 9 files changed, 165 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt index ce1c4da0f..509004ba7 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreGameSidecar.kt @@ -60,7 +60,7 @@ class StoreGameSidecar( put("title", title) if (exe.isNotEmpty()) put("exe", exe) if (state != STATE_INSTALLED) put("state", state) - if (store == Store.EPIC) put("epic", JSONObject().put("eos", epic.eos).put("offline", epic.offline).put("overlay", epic.overlay)) + if (store == Store.EPIC) put("epic", JSONObject().put("v", EpicOptions.VERSION).put("eos", epic.eos).put("offline", epic.offline).put("overlay", epic.overlay)) if (!launcher.isNullOrEmpty()) put("launcher", launcher) if (args.isNotEmpty()) put("args", JSONArray(args)) if (env.isNotEmpty()) put("env", JSONObject(env as Map<*, *>)) @@ -87,6 +87,18 @@ class StoreGameSidecar( fun file(folder: File): File = File(folder, FILE_NAME) + /** + * Changes an Epic game's launch choices on disk: reads the sidecar fresh (never a copy held + * by a screen), applies [change], writes it, and reads it back. The written sidecar, or null + * when there is none or the write did not take. Blocking. + */ + fun updateEpic(folder: File, change: (EpicOptions) -> EpicOptions): StoreGameSidecar? { + val current = read(folder) ?: return null + val next = current.copy(epic = change(current.epic)) + next.write(folder) + return read(folder)?.takeIf { it.epic == next.epic } + } + /** The sidecar in [folder], or null when there is none or it cannot be read. */ fun read(folder: File): StoreGameSidecar? { val f = file(folder) @@ -122,7 +134,10 @@ class StoreGameSidecar( extra = extra, state = state, epic = o.optJSONObject("epic")?.let { e -> - EpicOptions(eos = e.optBoolean("eos", true), offline = e.optBoolean("offline", false), overlay = e.optBoolean("overlay", true)) + // Before v2 the overlay was on by default and written so without anyone choosing + // it; the overlay crashed games, so those read as off. From v2 it is a choice. + val chosen = e.optInt("v", 1) >= EpicOptions.VERSION + EpicOptions(eos = e.optBoolean("eos", true), offline = e.optBoolean("offline", false), overlay = chosen && e.optBoolean("overlay", false)) } ?: EpicOptions(), ) } @@ -136,9 +151,15 @@ class StoreGameSidecar( /** * An Epic game's launch choices. [eos]: pass a fresh exchange code (Epic sign-in). [offline]: start * with the Epic identity arguments only, no code - for a game that will not start when sign-in fails. - * [overlay]: point the game's prefix at Epic's EOS overlay. + * [overlay]: point the game's prefix at Epic's EOS overlay - off by default: with it on, a game can + * exit while loading on this stack (Metalstorm did, twice; off, it signs in and runs). */ -data class EpicOptions(val eos: Boolean = true, val offline: Boolean = false, val overlay: Boolean = true) { +data class EpicOptions(val eos: Boolean = true, val offline: Boolean = false, val overlay: Boolean = false) { /** Whether a launch asks for a code at all. */ val wantsCode: Boolean get() = eos && !offline + + companion object { + /** v2: the overlay is off unless chosen (Metalstorm exits while loading with it on). */ + const val VERSION = 2 + } } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt index 56ab6503e..d3244e335 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/StoreLaunchRequests.kt @@ -62,8 +62,10 @@ object StoreLaunchRequests { val code = StoreLaunch.epicCode(app, id) // The overlay is placed once and shared; the launch writes the prefix's pointer. val overlay = com.droiddeck.launcher.stores.epic.EpicOverlay.installedVersion(app) - Log.i(TAG, "epic overlay installed=${overlay ?: "no"} prefix=${request.optString("prefix").ifEmpty { "?" }}") - if (overlay == null) com.droiddeck.launcher.stores.epic.EpicOverlay.ensureAsync(app) + val wanted = StoreInstallRoot.gameFolders(app).firstNotNullOfOrNull { f -> StoreGameSidecar.read(f)?.takeIf { it.store == Store.EPIC && it.id == id } }?.epic?.overlay == true + Log.i(TAG, "epic overlay installed=${overlay ?: "no"} wanted=$wanted prefix=${request.optString("prefix").ifEmpty { "?" }}") + // Fetched only for a game that has the overlay on (656 MB; off by default). + if (overlay == null && wanted) com.droiddeck.launcher.stores.epic.EpicOverlay.ensureAsync(app) JSONObject().put("ok", true).put("code", code.written).put("reason", code.reason).put("overlay", overlay ?: "") } } diff --git a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt index 85823bef0..37439258e 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/epic/EpicBackend.kt @@ -51,8 +51,6 @@ object EpicBackend : StoreBackend, EpicLaunchSupport { when (result) { is EpicLibrary.SyncResult.Ok -> { StoresState.library[store] = result.games.map { EpicLibrary.toCatalogItem(app, it) }; StoresState.problems.remove(store) - // The EOS overlay, once, so a game's first launch already has it. - EpicOverlay.ensureAsync(app) } is EpicLibrary.SyncResult.Failed -> StoresState.problems[store] = result.message EpicLibrary.SyncResult.NotLoggedIn -> StoresState.problems[store] = "Epic session expired: sign in again." diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index 2f76db673..f2c408c0b 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -508,6 +508,7 @@ internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost /** The Epic card: sign-in, offline and overlay switches, read from and written to the game's sidecar. */ @Composable private fun EpicLaunchCard(host: MenuHost, folder: java.io.File) { + val appContext = androidx.compose.ui.platform.LocalContext.current.applicationContext var sidecar by remember(folder) { mutableStateOf(com.droiddeck.launcher.stores.StoreGameSidecar.read(folder)) } val options = sidecar?.epic ?: com.droiddeck.launcher.stores.EpicOptions() val value = buildList { @@ -515,19 +516,25 @@ private fun EpicLaunchCard(host: MenuHost, folder: java.io.File) { else if (options.eos) add(stringResource(R.string.epic_card_eos)) if (options.overlay) add(stringResource(R.string.epic_card_overlay)) }.ifEmpty { listOf(stringResource(R.string.epic_card_none)) }.joinToString(" · ") - fun set(next: com.droiddeck.launcher.stores.EpicOptions) { - val current = sidecar ?: return - val updated = current.copy(epic = next) - sidecar = updated - Thread({ runCatching { updated.write(folder) } }, "epic-options").start() + // Each switch goes straight to the sidecar on disk - both launch paths read it there - and the + // card shows what was read back, so a write that did not take is never shown as done. + fun set(change: (com.droiddeck.launcher.stores.EpicOptions) -> com.droiddeck.launcher.stores.EpicOptions) { + Thread({ + val written = runCatching { com.droiddeck.launcher.stores.StoreGameSidecar.updateEpic(folder, change) } + .onFailure { android.util.Log.w("EpicLaunchCard", "could not write ${folder.name}: ${it.message}") }.getOrNull() + if (written != null) android.util.Log.i("EpicLaunchCard", "epic options ${written.id} eos=${written.epic.eos} offline=${written.epic.offline} overlay=${written.epic.overlay}") + if (written?.epic?.overlay == true) com.droiddeck.launcher.stores.epic.EpicOverlay.ensureAsync(appContext) + else android.util.Log.w("EpicLaunchCard", "epic options not saved for ${folder.name}") + com.droiddeck.launcher.stores.StoresState.post { sidecar = written ?: com.droiddeck.launcher.stores.StoreGameSidecar.read(folder) } + }, "epic-options").start() } SettingCard(stringResource(R.string.epic_card_title), value, "card:epic", Modifier.fillMaxSize()) { host.open = if (host.open == "epic") null else "epic" } AnchoredMenu(host.open == "epic", onDismiss = { if (host.open == "epic") host.open = null }, title = stringResource(R.string.epic_card_title)) { first -> - MenuItem(stringResource(R.string.epic_eos), checked = options.eos, focusRequester = first) { set(options.copy(eos = !options.eos)) } - MenuItem(stringResource(R.string.epic_offline), checked = options.offline) { set(options.copy(offline = !options.offline)) } - MenuItem(stringResource(R.string.epic_overlay), checked = options.overlay) { set(options.copy(overlay = !options.overlay)) } + MenuItem(stringResource(R.string.epic_eos), checked = options.eos, focusRequester = first) { set { it.copy(eos = !it.eos) } } + MenuItem(stringResource(R.string.epic_offline), checked = options.offline) { set { it.copy(offline = !it.offline) } } + MenuItem(stringResource(R.string.epic_overlay), checked = options.overlay) { set { it.copy(overlay = !it.overlay) } } } } diff --git a/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt b/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt index d9098b03f..921707550 100644 --- a/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/stores/StoreGameSidecarTest.kt @@ -47,14 +47,34 @@ class StoreGameSidecarTest { @Test fun epicLaunchChoicesRoundTripAndDefaultOn() { val plain = StoreGameSidecar.parse(StoreGameSidecar(Store.EPIC, "m", "Metalstorm", "M.exe").toJson().toString())!! - assertEquals(EpicOptions(eos = true, offline = false, overlay = true), plain.epic) + assertEquals(EpicOptions(eos = true, offline = false, overlay = false), plain.epic) assertTrue(plain.epic.wantsCode) val set = StoreGameSidecar(Store.EPIC, "m", "Metalstorm", "M.exe", epic = EpicOptions(eos = true, offline = true, overlay = false)) val back = StoreGameSidecar.parse(set.toJson().toString())!! assertEquals(EpicOptions(eos = true, offline = true, overlay = false), back.epic) assertFalse(back.epic.wantsCode) - // A sidecar from before the choices existed reads as all on. + // A sidecar from before the choices existed reads as the defaults: sign-in on, overlay off. assertEquals(EpicOptions(), StoreGameSidecar.parse("""{"store":"epic","id":"m","title":"M","exe":"M.exe"}""")!!.epic) + // The old default wrote overlay=true without a version: nobody chose it, so it reads as off. + assertFalse(StoreGameSidecar.parse("""{"store":"epic","id":"m","title":"M","exe":"M.exe","epic":{"eos":true,"offline":false,"overlay":true}}""")!!.epic.overlay) + // Chosen from v2 on, it stays. + val on = StoreGameSidecar.parse(StoreGameSidecar(Store.EPIC, "m", "M", "M.exe", epic = EpicOptions(overlay = true)).toJson().toString())!! + assertTrue(on.epic.overlay) + } + + @Test fun aSwitchOnTheEpicCardChangesTheSidecarOnDisk() { + val folder = java.io.File(org.robolectric.RuntimeEnvironment.getApplication().filesDir, "Games/Stores/Epic/Metalstorm").apply { mkdirs() } + StoreGameSidecar(Store.EPIC, "m", "Metalstorm", "Metalstorm.exe", launcher = ".droiddeck-launch.bat").write(folder) + val written = StoreGameSidecar.updateEpic(folder) { it.copy(overlay = true) }!! + assertTrue(written.epic.overlay) + // What the Steam-launch helper reads: the file itself. + val onDisk = org.json.JSONObject(java.io.File(folder, StoreGameSidecar.FILE_NAME).readText()) + assertTrue(onDisk.getJSONObject("epic").getBoolean("overlay")) + assertEquals(EpicOptions.VERSION, onDisk.getJSONObject("epic").getInt("v")) + assertTrue(onDisk.getJSONObject("epic").getBoolean("eos")) + // Everything else in it stays. + assertEquals(".droiddeck-launch.bat", StoreGameSidecar.read(folder)!!.launcher) + assertEquals(null, StoreGameSidecar.updateEpic(java.io.File(folder, "missing")) { it }) } @Test fun anInstallUnderWayHasNoExeYetAndIsNotInstalled() { diff --git a/docs/progress-log.md b/docs/progress-log.md index c49473986..0392a353c 100644 --- a/docs/progress-log.md +++ b/docs/progress-log.md @@ -144,6 +144,24 @@ without it and Setup says "Stores engine not built"). (`token`, `__token__`, `f_token`, `hdnts`, access/refresh/id tokens, `code=`) and Authorization / Cookie headers are blanked outside URLs too. `StoreLogTest` covers a GOG secure link and an Epic Akamai URL. The engine's own logcat lines (tag `EpicNative` etc.) are written on the native side. +- **Epic overlay off by default** (device A/B, Metalstorm: overlay pointer set → exits while + loading, twice; off → signs in to its Starform account and runs at 118 fps). `EpicOptions.overlay` + defaults to false; the sidecar's `epic` block carries `"v": 2`, and an older block (the old default + wrote `"overlay": true` unasked) reads as off - in the app and in `droiddeck-store-launch` alike. + The 656 MB overlay is fetched only for a game that turns it on (the card's switch, or a launch of + such a game), no longer after every library sync. +- **The card's switches are saved where both launch paths read them:** each toggle reads the + sidecar fresh from disk, writes it and reads it back (`StoreGameSidecar.updateEpic`), and the card + shows what was read back; the app logs `epic options eos= offline= overlay=`. +- **Registry lines that a running wineserver would drop:** `droiddeck-store-launch` waits up to 8 s + for a wineserver still holding the prefix to exit before appending to `user.reg` (it rewrites the + file from memory when it exits, which is how `OverlayPath` vanished), reads the value back, and + logs `overlay-set`, `overlay-off`, `overlay-not-written` or `overlay-still-set`, plus + `server-running` when one never left. The helper's docstring is raw (no SyntaxWarning). +- **Browser hand-off diagnostics:** the game-PATH `xdg-open` logs every call as `scheme://host` + only and accepts `steam://openurl/
`. On the device it was never called while EOS waited + on its browser flow, so how EOS opens the browser under this Proton is still to be seen in the + next log. - **The exchange code no longer reaches the game's environment** (device: Metalstorm's Player.log dumped `DD_AUTH: … -AUTH_PASSWORD=`). The launcher read the code into a variable the game inherited. It now clears the variable on the same line that starts the game - cmd has expanded it diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch b/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch index 0aa5088bb..fdc6672e1 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-store-launch @@ -1,5 +1,5 @@ #!/usr/bin/python3 -"""Before Proton starts a store game, asks the app for what that store needs at launch. +r"""Before Proton starts a store game, asks the app for what that store needs at launch. Run by droiddeck-proton / droiddeck-proton-wrap (steam-compatibility) for every real game launch, whoever started it: the Games tab, a pinned shortcut, or the Steam client's own Play button. It @@ -39,6 +39,7 @@ EPIC_CODE = ".droiddeck-epic-code" TIMEOUT = 5.0 STALE_SECONDS = 300 OVERLAY_DIR = "/root/.local/share/droiddeck/epic-overlay" +SERVER_WAIT = 8.0 OVERLAY_DLL = "EOSOVH-Win64-Shipping.dll" EOS_KEY = "[Software\\\\Epic Games\\\\EOS]" BROWSER_KEY = "[Software\\\\Wine\\\\WineBrowser]" @@ -171,24 +172,74 @@ def remove_value(reg, key, name): def options(sidecar): """An Epic game's launch choices from its sidecar, as the Games tab's Epic card sets them.""" e = sidecar.get("epic") if isinstance(sidecar.get("epic"), dict) else {} - return {"eos": e.get("eos", True) is not False, "offline": e.get("offline", False) is True, "overlay": e.get("overlay", True) is not False} + # The overlay is off unless chosen (v2 on): the old default wrote overlay=true without a version, + # and with it on a game could exit while loading. The app's EpicOptions reads it the same way. + chosen = isinstance(e.get("v"), int) and e.get("v") >= 2 + return {"eos": e.get("eos", True) is not False, "offline": e.get("offline", False) is True, "overlay": chosen and e.get("overlay") is True} -def provision_prefix(compat_data, overlay_dir=OVERLAY_DIR, overlay=True): +def server_socket(pfx): + """The wineserver socket for prefix [pfx], as Wine names it (server-- under /tmp/.wine-).""" + try: + st = os.stat(pfx) + except OSError: + return None + base = os.environ.get("BL_WINE_SERVER_BASE") or "/tmp/.wine-%d" % os.getuid() + return Path(base) / ("server-%x-%x" % (st.st_dev, st.st_ino)) / "socket" + + +def wait_for_server(pfx, timeout=SERVER_WAIT): + """ + Waits for a wineserver still holding this prefix (an earlier verb's, or the last launch's) to + exit: it rewrites user.reg from its own memory when it does, which drops a line appended + meanwhile. True when none is left. + """ + sock = server_socket(pfx) + if sock is None: + return True + deadline = time.monotonic() + timeout + while sock.exists() and time.monotonic() < deadline: + time.sleep(0.1) + return not sock.exists() + + +def has_value(reg, key, name, value=None): + """Whether the last word on `key` in user.reg sets "name" (to [value], when given).""" + if not reg.is_file(): + return False + inside, last = False, None + for row in reg.read_text(errors="replace").splitlines(): + if row.startswith("["): + inside = row.startswith(key + " ") or row == key + elif inside and row.startswith('"%s"=' % name): + last = row + if last is None: + return False + return value is None or last == '"%s"="%s"' % (name, reg_escape(value)) + + +def provision_prefix(compat_data, overlay_dir=OVERLAY_DIR, overlay=False, wait=SERVER_WAIT): """The EOS overlay pointer (or its removal, when [overlay] is off) and Wine's browser, in the game's prefix.""" if not compat_data: return "no-prefix" - reg = Path(compat_data) / "pfx" / "user.reg" + pfx = Path(compat_data) / "pfx" + reg = pfx / "user.reg" if not reg.is_file(): return "prefix-not-created" + # A wineserver still up for this prefix would write its own copy of user.reg over these lines. + busy = not wait_for_server(pfx, wait) set_value(reg, BROWSER_KEY, "Browsers", "xdg-open") if not overlay: remove_value(reg, EOS_KEY, "OverlayPath") - return "overlay-off" - if overlay_version(overlay_dir) is None: + state = "overlay-off" if not has_value(reg, EOS_KEY, "OverlayPath") else "overlay-still-set" + elif overlay_version(overlay_dir) is None: return "overlay-missing" - set_value(reg, EOS_KEY, "OverlayPath", windows_path(overlay_dir)) - return "overlay-set" + else: + target = windows_path(overlay_dir) + set_value(reg, EOS_KEY, "OverlayPath", target) + # Read back, so the log says what the file holds, not what was meant. + state = "overlay-set" if has_value(reg, EOS_KEY, "OverlayPath", target) else "overlay-not-written" + return state + (" server-running" if busy else "") def main(argv): diff --git a/tools/linuxfs/overlay/usr/local/lib/droiddeck/browser/xdg-open b/tools/linuxfs/overlay/usr/local/lib/droiddeck/browser/xdg-open index 38cd30074..bc8c2c7fe 100755 --- a/tools/linuxfs/overlay/usr/local/lib/droiddeck/browser/xdg-open +++ b/tools/linuxfs/overlay/usr/local/lib/droiddeck/browser/xdg-open @@ -20,9 +20,23 @@ def hand_over(url, launch_dir): os.rename(tmp, req / (rid + ".json")) +def where(url): + """scheme://host only, for the log: no path, no query.""" + from urllib.parse import urlsplit + try: + u = urlsplit(url) + return "%s://%s" % (u.scheme, u.hostname or "") + except ValueError: + return "?" + + def main(argv): url = argv[0] if argv else "" + # The Steam client's own form (steam://openurl/
), in case a launcher passes it on. + if url.startswith("steam://openurl/"): + url = url[len("steam://openurl/"):] launch_dir = os.environ.get("BL_LAUNCH_DIR", "") + print("droiddeck xdg-open: %s" % where(url), file=sys.stderr, flush=True) if url.startswith(("https://", "http://")) and launch_dir: hand_over(url, launch_dir) return 0 diff --git a/tools/tests/test_store_launch.py b/tools/tests/test_store_launch.py index 4d96f19c4..e55880f99 100644 --- a/tools/tests/test_store_launch.py +++ b/tools/tests/test_store_launch.py @@ -128,18 +128,18 @@ class Prefix(unittest.TestCase): def test_the_overlay_pointer_names_the_shared_copy_through_z(self): (self.overlay / store.OVERLAY_DLL).write_bytes(b'MZ') (self.overlay / 'droiddeck-version').write_text('1.0.42') - self.assertEqual('overlay-set', store.provision_prefix(str(self.compat), str(self.overlay))) + self.assertEqual('overlay-set', store.provision_prefix(str(self.compat), str(self.overlay), overlay=True)) text = self.reg.read_text() self.assertIn('[Software\\\\Epic Games\\\\EOS] ', text) self.assertIn('"OverlayPath"="Z:' + str(self.overlay).replace('/', '\\\\') + '"', text) self.assertIn('"Browsers"="xdg-open"', text) # A second launch adds nothing: the last word already says so. - store.provision_prefix(str(self.compat), str(self.overlay)) + store.provision_prefix(str(self.compat), str(self.overlay), overlay=True) self.assertEqual(text, self.reg.read_text()) self.assertEqual('1.0.42', store.overlay_version(str(self.overlay))) def test_no_overlay_yet_or_no_prefix_yet(self): - self.assertEqual('overlay-missing', store.provision_prefix(str(self.compat), str(self.overlay))) + self.assertEqual('overlay-missing', store.provision_prefix(str(self.compat), str(self.overlay), overlay=True)) self.assertNotIn('OverlayPath', self.reg.read_text()) self.assertEqual('prefix-not-created', store.provision_prefix(self.tmp.name + '/compatdata/1', str(self.overlay))) self.assertEqual('no-prefix', store.provision_prefix('', str(self.overlay))) @@ -150,7 +150,10 @@ class Choices(unittest.TestCase): with tempfile.TemporaryDirectory() as root: folder = game(root) sidecar = json.loads((folder / '.droiddeck-store.json').read_text()) - self.assertEqual({'eos': True, 'offline': False, 'overlay': True}, store.options(sidecar)) + self.assertEqual({'eos': True, 'offline': False, 'overlay': False}, store.options(sidecar)) + # The old default (no version) never turns it on; a v2 choice does. + self.assertFalse(store.options({'epic': {'eos': True, 'offline': False, 'overlay': True}})['overlay']) + self.assertTrue(store.options({'epic': {'v': 2, 'overlay': True}})['overlay']) sidecar['epic'] = {'eos': True, 'offline': True, 'overlay': False} (folder / '.droiddeck-store.json').write_text(json.dumps(sidecar)) (folder / '.droiddeck-epic-code').write_text('left over') @@ -185,6 +188,7 @@ class Browser(unittest.TestCase): 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 Launchers(unittest.TestCase):