Stores: the Epic code reaches the game on its command line only

Metalstorm's Player.log printed its environment, and with it the
exchange code: the launcher read the code into a variable the game
inherited. The variable (now with a plain name) is cleared on the same
line that starts the game - cmd has expanded it there already - so the
game gets the code as arguments and nothing of it in its environment.
A launcher from an earlier build is rewritten before the next Epic
launch, from the app and from the Steam-launch request alike.
This commit is contained in:
The412Banner committed 2026-10-09 00:03:34 -04:00
1 parent 7ab0c12c97
commit 1d1de10b2d
3 files changed
+51 -14

No files matched your search

@@ -44,16 +44,23 @@ object StoreLaunch {
lines += "rem Written by DroidDeck for ${sidecar.store.label}: ${sidecar.title}. Steam's shortcut runs this file."
lines += "cd /d \"%~dp0$dir\""
for ((k, v) in sidecar.env) if (ENV_NAME.matches(k)) lines += "set \"$k=${v.replace("\"", "")}\""
lines += "set \"DD_AUTH=\""
if (sidecar.store == Store.EPIC) {
lines += "if not exist \"%~dp0$EPIC_CODE\" goto run"
lines += "set /p DD_CODE=<\"%~dp0$EPIC_CODE\""
lines += "del \"%~dp0$EPIC_CODE\""
lines += "set \"DD_AUTH=-AUTH_LOGIN=unused -AUTH_PASSWORD=%DD_CODE% -AUTH_TYPE=exchangecode\""
lines += ":run"
}
val args = sidecar.args.joinToString(" ") { quoteArg(it) }
lines += "\"%~dp0$exe\"" + (if (args.isNotEmpty()) " $args" else "") + " %DD_AUTH%"
val run = "\"%~dp0$exe\"" + (if (args.isNotEmpty()) " $args" else "")
if (sidecar.store == Store.EPIC) {
// The one-shot code reaches the game on its command line only. cmd expands %DD_X% when it
// parses the last line, so the variable is cleared on that same line before the game
// starts: nothing of it is left in the environment the game inherits (and dumps).
lines += "set \"$CODE_VAR=\""
lines += "if exist \"%~dp0$EPIC_CODE\" set /p $CODE_VAR=<\"%~dp0$EPIC_CODE\""
lines += "if exist \"%~dp0$EPIC_CODE\" del \"%~dp0$EPIC_CODE\""
lines += "if defined $CODE_VAR goto signed"
lines += run
lines += "goto :eof"
lines += ":signed"
lines += "set \"$CODE_VAR=\" & $run -AUTH_LOGIN=unused -AUTH_PASSWORD=%$CODE_VAR% -AUTH_TYPE=exchangecode"
} else {
lines += run
}
return lines.joinToString("\r\n") + "\r\n"
}
@@ -70,6 +77,23 @@ object StoreLaunch {
* fresh exchange code written beside its launcher (a few seconds at most; the launch goes
* ahead without one when the store cannot be reached). Other stores need nothing.
*/
/** The launcher's variable for the one-shot code; a plain name, cleared before the game starts. */
private const val CODE_VAR = "DD_X"
/**
* Rewrites a game's launcher when its text is not what this build writes - an install from an
* earlier build keeps its old .bat otherwise. Called before every Epic launch.
*/
fun refreshLauncher(folder: File, sidecar: StoreGameSidecar) {
if (sidecar.launcher != LAUNCHER) return
val file = File(folder, LAUNCHER)
val text = launcherText(sidecar)
if (runCatching { file.readText() }.getOrNull() != text) {
file.writeText(text)
Log.i(TAG, "launcher rewritten for ${sidecar.id}")
}
}
fun prepare(context: Context, store: Store, id: String, onReady: () -> Unit) {
if (store != Store.EPIC) { onReady(); return }
val app = context.applicationContext
@@ -93,7 +117,9 @@ object StoreLaunch {
val app = context.applicationContext
val result = try {
val folder = StoreInstallRoot.gameFolders(app).firstOrNull { f -> StoreGameSidecar.read(f)?.let { it.store == Store.EPIC && it.id == id } == true }
val options = folder?.let { StoreGameSidecar.read(it)?.epic }
val sidecar = folder?.let { StoreGameSidecar.read(it) }
sidecar?.let { refreshLauncher(folder, it) }
val options = sidecar?.epic
if (folder == null) CodeResult(false, "not-installed")
else if (options != null && !options.wantsCode) {
File(folder, EPIC_CODE).delete()
@@ -88,17 +88,22 @@ class StoreGameSidecarTest {
val text = StoreLaunch.launcherText(sample.copy(exe = "Binaries/Win64/Celeste.exe"))
assertTrue(text.contains("cd /d \"%~dp0Binaries\\Win64\"\r\n"))
assertTrue(text.contains("set \"FOO=bar\"\r\n"))
assertTrue(text.contains("\"%~dp0Binaries\\Win64\\Celeste.exe\" -EpicPortal -epicusername=\"Some One\" %DD_AUTH%\r\n"))
assertTrue(text.contains("\r\n\"%~dp0Binaries\\Win64\\Celeste.exe\" -EpicPortal -epicusername=\"Some One\"\r\n"))
// Epic reads the one-shot exchange code the app leaves beside the launcher, then deletes it.
assertTrue(text.contains("set /p DD_CODE=<\"%~dp0.droiddeck-epic-code\""))
assertTrue(text.contains("-AUTH_TYPE=exchangecode"))
assertTrue(text.contains("set /p DD_X=<\"%~dp0.droiddeck-epic-code\""))
// The code goes on the command line only: the variable is cleared on the very line that
// starts the game (cmd has already expanded it there), so the game inherits none of it.
assertTrue(text.contains("set \"DD_X=\" & \"%~dp0Binaries\\Win64\\Celeste.exe\" -EpicPortal -epicusername=\"Some One\" -AUTH_LOGIN=unused -AUTH_PASSWORD=%DD_X% -AUTH_TYPE=exchangecode\r\n"))
assertFalse(text.contains("AUTH="))
assertFalse(text.contains("DD_AUTH"))
}
@Test fun gogLauncherHasNoEpicBlockAndIsOnlyWrittenWhenNeeded() {
val plain = StoreGameSidecar(Store.GOG, "1", "T", "game.exe")
val text = StoreLaunch.launcherText(plain.copy(args = listOf("-windowed")))
assertFalse(text.contains("exchangecode"))
assertTrue(text.contains("\"%~dp0game.exe\" -windowed %DD_AUTH%"))
assertTrue(text.contains("\"%~dp0game.exe\" -windowed\r\n"))
assertFalse(text.contains("DD_X"))
assertEquals("\"two words\"", StoreLaunch.quoteArg("two words"))
assertEquals("-x=\"a b\"", StoreLaunch.quoteArg("-x=\"a b\""))
}
+6
View File
@@ -144,6 +144,12 @@ 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.
- **The exchange code no longer reaches the game's environment** (device: Metalstorm's Player.log
dumped `DD_AUTH: … -AUTH_PASSWORD=<code>`). 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
there already - so the code is on the command line only; the variable has a plain name. An old
launcher is rewritten before the next Epic launch (`StoreLaunch.refreshLauncher`, from both the
app and the Steam-launch request).
- **A store game page shows no description that is only its title again** (Metalstorm's).
- **Epic launch card** (Games tab › Launch settings, Epic games only): "Epic sign-in (EOS)" on,
"Launch offline" off, "Epic overlay" on, stored in the game's sidecar (`"epic": {"eos", "offline",