Conversation
📝 WalkthroughWalkthroughRenames GameToken→EpicGameToken and adds Epic launch flow: new token-exchange and ownership-token endpoints in EpicAuthClient, orchestration in EpicAuthManager, launch-parameter assembly and .ovt persistence/cleanup in EpicGameLauncher, ECOMMERCE_HOST constant, UI cloud-save sync and token cleanup, and XServerScreen now requests launch params (Context added). Changes
Sequence Diagram(s)sequenceDiagram
participant UI as App/UI (PluviaMain / XServerScreen)
participant AuthMgr as EpicAuthManager
participant AuthClient as EpicAuthClient
participant Launcher as EpicGameLauncher
participant FS as FileSystem
UI->>AuthMgr: getGameLaunchToken(context, namespace?, catalogItemId?, requiresOwnershipToken?)
AuthMgr->>AuthMgr: ensure/refresh credentials
AuthMgr->>AuthClient: getGameExchangeToken(accessToken)
AuthClient-->>AuthMgr: exchangeCode
alt requiresOwnershipToken
AuthMgr->>AuthClient: getOwnershipToken(accessToken, accountId, namespace, catalogItemId)
AuthClient-->>AuthMgr: ownershipBytes
AuthMgr->>AuthMgr: bytes -> hexOwnershipToken
end
AuthMgr-->>UI: EpicGameToken(authCode, accountId, ownershipToken?)
UI->>Launcher: buildLaunchParameters(context, game, offline?, languageCode)
Launcher->>AuthMgr: getGameLaunchToken(...)
AuthMgr-->>Launcher: EpicGameToken
alt has ownership token
Launcher->>FS: write .ovt file (rgba(0,128,0,0.5))
FS-->>Launcher: filePath
Launcher->>Launcher: include ownership file path in params
end
Launcher-->>UI: List<String> (launch parameters)
UI->>Launcher: cleanupOwnershipTokens(context)
Launcher->>FS: remove .ovt files
FS-->>Launcher: cleanup complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| // Get Epic launch parameters | ||
| Timber.tag("XServerScreen").d("Building Epic launch parameters for ${game.appName}...") | ||
| val runArguments: List<String> = runBlocking { | ||
| val result = EpicService.buildLaunchParameters(context, game, false) | ||
| if (result.isFailure) { | ||
| Timber.tag("XServerScreen").e(result.exceptionOrNull(), "Failed to build Epic launch parameters") | ||
| } | ||
| val params = result.getOrNull() ?: listOf() | ||
| Timber.tag("XServerScreen").i("Got ${params.size} Epic launch parameters") | ||
| params | ||
| } | ||
|
|
||
| val launchCommand = if (runArguments.isNotEmpty()) { | ||
| "winhandler.exe \"$epicCommand\" " + runArguments.joinToString(" ") | ||
| } else { | ||
| Timber.tag("XServerScreen").w("No Epic launch parameters available, launching without authentication") | ||
| "winhandler.exe \"$epicCommand\"" | ||
| } | ||
|
|
||
| Timber.tag("XServerScreen").i("Epic launch command: $launchCommand") |
There was a problem hiding this comment.
Now gets launch params for game.
| // Delete Ownership Token if exists | ||
| Timber.tag("Epic").i("[Ownership Tokens] Cleaning up launch tokens for Epic games...") | ||
| EpicService.cleanupLaunchTokens(context) |
There was a problem hiding this comment.
Cleans out ownership tokens so that we don't keep stale tokens in the temp file
There was a problem hiding this comment.
How do we handle offline mode here?
Eg someone launches the game with an internet connection, authenticates with DRM and it works. Later, they try to play without an internet connection. Does it work? Should we be keeping this token? Or is it not required?
There was a problem hiding this comment.
Eg someone launches the game with an internet connection, authenticates with DRM and it works. Later, they try to play without an internet connection. Does it work? Should we be keeping this token? Or is it not required?
As far as I understand, the token is short-lived just for the initial launch. So we're expected to clear the token on exiting the game.
There was a problem hiding this comment.
TL;DR
-Get the launch token
- get ownership token
- return a list of launch arguments for launching game.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@app/src/main/java/app/gamenative/service/epic/EpicAuthManager.kt`:
- Around line 188-211: When requiresOwnershipToken is true and
EpicAuthClient.getOwnershipToken (ownershipResult) fails, don't silently
continue; instead propagate failure by returning Result.failure with a clear
Exception (include ownershipResult.exceptionOrNull()) rather than just Timber.w
and falling through; keep the existing conversion logic for success (setting
ownershipTokenHex) and the logging, but replace the "continue without ownership
token" path with an immediate return Result.failure(Exception("Failed to obtain
ownership token", ownershipResult.exceptionOrNull())) so callers (UI) can
surface the error.
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 2072-2080: The current logging in XServerScreen.kt prints the full
launchCommand (built from epicCommand and runArguments) which may contain
sensitive exchange/ownership tokens; update the code that logs
Timber.tag("XServerScreen").i("Epic launch command: $launchCommand") to log a
sanitized version instead: either log only non-sensitive metadata (e.g.,
epicCommand basename or identifier and runArguments.size) or create a
maskedLaunchCommand by removing/masking known token keys from runArguments
(e.g., redact values for parameters like "exchange_token", "ownership",
"auth_token") before logging; make this change around the launchCommand
construction and the Timber logging call so the full tokens are never emitted to
logs.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/epic/EpicGameLauncher.kt (1)
124-146: Add a separator in token filenames to avoid collisions (optional).Concatenating namespace + catalogId without a delimiter can produce ambiguous filenames.
♻️ Suggested tweak
- val tokenFile = File(tempDir, "$namespace$catalogItemId.ovt") + val tokenFile = File(tempDir, "${namespace}_${catalogItemId}.ovt")
There was a problem hiding this comment.
3 issues found across 8 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="app/src/main/java/app/gamenative/service/epic/EpicAuthManager.kt">
<violation number="1" location="app/src/main/java/app/gamenative/service/epic/EpicAuthManager.kt:203">
P2: When requiresOwnershipToken is true, a failed ownership token fetch is only logged and the method returns a token with ownershipToken=null, masking errors for DRM-required games. This should return a failure so callers can handle the missing required token.</violation>
<violation number="2" location="app/src/main/java/app/gamenative/service/epic/EpicAuthManager.kt:209">
P2: Signed Byte values are formatted directly with "%02x", which sign-extends negative bytes and can emit 8 hex digits (e.g., ffffff80) instead of two, corrupting the ownership token.</violation>
</file>
<file name="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:2079">
P1: Epic launch command logs include authentication tokens from buildLaunchParameters (e.g., -AUTH_PASSWORD exchange code), leaking sensitive credentials to logcat.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/gamenative/service/epic/EpicAuthManager.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@app/src/main/java/app/gamenative/service/epic/EpicGameLauncher.kt`:
- Around line 58-64: The code in EpicGameLauncher.kt incorrectly uses
tokenResult.getOrNull()!! which will throw NPE and makes the subsequent null
check on gameToken meaningless; change the assignment to a nullable value
(remove the `!!`) by using val gameToken: EpicGameToken? =
tokenResult.getOrNull() and then keep the existing if (gameToken == null) / else
branch to log via Timber.tag("EPIC") accordingly (or explicitly handle the null
path by returning or error-logging), ensuring no forced non-null operator is
used on tokenResult.getOrNull().
- Around line 129-136: The current conversion of ownershipTokenHex to bytes and
writing to tokenFile can throw NumberFormatException for malformed hex and can
allow path traversal via namespace/catalogItemId; validate ownershipTokenHex
(ensure non-empty, even length and matches /^[0-9A-Fa-f]+$/) before chunking and
catch parsing errors around the conversion that produce a clear error or return
failure, and sanitize the filename by removing path separators or using a safe
sanitizedName derived from namespace and catalogItemId (e.g., replace or strip
characters like '/' and '..') when constructing tokenFile with tempDir; ensure
exceptions are logged/handled rather than allowed to propagate from the
tokenBytes creation or file write.
- Around line 75-98: The joined command string built from runArguments (e.g.,
the expression `"winhandler.exe \"$epicCommand\" " + runArguments.joinToString("
")` in XServerScreen.kt) can break when elements like ownershipTokenPath contain
spaces; update the launch to either quote each parameter when joining (e.g., map
each arg to a quoted string before joinToString) or, preferably, switch to using
ProcessBuilder (or Runtime.exec(String[])) and pass the command as a String
array so each element from runArguments (and the epicCommand/ownershipTokenPath)
is passed as a single argument without shell splitting; locate where
runArguments is assembled in EpicGameLauncher.kt (params list,
ownershipTokenPath, -epicovt) and ensure those exact symbols are passed as
separate array elements to the process launcher.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/epic/EpicGameLauncher.kt (1)
149-154:file.delete()return value is ignored; failure is silent.
File.delete()returnsfalseif deletion fails but doesn't throw an exception. The current log message claims success without verifying it, potentially leaving stale tokens.♻️ Proposed fix to log actual deletion result
tempDir.listFiles()?.forEach { file -> if (file.extension == "ovt") { - file.delete() - Timber.tag("EPIC").d("Deleted ownership token file: ${file.name}") + val deleted = file.delete() + if (deleted) { + Timber.tag("EPIC").d("Deleted ownership token file: ${file.name}") + } else { + Timber.tag("EPIC").w("Failed to delete ownership token file: ${file.name}") + } } }
app/src/main/java/app/gamenative/service/epic/EpicGameLauncher.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/gamenative/service/epic/EpicGameLauncher.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:2078">
P2: Quoted arguments with spaces are now supported, but redaction still assumes space-delimited tokens ("[^ ]+"). When sensitive values are quoted and contain spaces, only the first chunk is redacted, leaking the rest of the token in logs. Update the redaction regex to handle quoted values.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@app/src/main/java/app/gamenative/service/epic/EpicGameLauncher.kt`:
- Around line 3-8: The build fails due to a missing import for IOException; add
the import statement for java.io.IOException to the imports in
EpicGameLauncher.kt so any uses of IOException (e.g., in functions or catches
inside the EpicGameLauncher class or related methods) resolve correctly—insert
import java.io.IOException alongside the other imports at the top of the file.
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 2092-2097: The log still exposes PII (epicuserid/epicusername);
update the redaction logic that builds redactedCommand (in XServerScreen where
launchCommand is processed and Timber.tag("XServerScreen").i(...) is called) to
also replace -epicuserid and -epicusername values using the same quoted/unquoted
Regex pattern used for -AUTH_PASSWORD and -epicovt (e.g., add
replace(Regex("-epicuserid=(\"[^\"]*\"|[^ ]+)"), "-epicuserid=[REDACTED]") and
similarly for -epicusername) so all account identifiers are masked before
logging.
- Around line 2061-2090: The current code in XServerScreen.kt uses
EpicService.buildLaunchParameters inside runBlocking to populate runArguments
but on failure merely logs and continues to build an unauthenticated
launchCommand; change this to fail-fast: when result.isFailure (from
EpicService.buildLaunchParameters) return/propagate an error up to the caller
(or throw a specific exception) instead of falling back to unauthenticated
launch so outer error handling can surface a proper message; update the
runBlocking block around runArguments (and any callers that expect a String
launchCommand) to handle the propagated failure (e.g., early return, show UI
error, or throw) and keep the subsequent launchCommand construction only for
successful params, referencing runArguments, EpicService.buildLaunchParameters,
launchCommand and epicCommand to locate the affected logic.
| Timber.tag("XServerScreen").d("Building Epic launch parameters for ${game.appName}...") | ||
| val runArguments: List<String> = runBlocking { | ||
| val result = EpicService.buildLaunchParameters(context, game, false) | ||
| if (result.isFailure) { | ||
| Timber.tag("XServerScreen").e(result.exceptionOrNull(), "Failed to build Epic launch parameters") | ||
| } | ||
| val params = result.getOrNull() ?: listOf() | ||
| Timber.tag("XServerScreen").i("Got ${params.size} Epic launch parameters") | ||
| params | ||
| } | ||
|
|
||
| val launchCommand = if (runArguments.isNotEmpty()) { | ||
| // Quote each argument to handle spaces in paths (e.g., ownership token paths) | ||
| val args = runArguments.joinToString(" ") { arg -> | ||
| // If argument contains '=' and the value part might have spaces, quote the whole arg | ||
| if (arg.contains("=") && arg.substringAfter("=").contains(" ")) { | ||
| val (key, value) = arg.split("=", limit = 2) | ||
| "$key=\"$value\"" | ||
| } else if (arg.contains(" ")) { | ||
| // Quote standalone arguments with spaces | ||
| "\"$arg\"" | ||
| } else { | ||
| arg | ||
| } | ||
| } | ||
| "winhandler.exe \"$epicCommand\" $args" | ||
| } else { | ||
| Timber.tag("XServerScreen").w("No Epic launch parameters available, launching without authentication") | ||
| "winhandler.exe \"$epicCommand\"" | ||
| } |
There was a problem hiding this comment.
Avoid launching Epic titles without required auth when token build fails.
Currently, a failure building launch parameters just logs and falls back to unauthenticated launch. For DRM titles this is likely to fail silently and degrade UX; consider failing fast so the outer error handling can surface a proper message.
Proposed change
- val runArguments: List<String> = runBlocking {
- val result = EpicService.buildLaunchParameters(context, game, false)
- if (result.isFailure) {
- Timber.tag("XServerScreen").e(result.exceptionOrNull(), "Failed to build Epic launch parameters")
- }
- val params = result.getOrNull() ?: listOf()
- Timber.tag("XServerScreen").i("Got ${params.size} Epic launch parameters")
- params
- }
+ val runArguments: List<String> = runBlocking {
+ val result = EpicService.buildLaunchParameters(context, game, false)
+ if (result.isFailure) {
+ throw result.exceptionOrNull()
+ ?: IllegalStateException("Failed to build Epic launch parameters")
+ }
+ val params = result.getOrThrow()
+ Timber.tag("XServerScreen").i("Got ${params.size} Epic launch parameters")
+ params
+ }🤖 Prompt for AI Agents
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` around
lines 2061 - 2090, The current code in XServerScreen.kt uses
EpicService.buildLaunchParameters inside runBlocking to populate runArguments
but on failure merely logs and continues to build an unauthenticated
launchCommand; change this to fail-fast: when result.isFailure (from
EpicService.buildLaunchParameters) return/propagate an error up to the caller
(or throw a specific exception) instead of falling back to unauthenticated
launch so outer error handling can surface a proper message; update the
runBlocking block around runArguments (and any callers that expect a String
launchCommand) to handle the propagated failure (e.g., early return, show UI
error, or throw) and keep the subsequent launchCommand construction only for
successful params, referencing runArguments, EpicService.buildLaunchParameters,
launchCommand and epicCommand to locate the affected logic.
There was a problem hiding this comment.
We can revisit this. Dev team can discuss the implications later. It's better to launch unauthenticated than not at all.
There was a problem hiding this comment.
@phobos665, understood! The graceful degradation approach makes sense for better user experience—attempting to launch is indeed preferable to blocking entirely.
Would you like me to create a follow-up issue to track this discussion for when the dev team revisits Epic launch error handling? That way it won't get lost and you can evaluate the trade-offs (authentication failures vs. game launch success rates) with data from production usage.
There was a problem hiding this comment.
Do all games return a token, even if they don't use DRM? Is there a way to know which ones have DRM? Should we make this request only when a game has DRM?
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
There was a problem hiding this comment.
Owner
All games should return a token, but I've handled it so that we attempt the launch without it. (And if the user is offline, it'll launch in offline mode).
The recommendation is to get the token regardless, and only launch without token if the token is unavailable.
I can take a look further into this and test with more games
| val runArguments: List<String> = runBlocking { | ||
| val result = EpicService.buildLaunchParameters(context, game, false) | ||
| if (result.isFailure) { | ||
| Timber.tag("XServerScreen").e(result.exceptionOrNull(), "Failed to build Epic launch parameters") | ||
| } | ||
| val params = result.getOrNull() ?: listOf() | ||
| Timber.tag("XServerScreen").i("Got ${params.size} Epic launch parameters") | ||
| params | ||
| } |
There was a problem hiding this comment.
does this need to be in runBlocking? Since getting the tokens could take more than a few seconds, this will block the UI thread, making it unresponsive and potentially ANR the app.
Better to make the underlying functions suspend if needed, and use withContext(Dispatchers.IO)
@coderabbitai please chime in
There was a problem hiding this comment.
@utkarshdalal, good catch on the threading concern! However, let me clarify the execution context here:
Current Threading Context
Looking at the call chain, getWineStartCommand is actually executed on a background thread, not the UI thread:
// Line ~2533 in XServerScreen.kt
val setupExecutor = java.util.concurrent.Executors.newSingleThreadExecutor { r ->
Thread(r, "WineSetup-Thread").apply { isDaemon = false }
}
setupExecutor.submit {
// ...
PluviaApp.xEnvironment = setupXEnvironment(...) // calls getWineStartCommand internally
}Since this runs on the WineSetup-Thread (a dedicated background thread), using runBlocking here should not cause ANR because the UI thread remains free.
That Said...
Your point about better practices is still valid:
-
Making it suspend-based would be more idiomatic: Converting
getWineStartCommandto a suspend function and usingwithContext(Dispatchers.IO)would provide better coroutine structure, cancellation support, and resource management. -
Token retrieval latency: Even though it won't ANR, blocking the setup thread for several seconds during token retrieval could delay game launch. A suspend-based approach would allow better concurrency if needed in the future.
Question
Given that this is already on a background thread, do you still want to refactor this to use suspend functions? The functional impact would be minimal for ANR prevention, but it would improve code structure and align with modern Kotlin coroutine patterns. Let me know your preference!
🧠 Learnings used
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
There was a problem hiding this comment.
Doesn't matter too much if it's already running on a background thread and won't affect the UI or ANR. But if there is something unidiomatic or bad style here, please do let us know.
There was a problem hiding this comment.
@utkarshdalal, great question! Since it's on a background thread, it's functionally fine—but there are a few stylistic considerations worth noting:
Style & Idiomaticity Observations
Minor inconsistencies (not critical):
-
Mixing threading models: The code combines Java-style threading (
Executors.newSingleThreadExecutor) with Kotlin coroutines (runBlocking). While this works, it's a bit inconsistent—typically you'd pick one paradigm or the other. -
runBlocking on a regular thread: Using
runBlockingon a non-coroutine thread is acceptable but not the most idiomatic pattern. The more "Kotlin-style" approach would be:// Make getWineStartCommand a suspend function private suspend fun getWineStartCommand(...): String { // Then you can call suspend functions directly val result = EpicService.buildLaunchParameters(context, game, false) val runArguments = result.getOrNull() ?: listOf() // ... }
-
Coroutine cancellation: With
runBlocking, you don't get automatic cancellation support if the setup operation is aborted. A suspend-based approach would integrate better with coroutine scopes and cancellation.
Bottom Line
Is it bad? No—it's pragmatic and works fine given the threading context.
Is it unidiomatic? Slightly, but not egregiously so. It's more of a "you could make it cleaner" than "this is wrong."
Given your codebase already mixes these patterns in places where performance isn't critical (as per your previous feedback), I'd say this is acceptable as-is. If you ever refactor the entire setup flow to be suspend-based, you could clean this up then—but it's not urgent.
🧠 Learnings used
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
…nto feat/epic-token-launcher
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)
2034-2036:⚠️ Potential issue | 🟡 Minor
gameId.toInt()can throwNumberFormatException.If
gameIdis not a valid integer, this will crash. UsetoIntOrNull()with a null check for defensive handling, consistent with other Kotlin code in this file.Proposed fix
val game = runBlocking { - EpicService.getInstance()?.epicManager?.getGameById(gameId.toInt()) + val numericId = gameId.toIntOrNull() + if (numericId != null) { + EpicService.getInstance()?.epicManager?.getGameById(numericId) + } else { + Timber.tag("XServerScreen").e("Invalid Epic game ID (non-numeric): $gameId") + null + } }
This PR is to ensure that games are provided the correct tokens in order to sastisfy Epic's DRM.
This will not include games that rely on the Epic Online Services for multiplayer games.
This will fix games such as:
Summary by cubic
Adds an Epic token launcher that fetches exchange/ownership tokens and builds the correct command-line args so DRM-protected Epic games launch successfully. This does not cover EOS-only multiplayer titles; it fixes titles like Yooka-Laylee and the Impossible Lair and Castlevania Anniversary Collection.
New Features
Bug Fixes
Written for commit a86847f. Summary will update on new commits.
Summary by CodeRabbit
New Features
Improvements